Jazz Cloud

Learn how to keep your app's data on Jazz Cloud, with your own backend in front for auth.

Jazz Cloud stores and syncs your app's data. Your own backend sits in front of it: browsers talk only to your backend, which signs users in with your auth and connects to Jazz Cloud. Jazz Cloud does no user auth of its own.

This page sets up your main backend: the backend that runs createJazz, serves sync to your browsers and apps, and decides who each user is. Other trusted machines reach Jazz through it as backend clients.

Install

Terminal
npm install jazz-tools

This installs Jazz 2, which is in alpha and leaves alpha soon. Versions before 2.0 are Classic Jazz, which has a different API: projects on jazz-tools 0.x and guides that use co.map, CoValue or Group are Classic.

Get an app ID

Your app ID namespaces your data for storage and sync. Click below to generate one on Jazz Cloud. You also get the secrets your backend needs. Generated apps are unclaimed until you claim them in the dashboard, and unclaimed apps are 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/

Connect your backend

Put the generated values in your backend's environment. All of them stay server-side; the browser never needs the app ID or the sync URL, and the admin secret is like a database password:

.env
JAZZ_APP_ID="<your app ID>"
JAZZ_SERVER_URL="https://v2.sync.jazz.tools/"
JAZZ_ADMIN_SECRET="<admin secret>"

Then point createJazz at Jazz Cloud with the remote() store:

jazz.server.ts
import { createJazz, remote } from "jazz-tools/backend";
import { app } from "./schema";
import permissions from "./permissions";
​
export const jazz = createJazz({
app,
permissions,
store: remote({
appId: process.env.JAZZ_APP_ID!,
url: process.env.JAZZ_SERVER_URL!,
secret: process.env.JAZZ_ADMIN_SECRET!, // the app's admin secret
}),
// Who is signed in, read from your own auth's session.
authenticate: async (request) => {
const session = await getSession(request);
return session ? { userId: session.user.id } : null;
},
});
  • app and permissions are the ones your code imports. The bundle that's running is the bundle Jazz enforces, so rolling back your backend rolls back its permissions too.
  • authenticate(request) reads your own session (a cookie, a header) and returns { userId, claims? }, or null when signed out. It is the only place that decides who a user is: sign-ins, sync connections and forReqUser all ask it. There's no Jazz token in the browser. If your provider issues JWTs, fromJWT() is a ready-made authenticate. See How Auth Works in Jazz.
  • Admin and backend routes never leave the process: from outside, only sync and sign-in are reachable under /jazz.
  • createJazz opens nothing when your module loads: the store opens on first use, such as the first request or jazz.db call, and there's one Jazz per store per process. So it's safe at a module's top level everywhere, including Next.js, whose build and dev workers import your modules too. await jazz.ready() opens it right away, for a server that should fail at startup rather than on its first request.

On start, your backend applies its schema, migrations and permissions to the app on Jazz Cloud. For each request it tells Jazz Cloud who the user is, which is why authenticate lives in your backend. Browsers sync with your backend under /jazz, never with Jazz Cloud directly.

Mount it

app/jazz/[...path]/route.ts
import { jazz } from "@/jazz.server";
​
export const { GET, POST, DELETE } = jazz.next();

Install ws and @vercel/functions, and add serverExternalPackages: ["jazz-napi"] to next.config.ts so Next doesn't bundle Jazz's native module; next dev needs all three. On Vercel, sync uses a WebSocket (through upgradeWebSocket from @vercel/functions). Under plain next dev and next start, which can't upgrade sockets, the browser switches to an HTTP stream on its own. With sqlite() (not vercel()), also install @vercel/sandbox for now: Next resolves it while building even though sqlite() never loads it.

A custom Next server can't use jazz.attach(server) yet: Next's own upgrade handler closes the /jazz sockets. Leave sockets to the HTTP stream there.

In a server component or server action there is no Request. forReqUser only hands the request to your authenticate, so build one from the incoming headers:

import { headers } from "next/headers";
import { jazz } from "@/jazz.server";
​
const db = await jazz.forReqUser(new Request("http://localhost", { headers: await headers() }));

Self-hosted on sqlite(), import jazz.server.ts only from the Next app itself. A custom server file or a script that opens the same data directory in another process is refused (the directory is locked to one process).

Connect the browser

The browser talks to /jazz on the same origin by default, so there's no serverUrl to set, and your app's own session cookie is all it needs. It needs no Jazz API key or token:

App.tsx
<JazzProvider signedOut={<SignIn />}>
<App />
</JazzProvider>

The client asks your backend who is signed in: authenticate answers, and the client gets the account, identity and claims as plain data. It asks again when the app starts, when the page regains focus and when another tab signs in or out. After your own sign-in or sign-out, call reauthenticate() from useJazzAuth(). Every sync connection asks authenticate again, so a sign-out on the server or changed claims apply without client code. See How Auth Works in Jazz.

Trusted backend use

jazz.db acts with backend authority: it sees and writes everything, so check access in your own code first. Use it for system work: jobs, webhooks, migrations, admin routes.

import { jazz } from "./jazz.server";
import { app } from "./schema";
​
await jazz.db.insert(app.auditLog, { event: "invoice.paid" });

User-scoped use

await jazz.forReqUser(request) runs your authenticate and returns a database with that user's policies and authorship. It's the default for anything a user triggers: route handlers, server actions, server components. Concurrent requests stay isolated.

app/api/todos/route.ts
import { jazz } from "@/jazz.server";
import { app } from "@/schema";
​
export async function GET(request: Request) {
const db = await jazz.forReqUser(request);
return Response.json(await db.all(app.todos));
}

For a signed-out request, forReqUser throws a RequestAuthenticationError (code: "signed_out"). Map it to a 401 rather than a 500:

import { isRequestAuthenticationError } from "jazz-tools/backend";
​
try {
const db = await jazz.forReqUser(request);
// …
} catch (error) {
if (isRequestAuthenticationError(error)) return new Response(null, { status: 401 });
throw error;
}

When a route acts with backend authority on a user's behalf (placing an order the user can't write directly, say), await jazz.accountFor(request) names that user's account, or null when signed out or when the user has no Jazz account yet, so the backend's db can write rows that point at it.

Users without an account

Your backend never creates an account on its own. A user gets one when they sign in from a Jazz client, when a guest links to them, or when you call jazz.createAccountFor(userId). Until then, forReqUser and withAttribution throw a NoJazzAccountError (code: "no_account", with the userId), and accountFor returns null.

For users who never open a client, such as API-only users, webhooks or imports, create the account yourself, for example in your sign-in route. It returns the account id, and calling it again returns the same one:

const user = await authenticate(request);
if (!user) return new Response(null, { status: 401 });
const account = await jazz.createAccountFor(user.userId);

Once a user has an account, a guest can no longer link to them, so don't call createAccountFor in a sign-up flow whose browser may hold a local-first guest. To tell the error apart, use isNoJazzAccountError(error) from jazz-tools/backend.

A browser whose user no longer has an account on the core, for example after a reset dev store or on a new core, signs in again when sync refuses it, instead of reconnecting forever.

Backend clients

Other trusted machines, such as a job runner, a worker on another cloud or a partner's service, connect to this backend's /jazz as backend clients, with their own machine-to-machine credential. List the ones you trust in createJazz({ backendClients }).

Coming next

  • Limits and pricing
  • Moving an app off Jazz Cloud onto your own deployment