npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lovable.dev/lovable-auth-js

v1.11.0

Published

Lovable Auth JS

Readme

@lovable.dev/lovable-auth-js

OIDC authentication for TanStack Start apps. Works with the Lovable platform identity, Lovable's OIDC server, or any OpenID Connect provider (Okta, Entra, Auth0, …).

Installation

npm install @lovable.dev/lovable-auth-js

Releases follow semver: depend on ^1.0.0 and every 1.x release is a drop-in update; breaking changes only ship in a new major.

Setups

createAuthServer takes a setup that selects where your app's users come from:

| Setup | What it means | | --- | --- | | "lovable-sso" | Use lovable.dev's SSO capabilities — your app's users are lovable.dev users | | "lovable-auth" | Lovable provisions an OIDC server for your app — it has its own user base | | "custom-oidc" | Integrate with a custom OIDC IdP (Okta, Entra, Auth0, …) |

Quick start (lovable-auth)

Define the auth server once and reuse the same instance for routes and server-side identity reads (see Server API for the options and their env-var defaults):

// app/lib/auth.server.ts
import { createAuthServer } from "@lovable.dev/lovable-auth-js/server";

export const auth = createAuthServer({ setup: "lovable-auth" });

TanStack Start route:

// app/routes/api/auth.$.ts
import { createFileRoute } from "@tanstack/react-router";
import { auth } from "~/lib/auth.server";

export const Route = createFileRoute("/api/auth/$")(auth.routes());

Server-side identity:

import { auth } from "~/lib/auth.server";

const session = await auth.getSession({ request }); // Session | null

// Or request a short-lived access token for a downstream service:
const result = await auth.requestAccessToken({
  request,
  audience: "urn:lovable:service:example_gateway",
  resource: "https://gateway-example.lovable.dev",
  scope: "example_gateway:invoke",
}); // { accessToken, expiresAt } | null

Client:

import { newAuthClient } from "@lovable.dev/lovable-auth-js";

export const authClient = newAuthClient();

await authClient.signIn({ callbackURL: "/account" });

lovable-auth setup binds lovableAppProvider() (override with provider), which reads the following env vars by default:

| Env var | Maps to | | --- | --- | | LOVABLE_AUTH_TENANT_ID | OIDCProvider.issuer = https://<tenantId>.auth.lovable.app; the main Lovable tenant is rejected — use lovableDevProvider() | | LOVABLE_AUTH_CLIENT_ID | OIDCProvider.clientId | | LOVABLE_AUTH_CLIENT_SECRET | OIDCProvider.clientSecret |

Override any of them by passing explicit arguments:

lovableAppProvider({
  tenantId: "abc123",
  clientId: "...",
  clientSecret: "...",
  scopes: ["openid", "email"], // optional; defaults to ["openid", "offline", "email", "profile"]
});

Custom OIDC provider (custom-oidc)

Construct an OIDCProvider and pass it to createAuthServer. Discovery resolves to <issuer>/.well-known/openid-configuration.

import { createAuthServer, type OIDCProvider } from "@lovable.dev/lovable-auth-js/server";

const oktaProvider: OIDCProvider = {
  id: "okta",
  issuer: "https://example.okta.com/oauth2/default",
  clientId: process.env.OKTA_CLIENT_ID!,
  clientSecret: process.env.OKTA_CLIENT_SECRET!,
  scopes: ["openid", "profile", "email", "offline_access"],
};

export const auth = createAuthServer({
  setup: "custom-oidc",
  provider: oktaProvider,
  secret: process.env.AUTH_SECRET!,
  trustedOrigins: ["https://my-app.example.com"],
});

The optional revokeEndpoint field revokes the provider session on sign-out; leave it unset for providers without one — sign-out then completes locally.

To support explicit signup requests, define OIDCProvider.onSignUpAuthorizeHook, for example onSignUpAuthorizeHook: () => ({ screen_hint: "signup" }). The SDK calls it on each signup request and adds its returned authorization query parameters; required OAuth parameters take precedence.

To accept what the app collected before sign-in (loginHint, loginMethod, flow, see SignInHints), define OIDCProvider.onSignInAuthorizeHook. It receives the hints the app passed and returns the authorization parameters your provider understands, for example onSignInAuthorizeHook: ({ loginHint, loginMethod }) => ({ login_hint: loginHint ?? "", connection: loginMethod ?? "" }). Without the hook the SDK forwards loginHint as the standard login_hint and rejects loginMethod and flow with a 400, so an app never silently loses the choice its user made. Signup-hook and required OAuth parameters take precedence over the sign-in hook's.

Lovable SSO (lovable-sso)

Your app's users are lovable.dev users. Visitors who are signed in to lovable.dev are recognized automatically — reading the session needs no sign-in flow in the app:

export const auth = createAuthServer({ setup: "lovable-sso" });

const session = await auth.getSession({ request });
export const authClient = newAuthClient({ setup: "lovable-sso" });

signIn() runs an OAuth sign-in with the visitor's Lovable account, for visitors the platform doesn't recognize automatically; signOut() signs out of the app only and stops automatic recognition on that browser until the next sign-in.

Mounting at a non-default path

If you mount the route at something other than /api/auth/$, pass the same basePath on both sides so server-generated callback URLs and client requests agree:

export const auth = createAuthServer({ setup: "lovable-auth", basePath: "/custom-auth" });
// ...
newAuthClient({ basePath: "/custom-auth" });

Where to call what in TanStack Start

The mounted route is the only wiring; there is no middleware to add. Pick the half that matches where the code runs:

| Where the code runs | Session | Access token | | --- | --- | --- | | Browser (components, handlers) | authClient.useSession() / authClient.getSession() | authClient.requestAccessToken({ audience, scope }) | | Server (loaders, beforeLoad, server functions, SSR) | auth.getSession({ request }) | auth.requestAccessToken({ request, audience, scope }) | | API exposed to OAuth clients (bearer access token) | auth.getSession({ accessToken }) | auth.requestAccessToken({ accessToken, audience, scope }) |

authClient methods are cookie-carrying fetch calls against the mounted routes, so they work only in the browser — a loader running during SSR has no cookie jar. Route guards therefore read the session through a server function:

import { getRequest, setResponseHeader } from "@tanstack/react-start/server";

const getClientSessionFn = createServerFn().handler(async () => {
  setResponseHeader("Cache-Control", "private, no-store");
  return auth.getClientSession({ request: getRequest() });
});

export const Route = createFileRoute("/dashboard")({
  beforeLoad: async () => {
    const session = await getClientSessionFn();
    if (!session) throw redirect({ to: "/" });
    return { session };
  },
});

Use getClientSession for anything a route returns: TanStack Start serializes beforeLoad and loader results into the page, and getSession includes the session token. The Cache-Control header keeps shared caches from storing one user's session response and serving it to another.

Server-rendered session

On its own, authClient.useSession() renders as pending during SSR and fetches the session after hydration. Load the session in the root route's beforeLoad and pass it to AuthSessionProvider: the server render already shows the signed-in state, and child guards read context.session instead of fetching the session again. Mount the provider at the root, above every component that calls useSession().

This is TanStack Start's recommended pattern. The root beforeLoad runs on every navigation and preload, so each one makes a small server call; in return, guards always see the current session.

import { AuthSessionProvider } from "@lovable.dev/lovable-auth-js";

// getClientSessionFn as in the guard example above, with its Cache-Control header.
export const Route = createRootRoute({
  beforeLoad: async () => ({ session: await getClientSessionFn() }),
  headers: () => ({ "Cache-Control": "private, no-store" }),
  component: () => (
    <AuthSessionProvider client={authClient} session={Route.useRouteContext().session}>
      <Outlet />
    </AuthSessionProvider>
  ),
});

getClientSession drops the session token, so the session is safe to render into the page. A page that renders the session is personal: send it with Cache-Control: private, no-store so no shared cache serves it to another user.

After hydration the browser still fetches the session once in the background, so the server can renew the session cookie and a page rendered without the visitor's cookies (prerendered or cached) corrects itself. A signed-in session shows no pending state during that fetch, and its session.token is absent until it completes. A null session renders as pending, on the server too, until the fetch confirms it, so a client-side redirect never acts on an unconfirmed null. Sign-in and sign-out update useSession() on their own; router.invalidate() only refreshes the route context.

authClient.requestAccessToken POSTs to the mounted /access-token route: the server exchanges the current session (RFC 8693) and returns the minted downstream token plus its expiry — the session's own token never reaches the browser, and the minted token is treated as opaque (expiry comes from the exchange response, never from decoding the token). For repeated calls to one API, hold an accessTokenProvider — it caches the token per instance, refreshes ~30s before expiry, and dedupes concurrent mints:

const tokenProvider = authClient.accessTokenProvider({
  audience: "urn:lovable:service:example-gateway",
  scope: "example-gateway:invoke",
});

const response = await fetch(apiUrl, {
  headers: { Authorization: `Bearer ${await tokenProvider.get()}` },
});

Client libraries that take a token callback can be handed () => tokenProvider.get().

Client API

newAuthClient(options?: AuthClientOptions): AuthClient

AuthClient members:

| Member | Returns | Description | | --- | --- | --- | | signIn(options?) | Promise<AuthResult> | Start the OAuth sign-in flow. Picks redirect outside iframes and popup in live preview; override with mode. The popup only performs the login: it always finishes on the SDK's completion page, which hands the session to the opener as a sealed, 60-second handoff (the opener claims it in its own cookie partition, which matters inside a live preview) and closes. Any navigation, such as a hook redirect, happens in the opener. | | signUp(options?) | Promise<AuthResult> | Request the provider's signup flow. Accepts the same options as signIn and requires provider signup support. | | getAccountURL() | string | Local SDK route that redirects to the provider's hosted account page using OIDC discovery. Use it as a link or navigation target. | | openAccount(options?) | OpenAccountResult | Open the hosted account page in its own window, synchronously. mode: "auto" (the default) picks a popup inside an iframe such as the editor preview, and a new tab otherwise; pass "popup" or "tab" to force one. A blocked popup falls back to a tab. Returns { error, mode }, where mode is the window opened. | | signOut(options?) | Promise<SignOutResult> | With setup: "lovable-auth", clear the app session and run RP-initiated logout: a popup in embedded previews, a redirect in standalone apps. Other setups keep local sign-out and their optional revokeEndpoint hook. callbackURL selects the app page to return to. | | getSession() | Promise<SessionState> | Fetch the current session from the auth server. | | requestAccessToken(options?) | Promise<AccessTokenResult \| null> | Request a short-lived access token for resource/audience/scope — the auth server exchanges the current session (RFC 8693) and returns { accessToken, expiresAt }, the expiry taken from the exchange response's expires_in (the token stays opaque). null when anonymous or denied. | | accessTokenProvider(options) | AccessTokenProvider | Cached, single-flight token getter for one target API. Synchronous to construct; get() mints via the same exchange and refreshes ~30s before the expiry the exchange reported. Equal options return the same instance (one cache per target); caches are dropped on sign-in/sign-out. | | useSession | UseSession | React hook returning the current session reactively. Starts from the server session when AuthSessionProvider is mounted (see Server-rendered session). |

For managed Lovable Auth logout, including embedded previews:

await authClient.signOut({ callbackURL: "/signed-out" });

Register <app-origin><basePath>/sign-out/callback in the OAuth client's post_logout_redirect_uris (the default basePath is /api/auth). The SDK validates the logout return, then redirects to callbackURL, which defaults to the current page. The provider must advertise end_session_endpoint. The browser flow replaces the server-side revocation hook for this call.

Call popup logout directly from a click handler so the browser can open it. The popup returns through the same registered callback, notifies the opener, and closes. The opener then navigates to the same-origin callbackURL. Closing or blocking the popup returns an error. Once the local session is cleared, the app remains signed out even if provider logout is canceled. The callback does not clear a newer session created while logout was pending. A logout return the SDK cannot match to a pending logout (expired, replayed, or missing its state cookie) sends the user to the app root; they are already signed out. The lovable-sso setup keeps app-only sign-out so it does not sign the user out of the Lovable editor.

signIn example:

const result = await authClient.signIn({
  callbackURL: "/account",
  scopes: ["openid", "email"],
});
if (result.error) {
  // handle Error
}

To request the provider's signup flow:

await authClient.signUp({ callbackURL: "/account" });

signIn(options) starts the normal sign-in flow; signUp(options) requests registration. Neither changes the provider's registration policy. lovableAppProvider() returns prompt=create and flow=signup from its signup hook. Providers without onSignUpAuthorizeHook, including the default lovable-sso provider, reject explicit signup requests. newUserCallbackURL only controls where a newly registered user returns after authentication.

Sign-in hints

The hosted page owns every credential step, but the app may own the first screen. signIn and signUp accept what the app already collected and the provider continues from there:

| Option | Meaning | Lovable parameter | | --- | --- | --- | | loginHint | Identifier the user typed in the app, usually an email. | login_hint (standard OIDC) | | loginMethod | Method the user picked in the app: "google", "apple", "microsoft", "lovable", "email". | login_method | | flow | Flow to run: "signin" or "signup". signUp() sets it for you. | flow |

// Email collected in the app, account creation on the hosted page.
await authClient.signIn({ callbackURL: "/onboarding", loginHint: email, flow: "signup" });

// "Continue with Google" button rendered in the app.
await authClient.signIn({ callbackURL: "/account", loginMethod: "google" });

// Email sign-in with the email collected in the app; the hosted page handles the credential.
await authClient.signIn({ callbackURL: "/account", loginMethod: "email", loginHint: email });

loginHint works with every provider. loginMethod and flow need OIDCProvider.onSignInAuthorizeHook, which lovableAppProvider() defines; other providers reject them with a 400 unless you add the hook. The hints are strings so any IdP's names fit; for Lovable the accepted values are the LovableLoginMethod and LovableSignInFlow unions (also exported as the LOVABLE_LOGIN_METHODS and LOVABLE_SIGN_IN_FLOWS arrays from @lovable.dev/lovable-auth-js/server), and lovableAppProvider() rejects any other value with a 400 at the signIn call, before the redirect. A hook may throw for the same effect. The hints only steer the hosted page: it still verifies the credential, and a method the project has not enabled ends the flow with login_method_unavailable at errorCallbackURL. Passwords, one-time codes and password resets never run in the app.

To open the hosted account page, call openAccount() from a click handler:

<button
  type="button"
  onClick={() => {
    const { error } = authClient.openAccount();
    setAccountError(error?.message ?? null);
  }}
>
  Manage account
</button>

Inside an embedded preview it opens a popup, because the hosted page refuses to be framed. Outside one it opens a new tab, so the app stays open. Each click opens its own window, named lovable-account-<id>, so a repeat click never reloads an account window that is already open. error is set only when the browser blocks both the popup and the tab. The hosted page shows a Done button that closes the window when it was opened this way. Call it before awaiting anything, or the browser blocks the window.

For a plain link, use getAccountURL():

<a href={authClient.getAccountURL()} target="_blank" rel="noopener noreferrer">Manage account</a>

For same-tab navigation, use window.location.assign(authClient.getAccountURL()). The SDK's /account route passes the provider's full OIDC discovery document to OIDCProvider.onAccountURLHook, after validating its issuer. The hook returns an account URL or null when unavailable. A missing hook or null returns HTTP 404. The URL must be absolute HTTPS (HTTP loopback URLs are allowed for local development). The SDK sends no app tokens or return URL; the hosted page handles its own sign-in.

lovableAppProvider() reads the non-standard account_endpoint field in its hook. Custom providers can interpret their own discovery fields:

onAccountURLHook: (wellKnown) =>
  typeof wellKnown.profile_url === "string" ? wellKnown.profile_url : null

Lovable Auth advertises its login UI base URL plus /v1/account. The public base follows the tenant's vanity URL when configured; local development uses DEV_OAUTH_LOGIN_UI_BASE_URL. Apps using getAccountURL() pick up changes through discovery, which the SDK caches for 10 minutes. The default Lovable SSO provider has no account hook.

getSession returns a discriminated union — when error is non-null, data is null, and vice versa:

const result = await authClient.getSession();
if (result.error) {
  console.error(result.error);
} else if (result.data) {
  console.log("signed in as", result.data.user.email);
}

AuthClientOptions:

| Field | Default | Description | | --- | --- | --- | | setup | — | Must match the server's setup. | | logger | built-in console logger | Replaces the console logger (console, pino, … work). Without logLevel, its own info/debug presence decides verbosity. | | logLevel | "warn" | Lowest level to emit: "error", "warn", "info", "debug", or "none" for silence. | | baseURL | current window origin | Origin of the auth server. | | basePath | /api/auth | Path prefix for the auth endpoints. | | signInMode | "auto" | Default sign-in mode when signIn is called without mode. | | popupFeatures | centred 520×720 | window.open features string. | | popupTimeoutMs | 120_000 | How long to wait for the popup to complete. |

When sign-in fails

A failed sign-in lands on the sign-in's errorCallbackURL with an error code and, when the provider or the session hook gave a reason, error_description. In popup mode signIn resolves with the error instead. A failure with no errorCallbackURL to go to lands on the server's errorURL, or the app root when that is unset. This covers sign-ins that passed none and expired or replayed attempts (state_mismatch, state_expired, state_not_found); in a popup, the popup closes and signIn resolves with an error. Show a message and a way to sign in again on those pages. The error value is a code, not a sentence.

Server API

createAuthServer(options: AuthServerOptions): AuthServer

Returns a configured auth server for the given setup. Hold the result in module scope and reuse it across requests:

| Member | Returns | Description | | --- | --- | --- | | routes() | RegisteredAuthRoutes | TanStack Start route configuration for createFileRoute("/api/auth/$"). | | getSession(options) | Promise<Session \| null> | Resolve the session ({ user, session }) from the app's session cookie (request or headers), or from an explicit bearer accessToken (see Access tokens); one or the other. null when anonymous or the access token is invalid. | | getClientSession(options) | Promise<ClientSession \| null> | getSession without the session token, safe to render into the page; pass it to AuthSessionProvider. Same options. | | getUser(options) | Promise<User \| null> | Fetch the user's profile from the provider's userinfo endpoint with the session's access token. Explicit and remote, unlike getSession; same options. Returns the stored session user when the token lacks the openid scope or the session has no access token. null when anonymous or the provider rejects the token. | | requestAccessToken(options) | Promise<AccessTokenResult \| null> | Request a short-lived access token for resource/audience — an RFC 8693 exchange of the session's access token (or, when the lovable-sso session comes from the platform identity, the platform credential itself: the lovable-auth cookie value or the injected identity token), of a verified bearer accessToken, or of a caller-supplied subjectToken (+ optional subjectTokenType). Returns { accessToken, expiresAt }; null when there is nothing to exchange. | | accessTokenProvider(options) | ServerAccessTokenProvider | Stateless token getter bound to one target API — get({ request }) exchanges that session on every call (same options as getSession). Synchronous to construct, safe at module scope. |

AuthServerOptions is a union discriminated by setup. All setups accept:

| Field | Default | Description | | --- | --- | --- | | provider | lovableDevProvider() in lovable-sso; lovableAppProvider() in lovable-auth; required in custom-oidc | OIDC provider the server is bound to. | | secret | LOVABLE_AUTH_SECRET env var | Signs and encrypts session cookies. createAuthServer throws when neither is set. | | basePath | /api/auth | Path prefix the routes are mounted at; must match the client's basePath. | | trustedOrigins | LOVABLE_AUTH_TRUSTED_ORIGINS env var (comma-separated) | Additional origins accepted as request hosts, Origin headers, and redirect targets. | | fallbackBaseURL | LOVABLE_AUTH_BASE_URL env var | Base URL used when the request host is not a trusted origin. The OAuth routes require this, a non-wildcard trustedOrigins entry, or trustRequestOrigin. | | trustRequestOrigin | true in lovable-sso, else false | Use the request's own origin. If cross-origin requests are needed, use trustedOrigins. | | errorURL | the app root | App page, usually the sign-in page, for sign-in failures that have no errorCallbackURL: the user lands there with ?error=<code> (plus error_description when there is one). A path or a trusted origin. See When sign-in fails. | | logger | built-in console logger | Replaces the console logger (console, pino, … work). Without logLevel, its own info/debug presence decides verbosity. | | logLevel | "warn" | Lowest level to emit: "error", "warn", "info", "debug", or "none" for silence. | | cache | Built-in cache | CacheStore backend (e.g. Redis when self-hosting); null disables caching. See Caching. | | hooks.session.create.before | — | Runs once a user is authenticated, before their session is handed out. Can redirect or deny. See Session hook. |

const gatewayTokens = auth.accessTokenProvider({ audience: "urn:lovable:service:example_gateway" });
// per request:
const token = await gatewayTokens.get({ request });

Unlike the client provider, the server provider never caches a minted token on the instance: a module-scope instance is shared across users, so an instance cache would serve one user's token to another. The built-in exchange cache already dedups per subject. Callers passing subjectToken/subjectTokenType keep using requestAccessToken directly.

lovableAppProvider(options?: LovableAppProviderOptions): OIDCProvider

Convenience helper that produces an OIDCProvider pre-configured for a Lovable app's auth tenant; the lovable-auth setup default. See Quick start for env-var defaults.

lovableDevProvider(options: LovableDevProviderOptions): OIDCProvider

Same, but for the main Lovable tenant (Lovable users themselves): the issuer defaults to https://lovable.dev/oauth and no tenant id is involved. clientId / clientSecret fall back to LOVABLE_AUTH_CLIENT_ID / LOVABLE_AUTH_CLIENT_SECRET. This provider declares no revoke endpoint — sign-out completes locally. The lovable-sso setup binds it automatically for token exchanges.

Access tokens

An API your app exposes to OAuth clients receives an Authorization: Bearer access token the client obtained from your auth tenant (or from Lovable SSO in the lovable-sso setup) instead of a session cookie. Take the token out of the header and pass it as accessToken; it is then the only session proof. request/headers always mean the app's own session cookie and never read the Authorization header.

export async function handleApi(request: Request) {
  const accessToken = request.headers.get("authorization")?.match(/^Bearer\s+(.+)$/i)?.[1];
  const session = accessToken ? await auth.getSession({ accessToken }) : null;
  if (!session) {
    return new Response(null, { status: 401, headers: { "www-authenticate": 'Bearer error="invalid_token"' } });
  }

  const downstream = await auth.requestAccessToken({ accessToken, audience: "urn:lovable:service:example_gateway" });
  // call the downstream API as session.user ...
}

The token is verified against the provider's published keys before anything else: a wrong signature, wrong issuer, or an expired token yields null, and requestAccessToken never forwards such a token to the exchange. Only RFC 9068 access tokens (typ: at+jwt) are accepted, so an ID token cannot be replayed as one. accessTokenValidators adds your own checks on the claims, e.g. requireAudience(apiUrl) to accept only tokens issued for your API.

A session started from an access token is built from the token alone; nothing is fetched. getUser fetches the user's data from the IdP's userinfo endpoint:

const user = await auth.getUser({ accessToken });

Session hook

hooks.session.create.before runs on the server once a user is authenticated, before their session is handed out. Use it to provision app data (a profiles row, with a getOrCreate), to send incomplete profiles to onboarding, to refuse accounts that are not invited or on an approved domain, or to audit sign-ins.

import { APIError, createAuthServer } from "@lovable.dev/lovable-auth-js/server";

export const auth = createAuthServer({
  setup: "lovable-auth",
  hooks: {
    session: {
      create: {
        before: async ({ user }, ctx) => {
          if (!(await invitations.has(user.email))) throw new APIError(403, "not_invited");
          const profile = await profiles.getOrCreate(user.id, { email: user.email, name: user.name });
          if (!profile.complete) throw ctx.redirect(`/onboarding?returnTo=${encodeURIComponent(ctx.callbackURL)}`);
        },
      },
    },
  },
});

ctx carries the request, callbackURL, and redirect(url).

| Outcome | Effect | | --- | --- | | returns nothing | Continue to callbackURL. | | throw ctx.redirect(url) | Go to url instead; it must pass the same trust check as callbackURL. | | return false / throw new APIError(status, message) | Refuse: the session is discarded and the user lands on errorCallbackURL with error=access_denied. | | any other error | Logged; the sign-in continues. |

Under lovable-sso, automatically recognized visitors never sign in, so the hook runs the first time their identity is seen on a session read, cached per user. A refusal hides them; a redirect cannot apply there, so onboarding also needs a route guard.

Caching

Exchanged access tokens, OIDC discovery documents, and signing keys are cached. The cache option controls where:

  • Omitted — the built-in cache: shared and persistent on Lovable hosting, in-memory elsewhere.
  • A CacheStore — your backend (e.g. Redis when self-hosting):
  • null — caching disabled.
const auth = createAuthServer({
  setup: "lovable-auth",
  cache: {
    get: (key) => redis.get(key),
    set: async (key, value, options) => {
      await redis.set(key, value, { EX: options?.ttl ?? 300 });
    },
    delete: async (key) => {
      await redis.del(key);
    },
  },
});

Cache failures are silent: a fresh fetch or exchange is always the fallback.

Types

Type declarations ship with the package — client types from the top-level entry, server types from /server, shared types (Session, AuthSetup, Logger, OIDCProvider) from both.