Client Setup
Set up Jazz in your app — works out of the box with most frameworks, with manual configuration available when needed.
Zero-config client setup
Every client needs an appId and a secret for local-first auth. Without secret, the client runs in anonymous mode and every write is rejected with AnonymousWriteDeniedError. Add a serverUrl to enable sync. For the full auth matrix (anonymous, local-first, external JWT), see Authentication.
export function LocalFirstAuthApp() {
const { secret, isLoading } = useLocalFirstAuth();
if (isLoading || !secret) return null;
return (
<JazzProvider
config={{
appId: "my-app",
secret,
}}
>
<TodoApp />
</JazzProvider>
);
}useLocalFirstAuth() from jazz-tools/react loads the device secret (or generates one on first run) and exposes login / signOut for switching identity.
<script setup lang="ts">
import { createJazzClient, JazzProvider } from "jazz-tools/vue";
import { BrowserAuthSecretStore } from "jazz-tools";
import TodoList from "./TodoList.vue";
const secret = await BrowserAuthSecretStore.getOrCreateSecret();
const client = createJazzClient({
appId: "<your-app-id>",
secret,
});
</script>
<template>
<JazzProvider :client="client">
<h1>Todos</h1>
<TodoList />
</JazzProvider>
</template><script lang="ts">
import {
createJazzClient,
JazzSvelteProvider,
BrowserAuthSecretStore,
} from 'jazz-tools/svelte';
import TodoList from './TodoList.svelte';
let client = $state<ReturnType<typeof createJazzClient> | null>(null);
BrowserAuthSecretStore.getOrCreateSecret().then((secret) => {
client = createJazzClient({ appId: '<your-app-id>', secret });
});
</script>
{#if client}
<JazzSvelteProvider {client}>
{#snippet children()}
<h1>Todos</h1>
<TodoList />
{/snippet}
{#snippet fallback()}
<p>Loading...</p>
{/snippet}
</JazzSvelteProvider>
{/if}export function LocalFirstAuthExpoApp() {
const { secret, isLoading } = useLocalFirstAuth();
if (isLoading || !secret) return null;
return (
<JazzProvider
config={{
appId: "my-app",
secret,
}}
>
<View>
<Text>My App</Text>
<TodoApp />
</View>
</JazzProvider>
);
}ExpoAuthSecretStore persists the secret in the device's secure storage. Or use useLocalFirstAuth() from jazz-tools/expo for a hook-based API.
export async function createLocalFirstDb() {
const secret = await BrowserAuthSecretStore.getOrCreateSecret({ appId: "my-app" });
return createDb({
appId: "my-app",
env: "dev",
userBranch: "main",
secret,
});
}The secret is generated on first load, persisted locally (localStorage in the browser, secure storage on Expo), and reused on every subsequent run — the same device keeps the same Jazz identity.
Using Jazz completely offline? Leave out serverUrl and everything above still works — writes
stay on the device until a server is reachable.
The local-first secret is the user's identity. If it's lost (e.g. cleared localStorage), the
identity is gone and any data owned by that user becomes inaccessible. Plan for recovery with
backup and restore.
Client config options
| Field | Required | Description |
|---|---|---|
appId | yes | Application identifier. Isolates storage between apps. |
serverUrl | no | Jazz sync server URL. Omit for fully local/offline mode. |
secret | no | Local-first auth secret. Jazz derives a stable identity from it. |
jwtToken | no | External auth bearer JWT. Requires server-side JWKS configuration. |
adminSecret | no | Admin secret for privileged sync and /admin/* catalogue operations. |
runtimeSources | no | Runtime source overrides for browser workers, Wasm URLs, or Wasm input. |
driver | no | Storage driver: { type: "persistent" } (default) or { type: "memory" }. |
dbName | no | Local database name. Defaults to appId. |
env | no | Environment label (for example "dev", "prod"). |
userBranch | no | Branch name. Defaults to "main". |
For React Native and Expo clients, make sure serverUrl points to a host your runtime can reach. For example, the Android emulator often needs 10.0.2.2 instead of localhost.
Storage driver
Browser clients default to driver: { type: "persistent" }, which stores data locally for offline support. Set driver: { type: "memory" } to keep data in memory only. This option requires you to set a serverUrl since nothing is saved locally.
Dev plugins
Jazz ships bundler plugins that remove boilerplate in development. They start a local Jazz dev server, watch schema.ts and permissions.ts, auto-push both on change, and inject the app ID and server URL as framework-appropriate env vars so createJazzClient({ appId, serverUrl }) picks them up without any manual wiring.
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { jazzPlugin } from "jazz-tools/dev/vite";
export default defineConfig({
plugins: [react(), jazzPlugin()],
});Injects VITE_JAZZ_APP_ID and VITE_JAZZ_SERVER_URL into import.meta.env.
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { jazzPlugin } from "jazz-tools/dev/vite";
export default defineConfig({
plugins: [vue(), jazzPlugin()],
});Injects VITE_JAZZ_APP_ID and VITE_JAZZ_SERVER_URL into import.meta.env.
import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { jazzSvelteKit } from "jazz-tools/dev/sveltekit";
export default defineConfig({
plugins: [svelte(), jazzSvelteKit()],
});Injects PUBLIC_JAZZ_APP_ID and PUBLIC_JAZZ_SERVER_URL. Use jazzSvelteKit for both Vite+Svelte and SvelteKit projects.
import { sveltekit } from "@sveltejs/kit/vite";
import { jazzSvelteKit } from "jazz-tools/dev/sveltekit";
import { defineConfig } from "vite";
export default defineConfig({
// jazzSvelteKit must come before sveltekit so it populates
// process.env before SvelteKit captures $env/dynamic/public.
plugins: [jazzSvelteKit(), sveltekit()],
});Defaults schemaDir to src/lib/. Injects PUBLIC_JAZZ_APP_ID and PUBLIC_JAZZ_SERVER_URL.
import { withJazz } from "jazz-tools/dev/next";
export default withJazz({});Injects NEXT_PUBLIC_JAZZ_APP_ID and NEXT_PUBLIC_JAZZ_SERVER_URL. Only runs in the Next.js dev phase; production builds are untouched.
import { getDefaultConfig } from "expo/metro-config";
import { withJazz } from "jazz-tools/dev/expo";
const config = getDefaultConfig(__dirname);
await withJazz(config, { schemaDir: __dirname });
export default config;Injects EXPO_PUBLIC_JAZZ_APP_ID and EXPO_PUBLIC_JAZZ_SERVER_URL via expoConfig.extra. Skipped automatically when NODE_ENV=production.
On first run the plugin generates an app ID, persists it to .env, and starts a local Jazz server under node_modules/.cache/jazz-dev-server/. Every subsequent run reuses both.
Env var names
Each plugin writes the same two values (appId, serverUrl) under the prefix its bundler uses to expose env vars to the client bundle. SvelteKit's PUBLIC_* prefix also covers Svelte+Vite via jazzSvelteKit. Secrets are always unprefixed — they're server-only: JAZZ_ADMIN_SECRET and BACKEND_SECRET.
| Bundler | App ID | Server URL |
|---|---|---|
| Vite | VITE_JAZZ_APP_ID | VITE_JAZZ_SERVER_URL |
| Next.js | NEXT_PUBLIC_JAZZ_APP_ID | NEXT_PUBLIC_JAZZ_SERVER_URL |
| SvelteKit | PUBLIC_JAZZ_APP_ID | PUBLIC_JAZZ_SERVER_URL |
| Expo | EXPO_PUBLIC_JAZZ_APP_ID | EXPO_PUBLIC_JAZZ_SERVER_URL |
Jazz Cloud sync URL: https://v2.sync.jazz.tools/. For self-hosted deployments, point the server URL at your own host.
Plugin options
All plugins accept the same base options:
| Option | Description |
|---|---|
server | true (default) starts an embedded local server. false disables the plugin. A URL string connects to an existing server. An object configures the embedded server (see below). |
schemaDir | Directory containing schema.ts and permissions.ts. Defaults to the project root, or src/lib/ for SvelteKit. |
appId | Override the app ID. Defaults to the value in .env, otherwise a generated UUID persisted on first run. |
adminSecret | Required when server is a URL. Ignored for the embedded server (a random secret is generated). |
When server is an object, the embedded server accepts: port (default: random), dataDir (default: node_modules/.cache/jazz-dev-server), inMemory, allowLocalFirstAuth, jwksUrl, and the catalogue authority forwarding fields. See Server Setup for the full semantics.
Connecting to an existing server
Pass a URL to point the plugin at a server you already run — hosted cloud, staging, or a shared self-hosted instance. You must also provide adminSecret (or set JAZZ_ADMIN_SECRET) so the plugin can push schema and permissions.
jazzPlugin({
server: "https://my-jazz-server.example.com",
adminSecret: process.env.JAZZ_ADMIN_SECRET,
appId: process.env.VITE_JAZZ_APP_ID,
});What auto-pushes
On startup and on every change to schema.ts or permissions.ts, the plugin publishes the current structural schema and permissions bundle to the server. Structural schema push works without an admin secret in development, so a bare schema.ts edit is enough to see clients pick up the new shape. Permission pushes use the server's admin secret (generated for the embedded server, supplied by you for a remote server).
Auto-push covers the development loop. For production — or any change that needs a schema migration — run pnpm dlx jazz-tools@alpha deploy <appId> to publish the migration, the new schema, and the current permissions in one step. See Migrations.
Runtime source overrides
Use runtimeSources when Jazz can't automatically find its internal assets. This usually only happens with non-standard bundler configurations or on edge runtimes like Cloudflare Workers.
| Field | Use it when |
|---|---|
runtimeSources.baseUrl | Jazz runtime assets are served from a shared base path like /assets/jazz/ |
runtimeSources.wasmUrl | The Wasm file has an explicit public URL |
runtimeSources.workerUrl | The worker script has an explicit public URL |
runtimeSources.wasmSource | Your runtime gives you Wasm bytes directly |
runtimeSources.wasmModule | Your runtime gives you a precompiled WebAssembly.Module |
Jazz resolves these in this order:
runtimeSources.wasmModuleruntimeSources.wasmSourceruntimeSources.wasmUrl/runtimeSources.workerUrlruntimeSources.baseUrl- built-in zero-config fallback
Browser asset overrides
The config is the same across frameworks. Plain TypeScript uses createDb(...) directly.
export function AppWithRuntimeSources() {
return (
<JazzProvider
config={{
appId: "my-app",
serverUrl: "https://my-jazz-server.example.com",
runtimeSources: {
baseUrl: "/assets/jazz/",
},
}}
fallback={<p>Loading...</p>}
>
{/* ... */}
</JazzProvider>
);
}<script setup lang="ts">
import { createJazzClient, JazzProvider } from "jazz-tools/vue";
const client = createJazzClient({
appId: "my-app",
serverUrl: "https://my-jazz-server.example.com",
runtimeSources: {
baseUrl: "/assets/jazz/",
},
});
</script>
<template>
<JazzProvider :client="client">
<!-- ... -->
</JazzProvider>
</template><script lang="ts">
import { JazzProvider, createJazzClient } from "jazz-tools/svelte";
const client = createJazzClient({
appId: "my-app",
serverUrl: "https://my-jazz-server.example.com",
runtimeSources: {
baseUrl: "/assets/jazz/",
},
});
</script>
<JazzProvider {client}>
<!-- ... -->
</JazzProvider>const db = await createDb({
appId: "my-app",
serverUrl: "https://my-jazz-server.example.com",
runtimeSources: {
wasmUrl: "/static/jazz/jazz_wasm_bg.wasm",
workerUrl: "/static/jazz/worker/jazz-worker.js",
},
});Use baseUrl when both assets live together under one public directory. Use explicit wasmUrl / workerUrl when your app serves them from different places.
Edge-style Wasm setup
Some runtimes (like Cloudflare Workers) don't load Jazz the same way a browser does. In those cases, provide the Wasm module or bytes directly.
const db = await createDb({
appId: "my-app",
runtimeSources: {
wasmModule: jazzWasmModule,
},
});If your platform hands you raw bytes instead of a compiled module, use runtimeSources.wasmSource:
const db = await createDb({
appId: "my-app",
runtimeSources: {
wasmSource: wasmBytes,
},
});See the Cloudflare Wrangler example for a complete workerd setup.