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/generateJazz Cloud sync URL: https://v2.sync.jazz.tools/
Self-hosted database server
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:
| 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 |
--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, migrations push, and schema catalogue reads. In development mode, structural schema auto-sync works without it. | JAZZ_ADMIN_SECRET | unset |
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 aDbthat usesbackendSecretfor sync/auth when this process talks to a Jazz server. Writes from this handle are authored asjazz:systemunless you add explicit attribution.await context.forRequest(req)returns aDbthat 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 asprincipalId.context.withAttributionForSession(session)andawait 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-levelDbusing the context's configured runtime/auth. On server-connected backends, prefercontext.asBackend()orawait 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())
}