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.

App.tsx
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.

App.vue
<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>
App.svelte
<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}
App.tsx
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.

app.ts
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

FieldRequiredDescription
appIdyesApplication identifier. Isolates storage between apps.
serverUrlnoJazz sync server URL. Omit for fully local/offline mode.
secretnoLocal-first auth secret. Jazz derives a stable identity from it.
jwtTokennoExternal auth bearer JWT. Requires server-side JWKS configuration.
adminSecretnoAdmin secret for privileged sync and /admin/* catalogue operations.
runtimeSourcesnoRuntime source overrides for browser workers, Wasm URLs, or Wasm input.
drivernoStorage driver: { type: "persistent" } (default) or { type: "memory" }.
dbNamenoLocal database name. Defaults to appId.
envnoEnvironment label (for example "dev", "prod").
userBranchnoBranch 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.

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 { 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.

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

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.

metro.config.js
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.

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
ExpoEXPO_PUBLIC_JAZZ_APP_IDEXPO_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:

OptionDescription
servertrue (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).
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. 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.

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 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.

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.workerUrlThe worker script has an explicit public URL
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.workerUrl
  4. runtimeSources.baseUrl
  5. built-in zero-config fallback

Browser asset overrides

The config is the same across frameworks. Plain TypeScript uses createDb(...) directly.

App.tsx
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>
  );
}
App.vue
<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>
App.svelte
<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>
app.ts
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.

worker.ts
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:

worker.ts
const db = await createDb({
  appId: "my-app",
  runtimeSources: {
    wasmSource: wasmBytes,
  },
});

See the Cloudflare Wrangler example for a complete workerd setup.

On this page