> Markdown version of https://authpi.com/docs/quickstarts/sveltekit/ — fetch the complete AuthPI docs index at https://authpi.com/llms.txt to discover all available pages.

# Add authentication to a SvelteKit app

Add authentication to your [SvelteKit](https://svelte.dev/docs/kit) app with AuthPI — using the AuthPI SDK or any OIDC client library.

## Overview

This guide walks through adding AuthPI authentication to a SvelteKit application. Every code section shows two approaches side by side:

- **AuthPI SDK** (`@authpi/idp`) — the official SDK with built-in PKCE, token refresh, and agent management.
- **Arctic** (`arctic`) — a lightweight, generic OAuth 2.0 client where you wire up endpoints yourself.

Both implement the Authorization Code flow with PKCE (S256), using SvelteKit server routes (`+server.ts`) and a server `load` function so tokens never reach the browser.

### Prerequisites

- An AuthPI account with an **Issuer** configured
- A registered **Client** (Web type) with redirect URI set to `http://localhost:5173/auth/callback`
- Your **Client ID** and **Client Secret** noted
- A test **User** created in your Issuer

## Install

**AuthPI SDK:**
```bash
npm install @authpi/idp
```

**Arctic:**
```bash
npm install arctic
```

## Environment variables

Create a `.env` file in your project root. SvelteKit exposes non-public variables to server code via `$env/static/private`, and `PUBLIC_`-prefixed ones to both server and browser via `$env/static/public`:

```bash
AUTHPI_ISSUER_URL=https://idp.authpi.com/i_your_issuer_id
AUTHPI_CLIENT_ID=c_your_client_id
AUTHPI_CLIENT_SECRET=your_client_secret
PUBLIC_BASE_URL=http://localhost:5173
```

## Configure the client

**AuthPI SDK:**
```typescript
// src/lib/server/auth.ts
import { IdpClient } from "@authpi/idp";
import { AUTHPI_ISSUER_URL, AUTHPI_CLIENT_ID, AUTHPI_CLIENT_SECRET } from "$env/static/private";
import { PUBLIC_BASE_URL } from "$env/static/public";

export const authClient = new IdpClient({
  issuerUrl: AUTHPI_ISSUER_URL,
  clientId: AUTHPI_CLIENT_ID,
  clientSecret: AUTHPI_CLIENT_SECRET,
  redirectUri: `${PUBLIC_BASE_URL}/auth/callback`,
});
```

**Arctic:**
```typescript
// src/lib/server/auth.ts
import * as arctic from "arctic";
import { AUTHPI_ISSUER_URL, AUTHPI_CLIENT_ID, AUTHPI_CLIENT_SECRET } from "$env/static/private";
import { PUBLIC_BASE_URL } from "$env/static/public";

export const issuerUrl = AUTHPI_ISSUER_URL;

export const oauthClient = new arctic.OAuth2Client(
  AUTHPI_CLIENT_ID,
  AUTHPI_CLIENT_SECRET,
  `${PUBLIC_BASE_URL}/auth/callback`
);
```

## Login route

Redirect the user to AuthPI's authorization endpoint. Store the PKCE code verifier and state in httpOnly cookies so they survive the redirect. SvelteKit requires an explicit `path` on every cookie.

**AuthPI SDK:**
```typescript
// src/routes/auth/login/+server.ts
import { redirect } from "@sveltejs/kit";
import { dev } from "$app/environment";
import { authClient } from "$lib/server/auth";
import type { RequestHandler } from "./$types";

export const GET: RequestHandler = async ({ cookies }) => {
  const { url, codeVerifier, state } = await authClient.createAuthorizationUrl({
    scopes: ["openid", "profile", "email"],
  });

  const cookieOptions = { httpOnly: true, secure: !dev, path: "/", maxAge: 60 * 10, sameSite: "lax" as const };
  cookies.set("code_verifier", codeVerifier, cookieOptions);
  cookies.set("oauth_state", state, cookieOptions);

  redirect(302, url);
};
```

**Arctic:**
```typescript
// src/routes/auth/login/+server.ts
import { redirect } from "@sveltejs/kit";
import { dev } from "$app/environment";
import * as arctic from "arctic";
import { oauthClient, issuerUrl } from "$lib/server/auth";
import type { RequestHandler } from "./$types";

export const GET: RequestHandler = async ({ cookies }) => {
  const state = arctic.generateState();
  const codeVerifier = arctic.generateCodeVerifier();

  const url = oauthClient.createAuthorizationURLWithPKCE(
    `${issuerUrl}/authorize`,
    state,
    arctic.CodeChallengeMethod.S256,
    codeVerifier,
    ["openid", "profile", "email"]
  );

  const cookieOptions = { httpOnly: true, secure: !dev, path: "/", maxAge: 60 * 10, sameSite: "lax" as const };
  cookies.set("code_verifier", codeVerifier, cookieOptions);
  cookies.set("oauth_state", state, cookieOptions);

  redirect(302, url.toString());
};
```

## Callback route

Exchange the authorization code for tokens, then store them in a session cookie. In production you should encrypt this cookie — here we keep it simple with JSON.

**AuthPI SDK:**
```typescript
// src/routes/auth/callback/+server.ts
import { redirect, error } from "@sveltejs/kit";
import { dev } from "$app/environment";
import { authClient } from "$lib/server/auth";
import type { RequestHandler } from "./$types";

export const GET: RequestHandler = async ({ url, cookies }) => {
  const code = url.searchParams.get("code");
  const state = url.searchParams.get("state");

  const storedState = cookies.get("oauth_state");
  const codeVerifier = cookies.get("code_verifier");

  if (!code || !state || !storedState || !codeVerifier || state !== storedState) {
    error(400, "Invalid callback parameters");
  }

  const agent = await authClient.exchangeCode(code, codeVerifier);

  cookies.set("session", JSON.stringify(agent.tokens), {
    httpOnly: true,
    secure: !dev,
    path: "/",
    maxAge: 60 * 60 * 24 * 7, // 7 days
    sameSite: "lax",
  });

  cookies.delete("code_verifier", { path: "/" });
  cookies.delete("oauth_state", { path: "/" });

  redirect(302, "/dashboard");
};
```

**Arctic:**
```typescript
// src/routes/auth/callback/+server.ts
import { redirect, error } from "@sveltejs/kit";
import { dev } from "$app/environment";
import { oauthClient, issuerUrl } from "$lib/server/auth";
import type { RequestHandler } from "./$types";

export const GET: RequestHandler = async ({ url, cookies }) => {
  const code = url.searchParams.get("code");
  const state = url.searchParams.get("state");

  const storedState = cookies.get("oauth_state");
  const codeVerifier = cookies.get("code_verifier");

  if (!code || !state || !storedState || !codeVerifier || state !== storedState) {
    error(400, "Invalid callback parameters");
  }

  const tokens = await oauthClient.validateAuthorizationCode(
    `${issuerUrl}/token`,
    code,
    codeVerifier
  );

  const sessionData = {
    accessToken: tokens.accessToken(),
    refreshToken: tokens.refreshToken(),
    idToken: tokens.idToken(),
    expiresAt: tokens.accessTokenExpiresAt().toISOString(),
  };

  cookies.set("session", JSON.stringify(sessionData), {
    httpOnly: true,
    secure: !dev,
    path: "/",
    maxAge: 60 * 60 * 24 * 7, // 7 days
    sameSite: "lax",
  });

  cookies.delete("code_verifier", { path: "/" });
  cookies.delete("oauth_state", { path: "/" });

  redirect(302, "/dashboard");
};
```

## Protected page

Read the session cookie in a server `load` function and pass user information to the page. The AuthPI SDK can reconstitute an agent from stored tokens and will auto-refresh expired access tokens. With arctic, you fetch the userinfo endpoint manually.

**AuthPI SDK:**
```typescript
// src/routes/dashboard/+page.server.ts
import { redirect } from "@sveltejs/kit";
import { authClient } from "$lib/server/auth";
import type { PageServerLoad } from "./$types";

export const load: PageServerLoad = async ({ cookies }) => {
  const sessionCookie = cookies.get("session");

  if (!sessionCookie) {
    redirect(302, "/auth/login");
  }

  const tokens = JSON.parse(sessionCookie);
  const agent = await authClient.createAgent(tokens);

  return { userId: agent.id, email: agent.email };
};
```

**Arctic:**
```typescript
// src/routes/dashboard/+page.server.ts
import { redirect } from "@sveltejs/kit";
import { issuerUrl } from "$lib/server/auth";
import type { PageServerLoad } from "./$types";

export const load: PageServerLoad = async ({ cookies }) => {
  const sessionCookie = cookies.get("session");

  if (!sessionCookie) {
    redirect(302, "/auth/login");
  }

  const session = JSON.parse(sessionCookie);

  const response = await fetch(`${issuerUrl}/userinfo`, {
    headers: { Authorization: `Bearer ${session.accessToken}` },
  });

  if (!response.ok) {
    redirect(302, "/auth/login");
  }

  const user = await response.json();

  return { userId: user.sub, email: user.email };
};
```

Render the returned data in the page component:

```svelte
<!-- src/routes/dashboard/+page.svelte -->
<script lang="ts">
  import type { PageData } from "./$types";

  let { data }: { data: PageData } = $props();
</script>

<main>
  <h1>Dashboard</h1>
  <p>User ID: {data.userId}</p>
  <p>Email: {data.email}</p>
  <a href="/auth/logout">Log out</a>
</main>
```

## Logout route

Revoke the refresh token, clear the session cookie, and redirect to AuthPI's logout endpoint so the IdP session is also terminated.

**AuthPI SDK:**
```typescript
// src/routes/auth/logout/+server.ts
import { redirect } from "@sveltejs/kit";
import { authClient } from "$lib/server/auth";
import { PUBLIC_BASE_URL } from "$env/static/public";
import type { RequestHandler } from "./$types";

export const GET: RequestHandler = async ({ cookies }) => {
  const sessionCookie = cookies.get("session");

  if (sessionCookie) {
    const tokens = JSON.parse(sessionCookie);

    if (tokens.refreshToken) {
      await authClient.revokeToken(tokens.refreshToken, "refresh_token");
    }
  }

  const logoutUrl = authClient.createLogoutUrl({
    postLogoutRedirectUri: PUBLIC_BASE_URL,
  });

  cookies.delete("session", { path: "/" });

  redirect(302, logoutUrl);
};
```

**Arctic:**
```typescript
// src/routes/auth/logout/+server.ts
import { redirect } from "@sveltejs/kit";
import { oauthClient, issuerUrl } from "$lib/server/auth";
import { PUBLIC_BASE_URL } from "$env/static/public";
import type { RequestHandler } from "./$types";

export const GET: RequestHandler = async ({ cookies }) => {
  const sessionCookie = cookies.get("session");

  if (sessionCookie) {
    const session = JSON.parse(sessionCookie);

    if (session.refreshToken) {
      await oauthClient.revokeToken(`${issuerUrl}/revoke`, session.refreshToken);
    }
  }

  const logoutUrl = new URL(`${issuerUrl}/logout`);
  logoutUrl.searchParams.set("post_logout_redirect_uri", PUBLIC_BASE_URL);

  cookies.delete("session", { path: "/" });

  redirect(302, logoutUrl.toString());
};
```

## Next steps

You now have a working login/logout flow. From here you can:

- Add [Organizations](/docs/concepts/organizations) support for multi-tenant access control
- Subscribe to [Events](/docs/concepts/events) for audit logging
- Set up [Webhooks](/docs/guides/webhooks) for real-time notifications when users sign up, update their profile, or change authentication methods

The AuthPI SDK handles token refresh automatically via `createAgent()`. If you are using arctic, you will need to implement refresh logic manually by calling the `/token` endpoint with `grant_type=refresh_token` when the access token expires.