Backups & Upgrades
Learn how to keep the data in your backend's Jazz directory safe and upgrade it across releases.What the data directory holds
Everything your main backend stores lives in the directory you pass to
sqlite({ path }):
| File or folder | What it is |
|---|---|
jazz.sqlite (with -wal and -shm) | Rows, history, schemas, permissions and the index of large-value chunks |
blobs/ | The chunks of large values, unless you chose another place |
app-id | The app this directory belongs to |
store-id, blob-store | This directory's identity, and where its chunks live |
jazz.lock | Held by the running process, so a second one on the same directory is refused |
If your chunks are in another directory (files({ dir })) or a bucket (s3()), they're part of the
data too.
Backing up
Back up the whole directory together, and its chunk directory or bucket prefix with it:
- Stop the backend first, or snapshot the disk it's on atomically (a volume snapshot). Copying
jazz.sqlitewhile it's being written can give you a copy that doesn't open. - Copy the
-walfile with the database. It holds recent writes that aren't injazz.sqliteyet. - Back up chunks at least as recently as the database. A database that refers to chunks the backup doesn't have can't serve those large values.
Restoring
To restore in place, stop the backend, put the files back, and start it again.
To restore into a new place while the original keeps running, such as a staging copy, give the copy
its own chunk directory or s3() prefix and delete its store-id and blob-store files before it
first starts. Otherwise it claims the original's chunks.
Upgrading between releases
Upgrade by deploying your backend with the new jazz-tools. On start, it opens the data directory
in the new release's format and applies your schema, migrations and permissions as usual. Upgrade
browsers and backends together: they ship in one bundle, and a release notes when an older client
can't talk to a newer backend.
Back up before you upgrade. Releases don't migrate a data directory back to an older format, so rolling back means restoring the backup.
Upgrading from alpha.59
Alpha.60 changes how Jazz runs, so an alpha.59 app needs more than a version bump:
- Jazz runs inside your backend. Clients no longer talk to a Jazz server with a JWT. Add a main
backend with
createJazz(see Your Own Server), and have it point at Jazz Cloud withremote()if that's where your data is. The core admits only its own app's backend, so browsers can't connect to it directly any more. - Auth moves into
authenticate. Your backend decides who each request is. To keep returning users on their old accounts, return the old token'ssubasuserId(see Custom auth). - Clients and backends upgrade together. Alpha.59 clients can't sync with an alpha.60 backend.
Ship the new client in the same release as the new backend. A client left on alpha.59 looks
healthy: its session reports ready and local writes succeed, but nothing syncs and nothing is
logged. Against a
createJazzbackend, queries and waits that need the server reject with a bareError: [object Event], and a reloaded alpha.59 tab never signs in. Against Jazz Cloud, the first query that waits for the server rejects with "this Jazz client speaks wire protocol 3..=3, older than this server's 6". - The storage engine is now SQLite. Alpha.59's data directory doesn't open in alpha.60.
Code changes
Besides moving to createJazz and authenticate, change these before the new backend first
starts:
-
Pass your migrations to
defineApp, also when your data was imported:schema.tsimport { migrations } from "./migrations";export const app = s.defineApp(schema, { migrations });Don't use
{ migrations: "none" }here: it's for an app that only ever has one version, such as a test's. Then runjazz-tools validate. It createsmigrations/index.tsif it's missing and moves alpha.59's migrations tomigrations/legacy/, and every snapshot except the one matchingschema.tstomigrations/legacy/snapshots/. The upgraded app's history starts again from its current schema. If no snapshot matchesschema.ts, validate keeps the newest one and tells you to runjazz-tools migrations createbefore the app starts. -
Snapshot the current schema before you change it. Run
jazz-tools migrations createwith no flags whileschema.tsstill matches what you imported. Later schema changes follow Migrations as usual. -
Replace
isCreatorwith{ $createdBy: session.user }. It compares accounts, so it keeps matching the rows users wrote on alpha.59 whose author was imported with an account. -
Replace
session.user.accountwithsession.user, which is now the account, andsession.user.identitywithsession.identity, in permissions and in client code. Permissions that still use the old form fail with an error saying so; in client code it is silentlyundefined. -
Rename removed read tiers. In reads,
"global"is now"remote"(ReadTier.Remote), and"local"is"local-first";"edge"is gone. Write waits keep"local"and"global":wait({ tier: "remote" })throws. See Removed read tiers. -
Create accounts for users who never open a client.
forReqUserandaccountForno longer create an account. If your API serves users before they sign in from a client, calljazz.createAccountFor(userId)in your sign-in route; see Users without an account.
Data on Jazz Cloud
Jazz Cloud moves your app's data to alpha.60 for you. The move keeps every row with its history,
authors and timestamps, your current schema and permissions, and every account. It also links each
signed-in user's alpha.59 identity to jazz:backend so authenticate can open their old
account.
Self-hosted alpha.59 servers
Move a self-hosted data directory with the same import tool Jazz Cloud uses. You need only the two downloads below, no Node or Rust toolchain, and nothing writes to your alpha.59 directory.
Before you start:
- Plan the downtime. It runs from stopping the alpha.59 server until the new backend and the new client bundle are both live, since old clients can't sync with the new backend. The tools themselves take seconds for a small app.
- Let clients finish syncing, then close them. Writes still queued in an alpha.59 browser, such as ones made offline, are not carried over: alpha.60 opens a new local database and the old one is left unused. Rolling back to alpha.59 is the only way to recover them, so drain open tabs before step 1.
- Rehearse on a copy if you like.
cp -a ./data ./data-rehearsalwith the alpha.59 server stopped, then run steps 3 and 4 against the copy; nothing in them writes to it. - Make the code changes first, so the new backend can start as soon as the import is verified.
- Run
verifybefore the new backend's first start. That first start moves large values out ofjazz.sqliteintoblobs/, andverifycan't read the directory after that.
Then:
-
Stop the alpha.59 server. It drains open connections first, for up to 30 seconds by default (
--shutdown-timeout-secs). -
Download the tools. Get
jazz-alpha59-alpha60-export-<platform>andjazz-alpha59-alpha60-import-<platform>(linux-x64,linux-arm64ordarwin-arm64) and the two checksum files from the v2.0.0-alpha.60 release. Each checksum file lists every platform, so check with--ignore-missing, then rename the tools to drop the platform suffix and make them executable:Terminalsha256sum -c --ignore-missing jazz-alpha59-alpha60-export.sha256 jazz-alpha59-alpha60-import.sha256mv jazz-alpha59-alpha60-export-linux-x64 jazz-alpha59-alpha60-exportmv jazz-alpha59-alpha60-import-linux-x64 jazz-alpha59-alpha60-importchmod +x jazz-alpha59-alpha60-export jazz-alpha59-alpha60-importOn macOS, use
shasum -a 256 -c --ignore-missingand thedarwin-arm64names. -
Export the data with alpha.59's own code.
--workis an empty scratch directory: the exporter reads a private copy of your data there.Terminal./jazz-alpha59-alpha60-export --data-dir ./data --work ./export-work --out app.dump -
Import and verify into a new, empty directory. Verify checks every row, sign-in and identity link against what alpha.59 read.
Terminal./jazz-alpha59-alpha60-import import --dump app.dump --data-dir ./data-new --app-id <appId>./jazz-alpha59-alpha60-import verify --dump app.dump --data-dir ./data-new --app-id <appId> -
Start your new backend with
sqlite({ path: "./data-new" }), then deploy the new client. If your alpha.59 app had local-first guests, pass your old app ID asscopeto the client, so each guest's device finds its account again:const jazz = await createJazzClient({ scope: "<your alpha.59 app ID>" });(
<JazzProvider scope="…">in React.) Withscopeset, each guest's browser finds its alpha.59 account by itself, on the same origin; there's nothing else to do. Without it, those guests can't open their accounts. The import prints the exactscopeline when it brings local-first guests over. If the alpha.59 app was reached through more than one Jazz server URL on that origin, guests aren't picked up automatically. Keep./dataand the dump until you're happy with the result.
--app-id is your app's ID exactly as before, case included: the import uses it to link each
returning user's old sign-in to their account, as jazz:backend with their old sub (as fromJWT() returns it), and to keep
local-first guests on their accounts once the client is scoped to the alpha.59 app ID (step 5). It's written to <data-dir>/app-id, so your code never needs
it: sqlite({ path }) runs as the old app. Pass
--no-backend-identity-links to skip that. The dump holds all your app's data, so store it like the
data directory and delete it when you're done.
The import brings over your current schema and its permissions. A few things alpha.59 could store
are not supported yet, and the import stops with an error naming them rather than leaving them
out: branched rows, writes the server hadn't settled yet, and rows with no migration path to the
current schema. Versions that carry transaction metadata are refused too; pass
--drop-transaction-metadata to import them without it. A failed import cleans up after itself.