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

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

Terminal
export 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:

OptionPurposeEnvironment variableDefault
<APP_ID> (positional)App namespace identifier (required)--
-p, --port <PORT>Listen port-1625
-d, --data-dir <DATA_DIR>Persistent storage directory-./data
--in-memoryUse in-memory storage instead of files; data is lost when the process exits-off
--jwks-url <JWKS_URL>JWKS endpoint for external JWT validationJAZZ_JWKS_URLunset
--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_KEYunset
--allow-local-first-authAllow local-first auth (Authorization: Bearer <self-signed Jazz JWT>)JAZZ_ALLOW_LOCAL_FIRST_AUTHsee NODE_ENV note below
--backend-secret <BACKEND_SECRET>Enable backend session impersonationJAZZ_BACKEND_SECRETunset
--admin-secret <ADMIN_SECRET>Required for deploy, migrations push, and schema catalogue reads. In development mode, structural schema auto-sync works without it.JAZZ_ADMIN_SECRETunset

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.

Backend context setup

Every backend needs an app ID, typed schema, and usually a permissions bundle. If the backend syncs through a Jazz server, also pass serverUrl and the relevant secrets. For auth modes and upgrade/link flow, see Authentication. For frontend/browser context creation, see Client Setup.

const context = createJazzContext({
  appId,
  app: schemaApp,
  permissions,
  driver: { type: "persistent", dataPath: dbPath },
  serverUrl,
  backendSecret,
  adminSecret,
  jwksUrl,
  jwtPublicKey,
  allowLocalFirstAuth,
  env: "dev",
  userBranch: "main",
});
const db = context.asBackend();
let context = AppContext {
    app_id: AppId::from_name(&app_id),
    client_id: None,
    schema,
    server_url,
    data_dir: PathBuf::from(data_dir),
    storage: ClientStorage::Persistent,
    jwt_token: None,
    backend_secret: None,
    admin_secret: None,
    sync_tracer: None,
};

Backend identity pattern

On the client side, queries automatically run as the logged-in user. On a server-connected backend, create the context once at startup, then choose whether work should run as the backend itself or as the caller.

  • context.asBackend() returns a Db that uses backendSecret for sync/auth when this process talks to a Jazz server. Writes from this handle are authored as jazz:system unless you add explicit attribution.
  • await context.forRequest(req) returns a Db that runs queries as the authenticated user who made the request, so permission policies apply to them rather than the process itself. Writes are also attributed to that user.
  • context.forSession(session) is the same idea when you already have a resolved Jazz session.
  • context.withAttribution(principalId) keeps backend permissions, but stamps new writes and updates as principalId.
  • context.withAttributionForSession(session) and await context.withAttributionForRequest(req) derive that authorship from a resolved session or authenticated request without impersonating the user for permission checks.
  • context.db() gives you a high-level Db using the context's configured runtime/auth. On server-connected backends, prefer context.asBackend() or await context.forRequest(req); on embedded or local-only setups, context.db() is the right choice.

In TypeScript backends, create the context once with both app and permissions, then use context.asBackend() for server-owned work and await context.forRequest(req) when you need a bearer-JWT-scoped high-level Db.

forRequest reads authentication from standard HTTP headers, so req can be any object that exposes them: Express, Hono, Fastify, or a Web Fetch API Request all work. Configure jwksUrl or jwtPublicKey on createJazzContext(...) if those bearer tokens come from an external IdP, but do not set both at once. Without either one, backend forRequest() only accepts Jazz self-signed tokens; set allowLocalFirstAuth: false to disable those too.

For embedded or local-only runtime setups where your backend is not connected to a server, use context.db() to get an unscoped, unauthenticated handle. This allows read/write access to the database directly, without permissions being evaluated.

Attribution without impersonation

Use withAttribution(...), withAttributionForSession(...), or await context.withAttributionForRequest(...) when backend code should keep backend-level access but stamp row edit metadata with a user's identity. See Sessions > Attribution without impersonation for details and examples.

Per-request user-scoped client

Pass req to run queries as the authenticated user, with all permission policies applied.

export async function listTodosForRequester(req: Request, res: Response): Promise<void> {
  try {
    const requester = await context.forRequest(req, schemaApp);
    const rows = await requester.all(schemaApp.todos.where({ done: true }));
    res.json(rows);
  } catch {
    sendQueryError(res);
  }
}
pub 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 = QueryBuilder::new("todos").build();
    let rows = user_client
        .query(query, None)
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    Ok(rows.len())
}

On this page