Server Setup
Hosted and self-hosted database server configuration, app provisioning, and backend context setup.Hosted database server
Your app ID namespaces your data for storage and sync. Click below to generate one on the Jazz hosted cloud — you'll also get the secrets you need to deploy your schema and permissions later. Generated apps are unclaimed until you claim them in the dashboard, and unclaimed apps are automatically deleted after 14 days.
Or from the command line (AI agents: use this to provision your own app):
curl -X POST https://v2.dashboard.jazz.tools/api/apps/generate
Jazz Cloud sync URL: https://v2.sync.jazz.tools/
Self-hosted database server
Terminalexport JAZZ_APP_ID="replace-with-your-app-id"export JAZZ_ADMIN_SECRET="replace-with-admin-secret"npx jazz-tools@alpha server "$JAZZ_APP_ID" \--port 1625 \--data-dir ./data \--admin-secret "$JAZZ_ADMIN_SECRET"
jazz-tools@alpha server <APP_ID> currently supports:
| Option | Purpose | Environment variable | Default |
|---|---|---|---|
<APP_ID> (positional) | App namespace identifier (required) | - | - |
-p, --port <PORT> | Listen port | - | 1625 |
-d, --data-dir <DATA_DIR> | Persistent storage directory | - | ./data |
--in-memory | Use in-memory storage instead of files; data is lost when the process exits | - | off |
--jwks-url <JWKS_URL> | JWKS endpoint for external JWT validation | JAZZ_JWKS_URL | unset |
--jwt-public-key <JWT_PUBLIC_KEY> | Single JWK JSON object or PEM public key for external JWT validation. Accepts inline contents or a path to a key file. | JAZZ_JWT_PUBLIC_KEY | unset |
--auth-cookie-name <AUTH_COOKIE_NAME> | Cookie name to read for browser authentication during WebSocket upgrades | JAZZ_AUTH_COOKIE_NAME | unset |
--allow-local-first-auth | Allow local-first auth (Authorization: Bearer <self-signed Jazz JWT>) | JAZZ_ALLOW_LOCAL_FIRST_AUTH | see NODE_ENV note below |
--backend-secret <BACKEND_SECRET> | Enable backend session impersonation | JAZZ_BACKEND_SECRET | unset |
--admin-secret <ADMIN_SECRET> | Required for deploy and schema catalogue reads. In development mode, structural schema auto-sync works without it. | JAZZ_ADMIN_SECRET | unset |
--shutdown-timeout-secs <SECONDS> | Graceful shutdown network-drain timeout in seconds | JAZZ_SHUTDOWN_TIMEOUT_SECS | 30 |
Local-first auth is enabled by default in development and requires --allow-local-first-auth in production. External JWT auth requires either --jwks-url or --jwt-public-key, but not both.
The server runs Core directly. It owns the account registry, permission checks,
and durable write confirmation. Browser workers and native local relays remain
client-side persistence components.
Cookie-based WebSocket auth is enabled with --auth-cookie-name or JAZZ_AUTH_COOKIE_NAME. When no
explicit auth credential is supplied, the sync server reads that named cookie and validates the JWT
it contains. If your app uses a separate application session cookie, resolve it in your own app server
and obtain an admitted account handle from its JWT. Pass that handle to await client.forAccount(account);
forRequest accepts bearer headers and does not parse application session cookies.
If you prefer to start the database server programmatically, you can use startLocalJazzServer from jazz-tools/dev.
It expects similar arguments as the CLI.
Backend context setup
A TypeScript backend creates one createJazzSession owner from jazz-tools/backend, with appId, app, permissions, driver, and serverUrl configured once. Select backend authority with initial: { backendSecret } or later with await session.becomeBackend({ backendSecret }). For authentication and account linking, see Authentication.
Backend admission requires a reachable Jazz server: the Node host sends POST /apps/{app}/backend/admit with X-Jazz-Backend-Secret, and proceeds only after that server validates it. Core validates the credential and owns account registration and linking. This also applies to a memory driver; backend initialization is not an offline privilege grant. Browser and React Native hosts reject backend selection.
main.tsconst session = await createJazzSession({appId,app: schemaApp,permissions,driver: { type: "persistent", dataPath: dbPath },serverUrl,initial: { backendSecret },jwksUrl,jwtPublicKey,allowLocalFirstAuth,env: "dev",});const snapshot = session.getSnapshot();if (snapshot.status !== "ready" || !snapshot.client) {throw snapshot.error ?? new Error("Backend session is not ready");}const client = snapshot.client;const db = client.db;
main.rslet context = AppContext {app_id: AppId::from_name(&app_id),client_id: None,schema,server_url,data_dir: PathBuf::from(data_dir),storage: ClientStorage::Persistent,storage_factory: Some(std::sync::Arc::new(jazz_storage_rocksdb::RocksDbStorageFactory,)),jwt_token: None,backend_secret: None,admin_secret: None,};
Backend identity pattern
The ready snapshot exposes client.db for backend-owned work. Backend accounts use the reserved nil account UUID and identity { issuer: "urn:jazz:system", subject: nodeUUID }, where nodeUUID is the actual originating native node. Copying that identity does not grant authority. The secret stays in private handle material and is never serialized into snapshots or account preferences.
client.dbhas backend permissions and records SYSTEM node provenance.await client.forRequest(req)verifies the caller's bearer and active core account assignment, then returns an immutable database scope with that user's policies and authorship.await client.forAccount(account)performs the same verification from an opaque admitted user account handle.await client.withAttribution(account)andawait client.withAttributionForRequest(req)keep backend permissions while recording the verified user's authorship. Use them after the application authorizes the operation.
Request scopes do not change the shared session's selected account, so concurrent requests retain separate policy contexts. Do not switch the shared owner to serve individual requests. When the application deliberately transitions the owner from backend to a user account, the old client shuts down and the replacement opens an ordinary native runtime without backend privilege. The shared lifecycle handles detach, sync, shutdown, retry, and logout; logout invalidates every issued handle. Backend selection is ephemeral across process restarts, while retained local-first signing roots remain available.
forRequest reads standard HTTP headers from Express, Hono, Fastify, or a Web Fetch API Request. Configure jwksUrl or jwtPublicKey on createJazzSession(...) for external IdP tokens, but not both. Without either, it accepts only Jazz local-first tokens; allowLocalFirstAuth: false disables those too. Application cookies must first be resolved to an admitted account by your auth integration.
The TypeScript backend's jwksUrl must use HTTPS, except for development HTTP
with a WHATWG-canonical hostname of localhost, [::1], or an IPv4 address in
127.0.0.0/8. For example, http://localhost:3000/api/auth/jwks is allowed;
http://localhost.:3000/api/auth/jwks (the trailing-dot spelling) and
http://keys.example/api/auth/jwks are rejected before fetching keys.
Every redirect is rejected, including redirects to HTTPS. Use the provider's
final JWKS URL directly. This policy applies to TypeScript request authentication,
not the separate Rust server's --jwks-url verifier.
Attribution without impersonation
Use await client.withAttribution(account) or await client.withAttributionForRequest(req) to retain backend access while recording verified user provenance. Raw session objects and issuer/subject strings are not public authority inputs. See Sessions > Attribution without impersonation.
Per-request user-scoped client
Pass req to run queries as the authenticated user, with all permission policies applied.
request-context.tsexport async function listTodosForRequester(req: Request, res: Response): Promise<void> {try {const requester = await client.forRequest(req);const rows = await requester.all(schemaApp.todos.where({ done: true }));res.json(rows);} catch {sendQueryError(res);}}
request-context.ts// Written against `Jazz` only, so the same code runs in a browser, in a test// against an in-memory database, or here as the requesting user.export const listDoneTodos = Effect.gen(function* () {const jazz = yield* Jazz;return yield* jazz.all(schemaApp.todos.where({ done: true }));});// A standalone route: mount it with the quickstart routes below if you want it served.export const listDoneTodosRoute = HttpRouter.add("GET","/api/todos/done",listDoneTodos.pipe(Effect.flatMap((rows) => HttpServerResponse.json(rows)),JazzBackend.forCurrentRequest(),Effect.catchTag("JazzError", () =>HttpServerResponse.json({ error: "Failed to query todos" }, { status: 500 }),),),);
request-context.rspub async fn list_todos_for_request(headers: &HeaderMap,client: &JazzClient,) -> Result<usize, StatusCode> {let user_client = client.for_session(requester_session_from_headers(headers)?);let query = Query::from("todos");let rows = user_client.query(query, None).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;Ok(rows.len())}