Skip to main content

SvelteKit SDK

Use @ops-ai/toggly-sveltekit for server hooks, loads and actions in SvelteKit, with a separate browser store for each layout. The server delegates local evaluation to the Node SDK. The browser consumes signed, worker-evaluated definitions and retains entity gates for local entity checks.

Grab the printable SvelteKit cheat sheet (download PDF) — hooks, loads, hydration, Feature + negate.

Requires Svelte 5, SvelteKit 2 and Node 22.12 or later. The current packed host uses Svelte 5.57.0, SvelteKit 2.70.3 and adapter-node 5.5.7. The package requires Node core 0.9.1+ and signed-defs 1.2.6+ for the shared verification contract. The server integration supports adapter-node. See the complete sample for seven navigable sections, filter presets, a missing-key banner and a first-toggle exercise.

Installation​

npm install @ops-ai/toggly-sveltekit

Configure separate keys. TOGGLY_APP_KEY is a backend key and stays in server-only modules. PUBLIC_TOGGLY_APP_KEY is a Front-end App Key and is visible to the browser. In App Settings, generate an additional Front-end App Key, configure the exact allowed local origin, and mark each intended flag Available to Client SDK. Use the same environment on both sides.

TOGGLY_APP_KEY=
PUBLIC_TOGGLY_APP_KEY=
TOGGLY_ENVIRONMENT=Production
PUBLIC_TOGGLY_ENVIRONMENT=Production

Bind server context once per request​

Initialize the Node client once in src/hooks.server.ts. Pass user context to the hook for each request; do not call setIdentity on the shared client from request handling. This anonymous starting point can be connected to your existing authenticated session.

src/hooks.server.ts
import { env } from "$env/dynamic/private";
import { env as publicEnv } from "$env/dynamic/public";
import {
createTogglyClient,
createTogglyHandle,
} from "@ops-ai/toggly-sveltekit/server";

const client = createTogglyClient({
appKey: env.TOGGLY_APP_KEY,
environment: env.TOGGLY_ENVIRONMENT ?? "Production",
verifySignatures: true,
featureDefaults: { "new-dashboard": false },
});
await client.init();
process.once("SIGTERM", () => {
void client.close();
});

export const handle = createTogglyHandle({
client,
context: () => ({ identity: "", groups: [], claims: {} }),
frontend: {
appKey: publicEnv.PUBLIC_TOGGLY_APP_KEY,
environment: publicEnv.PUBLIC_TOGGLY_ENVIRONMENT ?? "Production",
expose: ["new-dashboard", "api-v2", "ExpressCheckout"],
featureDefaults: { "new-dashboard": false },
},
});

The hook copies identity, groups and claims and captures User-Agent, Accept-Language and cf-ipcountry from the request. Your context callback can override the request fields when using a trusted proxy or a controlled test. Do not treat client-supplied country or demo claims as authentication.

clientContext(event, context) is a separate, explicit projection of public targeting data. By default the frontend is anonymous, with no serialized groups or claims. Return the public identity/groups/claims needed for matching frontend rollouts. Never return secrets, access tokens or private session claims. A public snapshot uses the frontend key's worker evaluation, not backend rules or backend-only flags.

Load an allowlisted snapshot​

src/routes/+layout.server.ts
import { loadToggly } from "@ops-ai/toggly-sveltekit/server";
import { env } from "$env/dynamic/public";
import type { LayoutServerLoad } from "./$types";

export const load: LayoutServerLoad = async (event) => ({
toggly: await loadToggly(event),
publicKey: env.PUBLIC_TOGGLY_APP_KEY ?? "",
environment: env.PUBLIC_TOGGLY_ENVIRONMENT ?? "Production",
});

The first loadToggly call fetches a verified frontend snapshot; later calls in that request reuse it. Only keys in expose are serialized. Entity gates remain structured rules instead of being flattened into context-free booleans. SvelteKit serializes the load result; do not manually inject it into an inline script or return event.locals.toggly itself.

Hydrate and dispose a layout-owned store​

src/routes/+layout.svelte
<script lang="ts">
import { onMount, onDestroy, setContext } from 'svelte';
import { createToggly } from '@ops-ai/toggly-sveltekit';
import type { LayoutData } from './$types';
export let data: LayoutData;

const toggly = createToggly(data.toggly, {
appKey: data.publicKey,
environment: data.environment,
});
setContext('toggly', toggly);
$: toggly.update(data.toggly);
onMount(() => {
void toggly.start();
});
onDestroy(() => toggly.dispose());
</script>

<slot />

createToggly initializes synchronously from the server snapshot, so SSR and hydration select the same branch. start() begins browser refresh after mounting. update(snapshot) replaces the current context and definitions immediately and restarts the browser session if mounted; late responses from the previous session cannot overwrite it. After login or logout, invalidate the relevant server load so the layout receives a new snapshot. dispose() stops the socket, reconnect timer and polling timer; late responses cannot publish.

Declarative and programmatic gates​

<script lang="ts">
import { getContext } from 'svelte';
import type { TogglyStore } from '@ops-ai/toggly-sveltekit';
import Feature from '@ops-ai/toggly-sveltekit/Feature.svelte';
const toggly = getContext<TogglyStore>('toggly');
$: dashboard = $toggly && toggly.isEnabled('new-dashboard');
</script>

<Feature {toggly} feature="new-dashboard">
<p>The new dashboard is enabled.</p>
</Feature>
<Feature {toggly} feature="new-dashboard" options={{ negate: true }}>
<p>The classic dashboard is enabled.</p>
</Feature>
<Feature {toggly} feature={['new-dashboard', 'api-v2']} options={{ requirement: 'any' }}>
<p>At least one feature is enabled.</p>
</Feature>
<Feature {toggly} feature="api-v2" options={{ negate: true }}>
<p>The original API interface is visible.</p>
</Feature>
<p>Programmatic result: {dashboard}</p>

Use separate Feature blocks for enabled and disabled content. Keep their feature keys, requirement, entity and defaults identical, and add negate: true to the disabled block. Both evaluate the current snapshot synchronously; no loading state is introduced.

isEnabled(key, { defaultValue }) returns a boolean synchronously. Missing keys default to false unless you supply a call-site default. gate(keys, { requirement: 'all' | 'any', negate, entity }) combines checks; an empty gate is true before negation. These are boolean gates; this SDK does not assign A/B experiment variants.

Entity context​

Pass an explicit mapped entity rather than registering a process-global user-dependent mapper:

const order = {
kind: "Order",
key: "ord-vip",
attributes: { Id: "ord-vip", Vip: true, Total: 150 },
};
const enabled = toggly.isEnabled("ExpressCheckout", { entity: order });
const serverEnabled = await event.locals.toggly.isEnabled("ExpressCheckout", {
entity: order,
});

Create the Order context schema and bind the flag to it in Toggly. A frontend EntityGate without an entity fails closed, even when a missing-key default is true. Backend filters use the request's identity/claims/request context plus the per-call entity. See the filter matrix.

Server load and action guards​

src/routes/+page.server.ts
import { requireFeature } from "@ops-ai/toggly-sveltekit/server";
import type { Actions } from "./$types";

export const actions: Actions = {
submit: async (event) => {
await requireFeature(event, "enhanced-submit");
return { message: "The feature gate allowed this action." };
},
};

requireFeature throws HTTP 404 when its server gate is false. It also accepts an array and requirement, negate and entity options. Keep authentication and authorization in place: hiding a UI element never prevents a direct action request.

Refresh, offline behavior and local gates​

Browser verification is always enabled. allowedKeyIds restricts signing keys; maxSignatureAgeSeconds rejects old signatures. refreshInterval defaults to 180000 ms; zero disables polling. enableLiveUpdates: false disables the WebSocket. Plaintext and JSON update messages trigger unconditional signed requests; ETag invalidations pin the requested revision, and signing-key updates clear the JWKS cache. timeout defaults to 5000 ms and cancels unfinished requests; the adapter reconnects after a disconnect with exponential backoff. A failed refresh preserves the matching SSR or last verified in-memory snapshot. Optional persistent signed storage can restore verified definitions on a fresh browser store; parsed-flag caches are never trusted. On a new server request, a failed frontend fetch falls back to frontend.featureDefaults; it never borrows another user's cached values.

localGates applies device-local prerequisites using the shared local-gates package. Each gate has id, flagKeys and isEnabled(). Call notifyLocalGatesChanged() after local state changes. A local gate can disable a remotely enabled feature; it cannot enable a remotely disabled one. Ensure the initial local state agrees across SSR and hydration.

Persistent signed definitions​

Pass an optional storage adapter to browser configuration to restore matching definitions after a fresh process or browser restart without fetching signing keys:

const storage = {
getItem: (key: string) => window.localStorage.getItem(key),
setItem: (key: string, value: string) =>
window.localStorage.setItem(key, value),
};
const toggly = createToggly(data.toggly, { appKey: data.publicKey, storage });

These callbacks defer browser storage access until start() runs after mounting. The adapter stores a versioned exact signed envelope and only the public key that verified it. It partitions records by endpoint, app, environment and complete identity/groups/claims URL. Restore rechecks the current key pins, public-key constraints/expiry, signature age and complete entity schema before publishing; it never trusts a parsed-flag cache or fetches a URL supplied by a stored record. Failed storage access, corrupt data and verification failure leave defaults or already verified state intact.

loadToggly marks its fallback snapshot source: 'defaults', allowing a fresh browser store to restore the matching signed record before the network attempt. A successful server snapshot carries source: 'signed', its verified signedTimestamp and selected public signingKey. These are trusted host hydration metadata, not portable signed credentials: no raw backend definitions or signed envelope are serialized. Browser allowedKeyIds also applies before signed SSR values seed the UI. Older stored state never replaces signed SSR or live state. The layout retains observed key trust across navigation; current observed keys take precedence over stored keys. Manual snapshots without source metadata remain authoritative; explicitly label application defaults source: 'defaults' when they may be replaced by a verified cache.

Signature timestamps cannot move backwards within a running layout/context. Signing-key notifications replace the in-memory key-cache instance and retire historical stored contexts through an endpoint generation marker. If retirement cannot be saved, the client stops using persistence for that layout session, including navigation and reconnects; repair storage access or clear affected storage before a later restart.

Storage is application/origin-owned local trust material. A party able to replace both stored keys and envelopes can replace that trust anchor unless independent allowedKeyIds pins constrain it. Detecting rollback of the entire store after process loss needs external protected state and is not promised. maxSignatureAgeSeconds is rechecked on restore; unset/nonpositive values disable age expiry. Future timestamps and expired keys are rejected. Storage keys contain targeting data, so apply the application's privacy/logout lifecycle.

Definition persistence does not provide cached HTML/assets, offline server loads or actions. A full offline page launch needs an application-owned offline shell. A server-side snapshot failure still returns only the explicit exposed defaults.

Deployment boundaries​

The /server export is Node-only and must stay in hooks.server.ts, +page.server.ts, +layout.server.ts or $lib/server. Its browser export condition is blocked. Do not import it into universal loads or browser components.

adapter-node runs server hooks, personalized loads and actions at request time. Prerendered output is a build-time snapshot: there is no request-scoped identity or server action runtime in a static deployment. Use static defaults for public pages or keep personalized routes server-rendered. Other adapters require separate validation.