Skip to content

Add sveltekit cloudflare pages docs #415

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
---
page_id: 4f3a9e2a-12eb-4b4c-8790-48b6e09a224d
Copy link
Collaborator

@clairekinde11 clairekinde11 May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the following as front matter:

---
page_id: 4f3a9e2a-12eb-4b4c-8790-48b6e09a224d
title: Kinde with Sveltekit on Cloudflare Pages
sidebar:
  order: 9
relatedArticles:
  - 855e5ca8-f2fb-4162-a594-10cee8a2ff8b
  - f1ba22b9-b35f-478a-be09-4524d060fe36
  - 00d62179-e0e8-489c-90f7-9a593f3b058a
---

title: Kinde with SvelteKit on Cloudflare Pages
sidebar:
order: 9
relatedArticles:
- 855e5ca8-f2fb-4162-a594-10cee8a2ff8b
- f1ba22b9-b35f-478a-be09-4524d060fe36
- 00d62179-e0e8-489c-90f7-9a593f3b058a
---

# Kinde with SvelteKit on Cloudflare Pages

This guide walks you through implementing Kinde authentication in a SvelteKit application deployed to Cloudflare Pages using js-utils with KV storage.

<Aside type="warning">
Your Kinde application must be configured as a **Single Page Application** (SPA) to enable PKCE flow, which js-utils requires.
</Aside>

<Aside type="tip">
This implementation leverages **@kinde/js-utils** to handle the OAuth flow automatically, resulting in much cleaner code compared to manual implementations.
</Aside>

## What you need

- A Cloudflare account with Pages and KV access
- A Kinde account with an SPA application configured
- SvelteKit project ready for Cloudflare Pages deployment

## Step 1: Install dependencies

```bash
npm install @kinde/js-utils
npm install -D @sveltejs/adapter-cloudflare
```

## Step 2: Configure Cloudflare KV storage

1. In Cloudflare dashboard, go to **Workers & Pages > KV**
2. Create a new namespace (e.g., `AUTH_STORAGE`)
3. Copy the namespace ID

## Step 3: Configure environment variables

Update your `wrangler.toml`:

```toml
name = "your-project-name"
compatibility_date = "2023-06-28"
compatibility_flags = ["nodejs_compat_v2"]
pages_build_output_dir = "./build"

kv_namespaces = [
{ binding = "AUTH_STORAGE", id = "your-namespace-id" }
]

[vars]
KINDE_ISSUER_URL = "https://your-kinde-domain.kinde.com"
KINDE_CLIENT_ID = "your-client-id"
KINDE_REDIRECT_URL = "https://your-domain.pages.dev/api/auth/kinde_callback"
KINDE_POST_LOGIN_REDIRECT_URL = "https://your-domain.pages.dev/dashboard"
KINDE_POST_LOGOUT_REDIRECT_URL = "https://your-domain.pages.dev"
KINDE_AUTH_WITH_PKCE = "true"
KINDE_SCOPE = "openid profile email offline"
KINDE_DEBUG = "false"
```

Add your client secret securely:

```bash
npx wrangler secret put KINDE_CLIENT_SECRET
```

## Step 4: Create hybrid storage adapter

Create `src/lib/kindeAuth.ts`:

```typescript
import {
KvStorage,
setActiveStorage,
setInsecureStorage,
type SessionManager
} from '@kinde/js-utils';
import type { RequestEvent } from '@sveltejs/kit';

/**
* Cookie storage for temporary OAuth data (state, nonce, code verifier)
*/
class CookieStorage implements SessionManager {
constructor(private event: RequestEvent) {}

async setSessionItem(key: string, value: unknown): Promise<void> {
const cookieValue = typeof value === 'string' ? value : JSON.stringify(value);
this.event.cookies.set(`kinde_${key}`, cookieValue, {
path: '/',
maxAge: 3600,
httpOnly: true,
secure: true,
sameSite: 'lax'
});
}

async getSessionItem(key: string): Promise<unknown | null> {
const value = this.event.cookies.get(`kinde_${key}`);
if (!value) return null;

try {
return JSON.parse(value);
} catch {
return value;
}
}

async removeSessionItem(key: string): Promise<void> {
this.event.cookies.delete(`kinde_${key}`, { path: '/', secure: true });
}

async removeItems(keys: string[]): Promise<void> {
for (const key of keys) {
await this.removeSessionItem(key);
}
}

async clearSession(): Promise<void> {
const cookiesToClear = ['state', 'nonce', 'codeVerifier'];
for (const key of cookiesToClear) {
this.event.cookies.delete(`kinde_${key}`, { path: '/', secure: true });
}
}
}

/**
* Initialize Kinde authentication with hybrid storage:
* - KV storage for tokens (eventual consistency is fine)
* - Cookie storage for OAuth temp data (immediate consistency required)
*/
export function initializeKindeAuth(event: RequestEvent): boolean {
const platform = event.platform as any;
const env = platform?.env;
const AUTH_STORAGE = env?.AUTH_STORAGE;

if (!AUTH_STORAGE) {
console.error('KV storage not available');
return false;
}

// KV storage for long-term token storage
const tokenStorage = new KvStorage(AUTH_STORAGE, { defaultTtl: 2592000 });

// Cookie storage for temporary OAuth data
const tempStorage = new CookieStorage(event);

// Set up js-utils storage
setActiveStorage(tokenStorage); // For tokens
setInsecureStorage(tempStorage); // For OAuth temp data

Comment on lines +138 to +157
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid re-registering global storage on every request
setActiveStorage and setInsecureStorage mutate singleton state inside @kinde/js-utils. Calling them on every request can introduce race conditions under high concurrency and unnecessary object churn.

Suggested pattern:

-let tokenStorage: KvStorage | undefined;
-let tempStorage: CookieStorage | undefined;
-
-export function initializeKindeAuth(event: RequestEvent): boolean {
-
-  // KV storage for long-term token storage
-  tokenStorage = tokenStorage ?? new KvStorage(AUTH_STORAGE, { defaultTtl: 2592000 });
-  // Cookie storage for temporary OAuth data
-  tempStorage = new CookieStorage(event);
-  setActiveStorage(tokenStorage);
-  setInsecureStorage(tempStorage);
-
-}
+const globalTokenStorage = new WeakMap<object, KvStorage>();
+
+export function initializeKindeAuth(event: RequestEvent): boolean {
+
+  if (!globalTokenStorage.has(AUTH_STORAGE)) {
+    globalTokenStorage.set(AUTH_STORAGE, new KvStorage(AUTH_STORAGE, { defaultTtl: 2592000 }));
+    setActiveStorage(globalTokenStorage.get(AUTH_STORAGE)!);
+  }
+  setInsecureStorage(new CookieStorage(event)); // per-request, fine
+
+}

That ensures the heavy KV instance is initialised once per process while keeping per-request cookie storage.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx
around lines 138 to 157, the calls to setActiveStorage and setInsecureStorage
are made on every request, which can cause race conditions and unnecessary
object creation. Refactor the code to initialize the KV storage and call
setActiveStorage once per process outside the request handler, while keeping the
CookieStorage and setInsecureStorage calls inside the request handler to
maintain per-request state. This avoids re-registering global storage on every
request and improves concurrency safety.

return true;
}
```

## Step 5: Create authentication endpoints

Create `src/routes/api/auth/[...kindeAuth]/+server.ts`:

```typescript
import { json, redirect } from '@sveltejs/kit';
import type { RequestEvent } from "@sveltejs/kit";
import {
generateAuthUrl,
exchangeAuthCode,
frameworkSettings,
IssuerRouteTypes,
Scopes,
type LoginOptions
} from '@kinde/js-utils';
import { initializeKindeAuth } from '$lib/kindeAuth';
import {
KINDE_ISSUER_URL,
KINDE_CLIENT_ID,
KINDE_REDIRECT_URL,
KINDE_POST_LOGIN_REDIRECT_URL,
KINDE_POST_LOGOUT_REDIRECT_URL,
KINDE_DEBUG
} from '$env/static/private';

// Configure js-utils
frameworkSettings.framework = 'sveltekit';

const getConfig = () => ({
issuerURL: KINDE_ISSUER_URL,
clientID: KINDE_CLIENT_ID,
redirectURL: KINDE_REDIRECT_URL,
postLoginRedirectURL: KINDE_POST_LOGIN_REDIRECT_URL,
postLogoutRedirectURL: KINDE_POST_LOGOUT_REDIRECT_URL,
debug: KINDE_DEBUG === 'true'
});

export async function GET(event: RequestEvent) {
// Initialize storage for every request
if (!initializeKindeAuth(event)) {
return json({ error: 'KV storage not available' }, { status: 500 });
}

const path = event.params.kindeAuth ?? '';
const config = getConfig();

switch (path) {
case 'login':
return handleLogin(event, config, { isRegister: false });

case 'register':
return handleLogin(event, config, { isRegister: true });

case 'kinde_callback':
return handleCallback(event, config);

case 'logout':
return handleLogout(config);

default:
return json({ error: 'Unknown auth endpoint' }, { status: 404 });
}
}

async function handleLogin(
event: RequestEvent,
config: ReturnType<typeof getConfig>,
options: { isRegister: boolean }
) {
const url = new URL(event.request.url);
const orgCode = url.searchParams.get('org_code');

const loginOptions: LoginOptions = {
issuerRouteType: options.isRegister ? IssuerRouteTypes.register : IssuerRouteTypes.login,
scopes: [Scopes.openid, Scopes.profile, Scopes.email, Scopes.offline],
...(orgCode && { orgCode })
};

// Let js-utils handle the complete auth URL generation and state management
const authUrl = await generateAuthUrl(loginOptions);
return redirect(302, authUrl);
}

async function handleCallback(event: RequestEvent, config: ReturnType<typeof getConfig>) {
const url = new URL(event.request.url);
const error = url.searchParams.get('error');

if (error) {
return json({ error: `OAuth error: ${error}` }, { status: 400 });
}

try {
// Let js-utils handle the complete token exchange
await exchangeAuthCode({
urlParams: url.searchParams,
domain: config.issuerURL,
clientId: config.clientID,
clientSecret: '', // Not needed for PKCE flow
redirectUri: config.redirectURL
});

return redirect(302, config.postLoginRedirectURL);

} catch (error) {
// Handle expected window error in server environment
if (error instanceof Error && error.message.includes('window')) {
// Wait for async token storage to complete
await new Promise(resolve => setTimeout(resolve, 2000));
return redirect(302, config.postLoginRedirectURL);
Comment on lines +266 to +270
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Replace arbitrary 2 s sleep with deterministic wait
Using setTimeout(2000) to “wait for async token storage” is brittle: slow writes may still race; fast writes waste 2 s. Prefer awaiting the promise returned by exchangeAuthCode (it already resolves after storage) or expose a flush() helper from your storage adapter.

-      await new Promise(resolve => setTimeout(resolve, 2000));
+      // Storage write completed inside exchangeAuthCode; no need to delay.

If @kinde/js-utils truly emits the window error before the promise settles, consider opening an issue upstream instead of hard-coding a delay.

🤖 Prompt for AI Agents
In
src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx
around lines 266 to 270, replace the arbitrary 2-second setTimeout delay used to
wait for async token storage with a deterministic wait by awaiting the promise
returned by the async token storage function (such as exchangeAuthCode) or by
calling a flush() method from the storage adapter if available. This ensures the
code only proceeds after token storage completes, avoiding brittle fixed delays.

}

console.error('Authentication error:', error);
return json({ error: 'Authentication failed' }, { status: 500 });
}
}

async function handleLogout(config: ReturnType<typeof getConfig>) {
const logoutUrl = new URL('/logout', config.issuerURL);
logoutUrl.searchParams.append('redirect', config.postLogoutRedirectURL);

return redirect(302, logoutUrl.toString());
}
Comment on lines +278 to +283
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Logout endpoint doesn’t clear local session data
Redirecting to Kinde’s /logout without first deleting session cookies or KV tokens means the user can still appear authenticated until the IdP callback returns. Clear both stores before redirecting:

 async function handleLogout(config: ReturnType<typeof getConfig>) {
+  // Clear local auth
+  await setActiveStorage().removeSession(); // pseudo-code – adjust to actual js-utils API
+
   const logoutUrl = new URL('/logout', config.issuerURL);
   logoutUrl.searchParams.append('redirect', config.postLogoutRedirectURL);
   return redirect(302, logoutUrl.toString());
 }

This ensures instant sign-out and avoids token reuse in subsequent requests.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function handleLogout(config: ReturnType<typeof getConfig>) {
const logoutUrl = new URL('/logout', config.issuerURL);
logoutUrl.searchParams.append('redirect', config.postLogoutRedirectURL);
return redirect(302, logoutUrl.toString());
}
async function handleLogout(config: ReturnType<typeof getConfig>) {
// Clear local auth
await setActiveStorage().removeSession(); // pseudo-code – adjust to actual js-utils API
const logoutUrl = new URL('/logout', config.issuerURL);
logoutUrl.searchParams.append('redirect', config.postLogoutRedirectURL);
return redirect(302, logoutUrl.toString());
}
🤖 Prompt for AI Agents
In
src/content/docs/integrate/third-party-tools/kinde-sveltekit-with-cloudflare.mdx
around lines 278 to 283, the logout function redirects to the logout URL without
clearing local session data, which can cause the user to appear authenticated
temporarily. Modify the function to clear session cookies and any KV tokens or
local storage related to authentication before performing the redirect. This
ensures immediate sign-out and prevents token reuse in subsequent requests.

```

## Step 6: Check authentication in protected routes

For protected routes, use js-utils authentication check:

```typescript
// src/routes/dashboard/+page.server.ts
import type { PageServerLoad } from './$types';
import { isAuthenticated, getUserProfile } from '@kinde/js-utils';
import { initializeKindeAuth } from '$lib/kindeAuth';
import { redirect } from '@sveltejs/kit';

export const load: PageServerLoad = async (event) => {
if (!initializeKindeAuth(event)) {
throw redirect(302, '/api/auth/login');
}

const authenticated = await isAuthenticated();

if (!authenticated) {
throw redirect(302, '/api/auth/login');
}

const userProfile = await getUserProfile();

return {
authenticated: true,
user: userProfile
};
};
```

## Usage

Once configured, you can use standard links for authentication:

- Login: `/api/auth/login`
- Register: `/api/auth/register`
- Logout: `/api/auth/logout`

Use `isAuthenticated()` and `getUserProfile()` from js-utils in your server-side code to check authentication status and retrieve user data.

## Troubleshooting

**"invalid_client" error**: Ensure your Kinde application is configured as a **Single Page Application** (SPA).

**Window errors in server logs**: These are expected in server environments and are handled gracefully.

Your Kinde authentication should now be working seamlessly with SvelteKit on Cloudflare Pages!
Loading