convex-auth-sveltekit
v0.1.0
Published
Convex Auth integration for SvelteKit — server-side token management, automatic refresh, and route protection
Maintainers
Readme
convex-auth-sveltekit
Convex Auth integration for SvelteKit. Server-side token management, automatic JWT refresh, and route protection — matching what @convex-dev/auth/nextjs provides for Next.js.
What this does
- Keeps refresh tokens server-side — stored in httpOnly cookies, never exposed to client JavaScript
- Automatic token refresh — refreshes JWTs before they expire on every server request
- Auth proxy — routes signIn/signOut through your server so the real refresh token never leaves the cookie
- Route protection — composable route matching for protected/public paths
- Client WebSocket auth — wires the Convex WebSocket to server-managed tokens
Install
npm install convex-auth-sveltekitPeer dependencies
npm install convex @convex-dev/auth @sveltejs/kit svelteSetup
1. Type definitions
Add the auth locals to your src/app.d.ts:
declare global {
namespace App {
interface Locals {
token: string | null;
isAuthenticated: boolean;
}
}
}
export {};2. Server hook
Create or update src/hooks.server.ts:
import { sequence } from "@sveltejs/kit/hooks";
import { redirect } from "@sveltejs/kit";
import type { Handle } from "@sveltejs/kit";
import {
createConvexAuthHandle,
createRouteMatcher,
} from "convex-auth-sveltekit/server";
import { CONVEX_URL } from "$env/static/private";
// Handles token refresh and populates event.locals.
const convexAuth = createConvexAuthHandle({ convexUrl: CONVEX_URL });
// Your custom route protection.
const isProtectedRoute = createRouteMatcher(["/dashboard", "/app", "/settings"]);
const isAuthRoute = createRouteMatcher(["/auth"]);
const routeProtection: Handle = async ({ event, resolve }) => {
if (isProtectedRoute(event.url.pathname) && !event.locals.isAuthenticated) {
throw redirect(303, "/auth/login");
}
if (isAuthRoute(event.url.pathname) && event.locals.isAuthenticated) {
throw redirect(303, "/dashboard");
}
return resolve(event);
};
export const handle = sequence(convexAuth, routeProtection);3. Auth proxy endpoint
Create src/routes/api/auth/+server.ts:
import { createAuthProxy } from "convex-auth-sveltekit/server";
import { CONVEX_URL } from "$env/static/private";
export const POST = createAuthProxy({ convexUrl: CONVEX_URL });4. Token endpoint
Create src/routes/api/auth/token/+server.ts (or wherever fits your route structure):
import { createTokenEndpoint } from "convex-auth-sveltekit/server";
export const GET = createTokenEndpoint();5. Client-side setup
In your authenticated layout (e.g. src/routes/(app)/+layout.svelte):
<script lang="ts">
import { setupConvex, useConvexClient } from "convex-svelte";
import { setupConvexAuth } from "convex-auth-sveltekit/client";
import { browser } from "$app/environment";
import { PUBLIC_CONVEX_URL } from "$env/static/public";
import { setContext } from "svelte";
let { data, children } = $props();
setupConvex(PUBLIC_CONVEX_URL);
const client = useConvexClient();
let isAuthed = $state(false);
setContext("convexAuth", { get isAuthed() { return isAuthed; } });
if (browser) {
setupConvexAuth(client, {
onAuthChange: (authenticated) => { isAuthed = authenticated; },
});
}
</script>
{@render children()}6. Server-side data loading
In +page.server.ts or +layout.server.ts:
import { getConvexClient } from "convex-auth-sveltekit/server";
import { api } from "$convex/_generated/api";
import { CONVEX_URL } from "$env/static/private";
export const load = async ({ locals }) => {
const client = getConvexClient(CONVEX_URL, locals.token);
const user = await client.query(api.src.users.queries.getCurrentUser, {});
return { user };
};7. Sign-in with form actions
In src/routes/auth/login/+page.server.ts:
import { proxySignIn } from "convex-auth-sveltekit/server";
import { fail, redirect } from "@sveltejs/kit";
import { CONVEX_URL } from "$env/static/private";
export const actions = {
sendCode: async ({ request, cookies, url }) => {
const formData = await request.formData();
const host = url.hostname;
const result = await proxySignIn(
{ provider: "resend-otp", params: { email: formData.get("email") } },
cookies,
host,
CONVEX_URL,
);
if (result.error) return fail(400, { error: result.error });
return { codeSent: true };
},
verifyCode: async ({ request, cookies, url }) => {
const formData = await request.formData();
const host = url.hostname;
const result = await proxySignIn(
{
provider: "resend-otp",
params: {
email: formData.get("email"),
code: formData.get("code"),
},
},
cookies,
host,
CONVEX_URL,
);
if (result.error) return fail(400, { error: result.error });
if (result.token) throw redirect(303, "/dashboard");
return fail(400, { error: "Verification failed." });
},
};8. Sign-out
<script lang="ts">
import { signOut } from "convex-auth-sveltekit/client";
</script>
<button onclick={() => signOut()}>Log out</button>API Reference
Server (convex-auth-sveltekit/server)
| Export | Description |
|--------|-------------|
| createConvexAuthHandle(config) | SvelteKit handle hook — refreshes tokens, populates locals |
| createAuthProxy(config) | POST handler factory for the auth proxy endpoint |
| createTokenEndpoint() | GET handler factory that returns the current JWT |
| createRouteMatcher(patterns) | Creates a path-matching function for route protection |
| getConvexClient(convexUrl, token?) | Creates a ConvexHttpClient, optionally authenticated with a JWT |
| proxySignIn(args, cookies, host, convexUrl) | Low-level signIn proxy for form actions |
| proxySignOut(cookies, host, convexUrl) | Low-level signOut proxy |
| getAuthTokens(cookies, host) | Reads JWT and refresh token from cookies |
| setAuthCookies(cookies, tokens, host) | Sets or clears auth cookies |
| refreshTokensIfNeeded(cookies, host, convexUrl) | Refreshes JWT if expiring soon |
| extractErrorMessage(raw) | Extracts user-friendly message from Convex errors |
Client (convex-auth-sveltekit/client)
| Export | Description |
|--------|-------------|
| setupConvexAuth(client, options?) | Wires a Convex client to server-managed auth tokens |
| signOut(options?) | Signs out via the auth proxy and redirects |
| signIn(args, options?) | Sends a signIn request through the auth proxy |
How it works
On every request, the
handlehook decodes the JWT from the cookie, checks if it's expiring soon, and refreshes it via Convex if needed. The new tokens are set back as cookies.Sign-in/sign-out go through the
/api/authproxy. The browser sends a dummy refresh token; the proxy swaps it with the real one from the httpOnly cookie before forwarding to Convex. The response splits the tokens: JWT goes to the client, refresh token stays in the cookie.The client Convex WebSocket authenticates by fetching the JWT from
/api/auth/token. It never sees the refresh token.
License
MIT
