SvelteKit

@loginwith/sveltekit — server-side sessions (BFF), or a browser client.

npm add @loginwith/sveltekit

Two modes, two entry points. Import the one you mean:

ImportModeTokens live
@loginwith/sveltekit/serverBFF — your server runs the sign-inIn an encrypted, httpOnly cookie. Never in the browser.
@loginwith/sveltekit/clientCSR — the tab runs the sign-inIn localStorage. See Browser SDK.

The package root re-exports /client, for older imports. It never re-exports /server, so server code cannot end up in a browser bundle by accident.

Server mode (BFF)

Configure

// src/lib/server/auth.ts
import { createSvelteKitAuth } from "@loginwith/sveltekit/server"
import { env } from "$env/dynamic/private"

export const auth = createSvelteKitAuth({
  api: "https://acme.loginwith.dev",
  issuer: "https://acme.loginwith.dev/",   // trailing slash
  clientId: env.LGW_CLIENT_ID,
  clientSecret: env.LGW_CLIENT_SECRET,     // a confidential client
  loginPage: "https://acme.loginwith.page/",
  sessionSecret: env.LGW_SESSION_SECRET,   // openssl rand -base64 32
})
OptionRequiredDefault
apiyesWhere API requests go — https://acme.loginwith.dev.
issueryesiss exactly as your tokens carry it: https://acme.loginwith.dev/, trailing slash included.
clientIdyesThe client your app signs in as. Its tokens carry it as aud.
clientSecretA confidential client’s secret, sent in the token request body (client_secret_post). Omit for a public client.
sessionSecretyesYour key for encrypting the session cookie.
loginPageyesWhere sign-in starts — https://acme.loginwith.page/ or your custom domain.
loginPath/lgw/loginThe path that starts sign-in.
callbackPath/lgw/callbackThe path the issuer redirects back to.
securetrueThe cookies’ Secure attribute. Set false only for http://localhost.
sessionTtl30 daysSession cookie lifetime, in seconds.
cookieNames{ session: "lgw_session", transaction: "lgw_tx" }Rename either cookie.
hostOverrideDevelopment only: forces the org via ?host= against a localhost issuer.
switchTenantGranturn:loginwith:params:oauth:grant-type:switch-tenantThe tenant-switch grant URN. Leave it unless your issuer is configured otherwise.

Wire it

Four files:

// src/hooks.server.ts                → export const { handle } = auth
// src/routes/lgw/login/+server.ts    → export const GET = auth.login
// src/routes/lgw/callback/+server.ts → export const GET = auth.callback
// src/routes/lgw/logout/+server.ts   → export const POST = auth.logout

Already have a hook? Compose it with SvelteKit’s sequence:

// src/hooks.server.ts
import { sequence } from "@sveltejs/kit/hooks"
import { auth } from "$lib/server/auth"

export const handle = sequence(auth.handle, yourHandle)

Register https://<your app>/lgw/callback as a redirect URI on the client in the console. The redirect URI is built per request from the request’s origin plus callbackPath, so behind a proxy make sure SvelteKit sees your public origin (for adapter-node, set ORIGIN).

Routes

HandlerDoes
auth.loginMints a PKCE verifier, seals it in the lgw_tx cookie, and redirects to loginPage. ?return_to=/path sets where to land afterwards — a path on your origin only; anything else is ignored.
auth.callbackRedeems the code (or a login-page voucher), verifies the token, sets the session cookie, clears lgw_tx, and redirects to return_to (default /). On failure, redirects to loginPath?error=<code> — an HTTP status or sign_in_failed, never the reason’s detail.
auth.logoutRevokes the session at the issuer (best-effort), clears the session cookie, redirects to /. A GET works too.

The routes are +server.ts endpoints, not pages, so link to them with a full navigation:

<a href="/lgw/login?return_to=/dashboard" data-sveltekit-reload>Sign in</a>

<form method="POST" action="/lgw/logout">
  <button>Sign out</button>
</form>

Read the session

Declare what handle sets:

// src/app.d.ts
import type { LoginWithLocals } from "@loginwith/sveltekit/server"

declare global {
  namespace App {
    interface Locals { loginwith: LoginWithLocals }
  }
}
export {}

locals.loginwith is { token, claims } for a signed-in request, null otherwise. token is the access token — send it as Authorization: Bearer to LoginWith or your own API. claims is its payload (sub the user id, tnt the tenant, exp, …).

// src/routes/dashboard/+page.server.ts
import { redirect } from "@sveltejs/kit"

export const load = async ({ locals, fetch }) => {
  const session = locals.loginwith
  if (!session) redirect(302, "/lgw/login?return_to=/dashboard")

  const res = await fetch("https://api.acme.com/projects", {
    headers: { authorization: `Bearer ${session.token}` },
  })
  return { userId: session.claims.sub, projects: await res.json() }
}

The cookies

lgw_sessionlgw_tx
HoldsAccess token, refresh token, expiryPKCE verifier, return_to
EncryptionAES-256-GCM (WebCrypto), key = SHA-256 of sessionSecretSame
AttributeshttpOnly, SameSite=Lax, Secure, Path=/Same
LifetimesessionTtl (30 days)10 minutes — one sign-in

Encrypted, not signed: the tokens cannot be read out of the cookie, not just not altered. A cookie that does not decrypt — wrong key, tampered — is treated as no session. WebCrypto only, so it runs on edge runtimes too.

SameSite=Lax, never Strict: the callback is a cross-site navigation, and Strict would withhold the transaction cookie there.

Refresh is transparent. On each request handle opens the cookie. When the access token is within 10 seconds of expiry, it spends the refresh token, verifies the new access token, and re-sets the cookie in the same response. If the issuer refuses the refresh, the cookie is cleared and locals.loginwith is null. If the issuer cannot be reached, the cookie is left for the next request — a network blip does not sign anyone out.

Machine-to-machine

A confidential client can mint a token for the app itself — no user:

const { accessToken } = await auth.clientCredentials()

It throws if clientSecret is not set, and it is not cached. For a cached, single-flight minter, see createTokenMinter.

auth.core is the framework-agnostic core underneath, for anything the three routes do not cover.

Client mode (CSR)

import { createLoginWith } from "@loginwith/sveltekit/client"

const LoginWith = createLoginWith({
  api: "https://acme.loginwith.dev",
  issuer: "https://acme.loginwith.dev/",   // trailing slash
  clientId: "cli_…",
  loginPage: "https://acme.loginwith.page/",
})
const lgw = new LoginWith(window)

The tab holds the tokens and calls your API with await lgw.getAccessToken(). Nothing in it is SvelteKit-specific: methods, the callback route and storage are on Browser SDK. It needs window, so construct it in the browser only — in onMount, or behind browser from $app/environment.