Schema sync

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.

The document

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: true
realtime: true
schema: {
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: true
filtering: 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.
  • A list can set 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.
  • Fields you leave out are left as they are, rather than reset to their defaults. To clear something, set it explicitly, e.g. "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.
  • An index needs a pointer when the sync creates it.

Syncing

Send the document to POST /sync-schema, or use the command line tool.

Shell
12npx @basementuniverse/jsonpad-sdk sync-schema --dry-run
npx @basementuniverse/jsonpad-sdk sync-schema

The 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: true
applied: false
scope: "recipe-app"
summary: {
create: 1
update: 1
adopt: 0
noChange: 2
error: 0
builds: 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: null
to: "/minutes"
}
valueType: {
from: null
to: "number"
}
}
build: {
reason: "created"
items: 1240
requiresConfirmation: 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.

Dry runs

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.

All or nothing

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.

Index rebuilds

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.

Scopes

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:

  • The lists the document declares are tagged with the scope, so you can find everything it manages by searching for #recipe-app.
  • JSONPad records that the scope manages those lists and indexes. A list managed by one scope can't be changed by a document with a different scope, or with no scope, so two apps' documents can't keep overwriting each other's settings.
  • An existing list that isn't managed by a scope yet is adopted the first time a scoped document declares it. Its action is 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.

Exporting existing lists

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:

Shell
1234npx @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-schema

Lists that a document couldn't sync, e.g. lists without a path name, are left out of the export with a warning.

The command line tool

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).

Shell
12345678# 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.

Continuous integration

For example, with GitHub Actions, and your token stored as a repository secret:

GitHub Actions
12345678910111213141516171819202122232425name: 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 --wait

Permissions

A 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.

Limitations

  • A sync never deletes lists or indexes that are no longer in the document.
  • Items, tokens and identities can't be synced. Tokens and identities have secrets that don't belong in a file in a repository.
  • A list without a path name can't be synced, because lists are matched by path name.
  • There's no way to move a list from one scope to another yet, other than deleting and recreating it.
2026-09-14