SvelteKit
Call Ridu directly from SvelteKit browsers and server loads with one session and generated types.
@riducms/sveltekit connects a SvelteKit application to Ridu without a proxy. Browsers call Ridu
directly for login, reads, writes, and uploads. Server loads and form actions call Ridu with a
request-scoped client. Both authenticate with the same session token, kept in a cookie on the
application’s own domain, so a SvelteKit site on Vercel and a Ridu API on Railway work together
without third-party cookies or a shared parent domain.
| Operation | Request path |
|---|---|
| Login, CRUD, search, uploads, and logout | Browser → Ridu |
| Server rendering and form actions | SvelteKit → Ridu |
| Private images and downloads | Browser → Ridu, through a short-lived URL |
Install and allow the origin
npm install @riducms/sdk @riducms/sveltekitbun add @riducms/sdk @riducms/sveltekitpnpm add @riducms/sdk @riducms/sveltekityarn add @riducms/sdk @riducms/sveltekitBrowsers call Ridu from the SvelteKit origin, so list it in the CMS’s
AllowedOrigins, for example RIDU_ALLOWED_ORIGINS=https://app.example.com.
The package supports SvelteKit 2.20 and later, and SvelteKit 3.
Bind the generated client
import { PUBLIC_RIDU_URL } from '$env/static/public';
import { defineRidu, type InferClient } from '@riducms/sveltekit';
import { createClient } from '$lib/ridu.generated';
export const ridu = defineRidu({
createClient,
baseURL: PUBLIC_RIDU_URL,
authCollection: 'users'
});
export type Client = InferClient<typeof ridu>;defineRidu holds configuration and types; it never creates a shared authenticated client.
authCollection must be an auth collection, and it types every session’s user. The Ridu URL is
public configuration because browsers call it.
SvelteKit 3 declares environment variables in src/env.ts instead. Mark the URL public there and
import it from $app/env/public:
import { defineEnvVars } from '@sveltejs/kit/env';
export const variables = defineEnvVars({
RIDU_URL: { public: true }
});Then use import { RIDU_URL } from '$app/env/public' and pass baseURL: RIDU_URL.
Add the server hook
import { createRiduHandle } from '@riducms/sveltekit/server';
import { ridu } from '$lib/ridu';
export const handle = createRiduHandle(ridu);import type { Client } from '$lib/ridu';
declare global {
namespace App {
interface Locals {
ridu: Client;
}
}
}
export {};The hook gives each request its own client in event.locals.ridu. Compose it with other hooks
through sequence, which SvelteKit 3 exports from @sveltejs/kit/hooks along with the Handle
type. A server-side login, logout, or rejected token updates the session cookie on the
response.
Server requests go through SvelteKit’s event.fetch. The client sends its token in the
Authorization header with credentials: "omit", so the web app’s own cookies are never forwarded
to Ridu. To use another fetch, pass fetch to defineRidu or createRiduHandle.
Load and provide the session
import { AUTH_DEPENDENCY } from '@riducms/sveltekit';
export const load = async ({ locals, depends }) => {
depends(AUTH_DEPENDENCY);
return { session: await locals.ridu.auth.getSession() };
};<script lang="ts">
import { ridu } from '$lib/ridu';
let { data, children } = $props();
ridu.provide({ session: () => data.session });
</script>
{@render children()}getSession() verifies the token with Ridu once per request and resolves null without a session.
An unavailable Ridu rejects instead, so an outage never looks like a logout. The returned snapshot
contains the user and session metadata, never the token.
provide creates one client for the mounted application. A server render gets an isolated client
that holds no credential. Later layout snapshots refresh the user, but never replace a newer
sign-in or sign-out in the tab.
Server loads
import { redirect } from '@sveltejs/kit';
import { AUTH_DEPENDENCY } from '@riducms/sveltekit';
export const load = async ({ locals, depends }) => {
depends(AUTH_DEPENDENCY);
const session = await locals.ridu.auth.getSession();
if (!session) redirect(303, '/login');
const bookmarks = await locals.ridu.list('bookmarks', {
where: { owner: { equals: session.user.id } },
sort: ['-createdAt']
});
return { bookmarks: bookmarks.docs };
};Declare AUTH_DEPENDENCY in loads whose result depends on who is signed in. Signing in or out reruns
exactly those loads; ordinary writes never trigger a full reload.
Components
<script lang="ts">
import { goto } from '$app/navigation';
import { ridu } from '$lib/ridu';
const client = ridu.use();
let email = $state('');
let password = $state('');
async function submit(event: SubmitEvent) {
event.preventDefault();
await client.auth.login({ email, password });
await goto('/app');
}
</script>
<p>
Signed in as {client.auth.session?.user.displayName ?? 'nobody'}
</p>ridu.use() returns the provided client. Its auth.session is reactive, and CRUD methods such as
client.create, client.update with a revision, and client.upload go straight to Ridu. Failures
remain structured RiduErrors, and the SDK never silently replays a write.
The package also handles the session details applications should not rebuild:
- Login stores the token before its promise resolves, so an immediate navigation is signed in.
- Every request reads the current token instead of one captured when the client was created.
- Logout revokes the session and clears the matching token, even when the server is unreachable.
- Tabs share sign-in and sign-out through
BroadcastChannel; a tab that missed a message, or whose token changed through a form action, catches up when it becomes visible. - A late response for an earlier token cannot restore a previous account or clear a newer login.
- A token Ridu rejects is removed, and the next server render also clears a stale cookie.
- Rotation is never automatic; the session ends at its configured expiry.
Private images
An <img> cannot send the session header. Mint short-lived delivery URLs in the load that renders
the images:
const urls = await locals.ridu.getUploadURLs(
'assets',
assets.map((asset) => ({ id: asset.id, size: 'thumb' })),
{ expiresIn: 1800 }
);The URLs point directly at Ridu, never contain the token, and stop working at logout or when read access ends. See private uploads in the browser.
Security trade-off
The session cookie is readable by scripts on the application’s domain, which is what lets the browser send it to Ridu directly. A cross-site scripting flaw in the application can therefore steal the session token. Ridu’s embedded admin keeps using its HttpOnly cookie. If your application cannot accept a script-readable token, keep Ridu’s cookie behind a same-origin proxy instead.