Schema sync lets you describe your lists and their indexes in a file, keep that file in your repository, and apply it to your account. JSONPad works out what's different, then creates and updates lists and indexes until the account matches. That makes it easy to set up the same lists in a new account or environment, review structural changes in a pull request, and apply them as part of a deploy.
A sync only ever creates and updates. It never deletes a list or an index, even if you remove it from the file.
A schema document is a JSON file, usually called jsonpad-schema.json. Lists are keyed by their path name, and each list's indexes are keyed by their path name within the list, so the file doesn't contain any ids and works the same in any account.
{$schema: "https://jsonpad.io/schema/sync-v1.json"scope: "recipe-app"lists: {recipes: {name: "Recipes"description: "Every recipe in the app"indexable: truerealtime: trueschema: {type: "object"properties: {slug: {type: "string"}title: {type: "string"}minutes: {type: "number"}}required: [0: "slug"1: "title"]}indexes: {slug: {name: "Slug"pointer: "/slug"alias: true}minutes: {name: "Cooking time"pointer: "/minutes"valueType: "number"sorting: truefiltering: true}}}ratings: {name: "Ratings"indexable: true}}}$schema is optional. It points editors at the document's JSON schema, which gives you autocomplete and validation as you type.scope is optional. See scopes.name, description, tags, schema, readonly, realtime, protected, indexable and indexes, and an index can set name, description, tags, pointer, valueType, alias, sorting, filtering, searching, guard and defaultOrderDirection. They mean the same as they do when you create a list or create an index."schema": null or "tags": []. Likewise, indexes that aren't in the file are left alone.pinned and activated can't be synced, and any other unknown field is refused, so a typo like "realtme" is caught rather than ignored. Lists and indexes that a sync creates are always activated.pointer when the sync creates it.Send the document to POST /sync-schema, or use the command line tool.
npx @basementuniverse/jsonpad-sdk sync-schema --dry-run
npx @basementuniverse/jsonpad-sdk sync-schemaThe response describes every list and index in the document, and what the sync did (or, in a dry run, would do) to it:
{syncId: "6f0e3e1c-9a53-4b53-9d0b-3e0c7bbd8a41"dryRun: trueapplied: falsescope: "recipe-app"summary: {create: 1update: 1adopt: 0noChange: 2error: 0builds: 1}changes: [0: {resourceType: "list"list: "recipes"listId: "1bbd0b1e-4b3a-4c52-8f2a-0b8b0a2b5d0e"action: "update"fields: {description: {from: "Recipes"to: "Every recipe in the app"}}}1: {resourceType: "index"list: "recipes"index: "minutes"action: "create"fields: {pointer: {from: nullto: "/minutes"}valueType: {from: nullto: "number"}}build: {reason: "created"items: 1240requiresConfirmation: false}}]blockedBy: null}Each change's action is create, update, adopt (see scopes), no-change or error. Updates list each field that changes, with its current and new values. Changes can also have warnings, e.g. that existing items haven't been re-validated against a list's new schema.
Add ?dryRun=true (or --dry-run) to see what a sync would do without changing anything. Running a dry run in a pull request, and the real sync when it's merged, means structural changes get reviewed like any other change.
Every change is checked against the same rules as the equivalent API request: plan limits, reserved path names, schema validity, alias and guard index rules, readonly and locked lists, and the token's permissions. If any change would be refused, the whole sync is refused with a 422 response and nothing is written. The response still contains the plan, with an errors array on each change that was refused, so you can see everything that needs fixing at once.
Creating an index, or changing an index's pointer, builds it in the background (see index builds). While an index is being rebuilt, filtering, ordering and alias lookups that use it are refused, so changing the pointer of an index in a list that already has items could break an app that relies on it.
A sync that would do that is refused unless you add ?allowRebuild=true (or --allow-rebuild). Creating a new index doesn't need it, because nothing can be using the index yet. The command line tool's --wait option waits for builds to finish, and fails if one of them does.
A sync never retries an index whose last build failed, because a build that failed because of the data in the list would only fail again on every deploy. The plan warns about it instead. Fix the problem, then rebuild the index.
If you use JSONPad for more than one app, give each app's document a scope, e.g. the app's name. A scope does three things:
#recipe-app.adopt, so a dry run shows when that's about to happen.Deleting a list or an index also removes the record of which scope managed it.
You don't have to write a document by hand for lists you already have. GET /sync-schema exports existing lists and their indexes as a document, with every field written out, so syncing it straight back changes nothing. You can export a scope's lists, lists with certain tags, or lists by path name.
For example, to start managing the lists you've tagged recipe-app with schema sync:
npx @basementuniverse/jsonpad-sdk export-schema --tagged recipe-app --out jsonpad-schema.json
# Add "scope": "recipe-app" to jsonpad-schema.json, then:
npx @basementuniverse/jsonpad-sdk sync-schema --dry-run
npx @basementuniverse/jsonpad-sdk sync-schemaLists that a document couldn't sync, e.g. lists without a path name, are left out of the export with a warning.
The JavaScript SDK includes a jsonpad command, so you can run it with npx without installing anything, or install the SDK in your project and run npx jsonpad. It needs Node.js 18.3 or later, and reads your API token from the JSONPAD_TOKEN environment variable (rather than an option, which would end up in your shell history and CI logs).
# Sync jsonpad-schema.json (or pass a different file)
jsonpad sync-schema [file] [--dry-run] [--allow-rebuild] [--wait] [--show-unchanged] [--json]
# Write a document for existing lists
jsonpad export-schema [--scope recipe-app] [--tagged recipe-app] [--lists recipes,ratings] [--out file]
# Rebuild an index whose last build failed
jsonpad rebuild-index <list> <index> [--wait]sync-schema exits with 0 when the sync was applied (or a dry run found nothing wrong), 1 when a change has errors, 2 when the sync needs --allow-rebuild, and 3 when an index build fails while waiting. A dry run exits the same way the real sync would, so it can fail a pull request check.
For example, with GitHub Actions, and your token stored as a repository secret:
name: Sync JSONPad schema
on:
pull_request:
push:
branches: [main]
jobs:
sync:
runs-on: ubuntu-latest
env:
JSONPAD_TOKEN: ${{ secrets.JSONPAD_TOKEN }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Check the schema
if: github.event_name == 'pull_request'
run: npx @basementuniverse/jsonpad-sdk sync-schema --dry-run
- name: Sync the schema
if: github.event_name == 'push'
run: npx @basementuniverse/jsonpad-sdk sync-schema --waitA token needs the sync-schema permission to sync or export a schema. It can read the structure of every list (not its items) with that permission alone. Each change a sync makes also needs the permission the equivalent request would need, e.g. permission to create lists, or to update a particular index, so a token can't use a sync to change a list it isn't otherwise allowed to change.
Every change a sync makes is recorded in the event log as a normal list-created, index-updated (etc.) event, with the sync's syncId and scope in its attachments. The index-built and index-build-failed events for builds that a sync started carry the syncId too.