Client Setup

Set up Jazz in your app — works out of the box with most frameworks, with manual configuration available when needed.

Configure one session

A Jazz session owns account selection and its current client. Give it the app configuration once, with initial: "local-first" to restore a saved account or start offline without signup. Its commands handle graceful shutdown and context replacement when authentication changes. See Lifecycle.

App.tsx
export function LocalFirstAuthApp({ config }: { config: JazzSessionConfig }) {
  return (
    <JazzSessionProvider config={{ ...config, initial: "local-first" }}>
      <TodoApp />
    </JazzSessionProvider>
  );
}

useJazzSession() exposes account state and bound auth commands in both the app and provider fallback.

App.vue
<script setup lang="ts">
import type { JazzSession, JazzClient } from "jazz-tools/vue";
import { JazzSessionProvider } from "jazz-tools/vue";
// Configure once with await createJazzSession({ appId, serverUrl, initial: "local-first" }).
defineProps<{ session: JazzSession<JazzClient> }>();
</script>
<template>
  <JazzSessionProvider :session="session"><slot /></JazzSessionProvider>
</template>

Use useJazzSession() to observe the shared session in Vue.

App.svelte
<script lang="ts">
  import type { Snippet } from "svelte";
  import type { JazzSessionConfig } from "jazz-tools/svelte";
  import { JazzSessionProvider } from "jazz-tools/svelte";
  let { config, children }: { config: JazzSessionConfig; children: Snippet } = $props();
</script>
<JazzSessionProvider config={{ ...config, initial: "local-first" }}>{@render children()}</JazzSessionProvider>

Use getJazzSession() for a Svelte store with account state and bound commands.

App.tsx
import { type ParentProps } from "solid-js";
import type { JazzSession } from "jazz-tools/solid";
import type { JazzClient } from "jazz-tools/client";
import { JazzSessionProvider } from "jazz-tools/solid";

// Configure once with await createJazzSession({ appId, serverUrl, initial: "local-first" }).
export function AuthLocalfirst(props: ParentProps<{ session: JazzSession<JazzClient> }>) {
  return (
    <JazzSessionProvider session={props.session} fallback={<p>Loading...</p>}>
      {props.children}
    </JazzSessionProvider>
  );
}

Use useJazzSession() for Solid session state and bound commands.

app.ts
export async function createLocalFirstSession() {
  return createJazzSession({
    appId: "my-app",
    serverUrl: "https://core.example",
    initial: "local-first",
  });
}

The browser account store retains the local signing root and selection. Provider JWTs are refreshed through getToken and are never persisted in this store.

React Native and Expo

React Native and Expo use the same session API with platform-specific runtime and account storage adapters:

import { JazzSessionProvider } from "jazz-tools/expo";

function App() {
  return (
    <JazzSessionProvider config={{ appId, serverUrl, initial: "local-first" }}>
      <TodoApp />
    </JazzSessionProvider>
  );
}

Install jazz-rn directly in the application alongside jazz-tools, add its Expo config plugin, and build the native app. Expo Go cannot load the relay. A bare React Native host uses the same direct dependency and New Architecture, with an OS-protected account store. The canonical Expo scaffold shows the complete lifecycle and platform configuration. The alpha has not yet proven two physical JSI runtimes attached to one relay.

For a local-only context, pass { ...config, serverUrl: undefined }. The handle keeps its registry authority; writes stay locally durable. A later context using the matching server URL can synchronize them.

The local-first root is that identity's signing credential. If it is lost, the account needs another linked identity or a recovery backup to regain access. Plan for recovery with backup and restore.

Client config options

FieldRequiredDescription
appIdyesApplication identifier. Isolates storage between apps.
accountyesOpaque AccountHandle returned by the account helpers. Fixes account and acting identity for this context.
serverUrlnoJazz sync server URL. Omit for fully local/offline mode.
runtimeSourcesnoRuntime source overrides for browser workers, Wasm URLs, or Wasm input.
drivernoStorage driver: { type: "persistent" } (default) or { type: "memory" }.
dbNamenoLogical local database base name. Jazz derives a separate physical browser database for each app, environment, and authentication scope. Defaults to appId.
envnoEnvironment label (for example "dev", "prod").
initialSyncFlushEverynoAdvanced initial-sync durability boundary, in writes. Defaults to 512 for clients.
logLevelnoAdvanced Wasm log level for debugging and benchmarks. Defaults to "warn".
telemetryCollectorUrlnoAdvanced OTLP/HTTP collector URL for Wasm trace telemetry.
devModenoAdvanced switch that enables runtime tracing for DevTools diagnostics.

Provide external credentials to accounts.registerJWT, accounts.loginJWT, or accounts.linkJWT through getToken. Contexts receive only the resulting handle; raw JWTs, cookie-session mirrors, and signing roots are not public context configuration.

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. Memory storage is also usable locally, but its data disappears when the context closes.

When you supply driver: { type: "persistent", dbName }, the name is a logical base. Jazz derives the physical root from registry, app, environment, and account. Linked identities share that account's durable root while opening distinct live authorization sessions. A root whose recorded scope does not match is rejected.

Browser worker failures

After the shared worker acknowledges startup, runtime initialisation has a five-minute grace period. If it does not become ready, startup reports an error instead of leaving the client pending indefinitely. Suspended tabs receive fresh grace when they resume.

While control requests are waiting for worker replies, Jazz probes the connection every 30 seconds and allows 30 seconds for a response. These are connection-health checks, not query or storage-operation timeouts: a responsive worker can continue a long operation, and idle connections are not continuously probed.

An unresponsive-worker error does not prove that an outstanding operation failed. It may already have completed. Jazz does not automatically retry operations with an unknown outcome or clear IndexedDB. Use the existing client and session lifecycle to recover, and establish a mutation's outcome before retrying it. Terminal shutdown reports the causal failure without waiting indefinitely for an unresponsive worker; it does not claim that an unacknowledged durable handoff succeeded.

An intentional storage reset releases the old foreground identity without returning it to the erased store. deleteClientStorage() waits for a fresh identity before the same application client can be used again. If shutdown starts during that acquisition, Jazz retires the unused identity instead. Inspector attachments are session-scoped: after a reset, open a new attachment rather than reusing the old one.

Browser storage eviction

{ type: "persistent" } writes to IndexedDB. By default browsers treat this as best-effort storage — under disk pressure, or after long inactivity, the browser can clear it without warning. For a local-first app that means a returning user can find their identity (the local-first secret) and any data owned only by that user wiped.

The trade-offs are:

  • Best-effort (default). No prompt, no friction. Acceptable when the server holds a copy of every row the user cares about, or when the app gracefully reseeds from sync. Anything that exists only on this device is at risk.
  • Persistent. Storage is only cleared by an explicit user action (clearing site data, uninstalling). Required for true offline-first apps and for any data that doesn't round-trip through a server. Browsers gate this behind navigator.storage.persist(), which may show a prompt or grant silently based on engagement heuristics.

To request persistent storage, call it once after the user has signed in or interacted enough that a prompt makes sense:

if (navigator.storage?.persist) {
  const granted = await navigator.storage.persist();
  if (!granted) {
    // Fall back: rely on sync, warn the user, or retry later.
  }
}

Check navigator.storage.persisted() on subsequent loads to see whether the grant is still in effect. Pair this with the backup and restore flow so the local-first secret can be recovered if storage is ever cleared.

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 for the account-manager bootstrap. Prepare a handle using those values, then pass { appId, serverUrl, account } to the client.

vite.config.ts
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.

vite.config.ts
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.

vite.config.ts
import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { jazzPlugin } from "jazz-tools/dev/vite";

export default defineConfig({
  plugins: [svelte(), jazzPlugin()],
});

Injects VITE_JAZZ_APP_ID and VITE_JAZZ_SERVER_URL into import.meta.env, matching this guide's Vite app bootstrap.

vite.config.ts
import { defineConfig } from "vite";
import solid from "vite-plugin-solid";
import { jazzPlugin } from "jazz-tools/dev/vite";

export default defineConfig({
  plugins: [solid(), jazzPlugin()],
});

Injects VITE_JAZZ_APP_ID and VITE_JAZZ_SERVER_URL into import.meta.env.

vite.config.ts
import { sveltekit } from "@sveltejs/kit/vite";
import { jazzSvelteKit } from "jazz-tools/dev/sveltekit";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [sveltekit(), jazzSvelteKit()],
});

Defaults schemaDir to src/lib/. Injects PUBLIC_JAZZ_APP_ID and PUBLIC_JAZZ_SERVER_URL.

next.config.ts
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.

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.

BundlerApp IDServer URL
ViteVITE_JAZZ_APP_IDVITE_JAZZ_SERVER_URL
Next.jsNEXT_PUBLIC_JAZZ_APP_IDNEXT_PUBLIC_JAZZ_SERVER_URL
SvelteKitPUBLIC_JAZZ_APP_IDPUBLIC_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:

OptionDescription
servertrue (default) starts an embedded local server. false disables managed-server startup. A URL string connects to an existing server. An object configures the embedded server (see below).
schemaDirDirectory containing schema.ts and permissions.ts. Defaults to the project root, or src/lib/ for SvelteKit.
appIdOverride the app ID. Defaults to the value in .env, otherwise a generated UUID persisted on first run.
adminSecretRequired when server is a URL. For the embedded server, used as its admin secret unless server.adminSecret is set; otherwise a random secret is generated.

When server is an object, the embedded server accepts: port (default: random), appId, adminSecret, dataDir (default: node_modules/.cache/jazz-dev-server), inMemory, allowLocalFirstAuth, and jwksUrl. 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.

vite.config.ts
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 deploys the current schema and permissions using the resolved admin secret (from server.adminSecret, root adminSecret, or a generated embedded-server secret). Both files are required. If a schema change requires a migration, prepare the migration before deploying; the plugin reports missing deployment inputs instead of publishing just the schema.

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.

FieldUse it when
runtimeSources.baseUrlJazz runtime assets are served from a shared base path like /assets/jazz/
runtimeSources.wasmUrlThe Wasm file has an explicit public URL
runtimeSources.brokerWorkerUrlThe browser broker worker has an explicit public URL
runtimeSources.wasmVersionAn immutable deployed build version for configured browser asset URLs
runtimeSources.wasmSourceYour runtime gives you Wasm bytes directly
runtimeSources.wasmModuleYour runtime gives you a precompiled WebAssembly.Module

Jazz resolves these in this order:

  1. runtimeSources.wasmModule
  2. runtimeSources.wasmSource
  3. runtimeSources.wasmUrl / runtimeSources.brokerWorkerUrl
  4. runtimeSources.baseUrl
  5. built-in zero-config fallback

Browser asset overrides

The prepared config is the same across frameworks. Plain TypeScript passes it directly to createDb. Prepare the account manager with the same runtime asset overrides.

App.tsx
// Prepare this handle with the same runtimeSources and registry authority.
export function AppWithRuntimeSources({ account }: { account: AccountHandle }) {
  return (
    <JazzProvider
      config={{
        appId: "my-app",
        account,
        serverUrl: "https://my-jazz-server.example.com",
        runtimeSources: {
          baseUrl: "/assets/jazz/",
          wasmVersion: "2026-08-25", // Change this for every deployed asset build.
        },
      }}
      fallback={<p>Loading...</p>}
    >
      {/* Your app's main component */}
      <TodoList />
    </JazzProvider>
  );
}
App.vue
<script setup lang="ts">
import { computed } from "vue";
import type { AccountHandle } from "jazz-tools";
import { JazzProvider } from "jazz-tools/vue";

const props = defineProps<{ account: AccountHandle }>();
// Prepare the handle with these same runtimeSources.
const config = computed(() => ({
  account: props.account,
  appId: "my-app",
  serverUrl: "https://my-jazz-server.example.com",
  runtimeSources: {
    baseUrl: "/assets/jazz/",
    wasmVersion: "2026-08-25", // Change this for every deployed asset build.
  },
}));
</script>

<template>
  <JazzProvider :config="config">
    <!-- ... -->
  </JazzProvider>
</template>
App.svelte
<script lang="ts">
  import type { Snippet } from "svelte";
  import type { AccountHandle } from "jazz-tools";
  import { JazzSvelteProvider } from "jazz-tools/svelte";
  // Prepare the handle with these same runtimeSources.
  let { account, children }: { account: AccountHandle; children: Snippet } = $props();
  const config = $derived({
    appId: "my-app", serverUrl: "https://my-jazz-server.example.com", account,
    runtimeSources: { baseUrl: "/assets/jazz/", wasmVersion: "2026-08-25" },
  });
</script>
<JazzSvelteProvider {config}>{@render children()}</JazzSvelteProvider>
App.tsx
import { type ParentProps } from "solid-js";
import type { AccountHandle } from "jazz-tools";
import { JazzProvider } from "jazz-tools/solid";

export function RuntimeConfigExample(props: ParentProps<{ account: AccountHandle }>) {
  return (
    <JazzProvider
      config={{
        appId: "my-app",
        account: props.account, // Prepare with these same runtimeSources.
        serverUrl: "https://my-jazz-server.example.com",
        runtimeSources: {
          baseUrl: "/assets/jazz/",
          wasmVersion: "2026-08-25", // Change this for every deployed asset build.
        },
      }}
    >
      {props.children}
    </JazzProvider>
  );
}
app.ts
const config = {
  appId: "my-app",
  serverUrl: "https://my-jazz-server.example.com",
  runtimeSources: {
    wasmUrl: "/static/jazz/jazz_wasm_bg.wasm",
    wasmVersion: "2026-08-25", // Change this for every deployed asset build.
  },
};
const accounts = await createAccountManager(config);
const account = accounts.getLoggedIn() ?? accounts.createLocalFirst();
const db = await createDb({ ...config, account });

Use baseUrl when both assets live together under one public directory. Use explicit wasmUrl / brokerWorkerUrl when your app serves them from different places.

For browser URL overrides, also set wasmVersion to an immutable value that changes whenever either the Wasm or broker-worker build changes (for example, your deployment's asset manifest version). Jazz adds that value to both asset URLs, so a rolling deployment cannot reuse a long-lived SharedWorker with bytes fetched from an older build at the same URL. Your asset host must serve these URLs with query strings intact.

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.

worker.ts
// Prepare this request's account handle before opening its context.
export function openRequestDb(account: AccountHandle) {
  return createDb({
    appId: "my-app",
    account,
    runtimeSources: { wasmModule: jazzWasmModule },
  });
}

If your platform hands you raw bytes instead of a compiled module, use runtimeSources.wasmSource:

worker.ts
import { createDb, type AccountHandle } from "jazz-tools";

// The request's account manager prepared this handle before context creation.
export function openRequestDb(account: AccountHandle, wasmBytes: Uint8Array) {
  return createDb({
    appId: "my-app",
    account,
    runtimeSources: { wasmSource: wasmBytes },
  });
}

See the Cloudflare Wrangler example for a complete workerd setup.

On this page