Skip to content

SurrealKit

Schema modules and targets

A SurrealKit project applies one set of schema files to one database. That covers most projects and needs no configuration. When a project outgrows it, surrealkit.toml can declare schema modules - independently tracked sets of .surql files - and targets - named databases to apply them to.

The two combine into a matrix. Selecting two modules and three targets gives six pairs, applied one at a time.

With no [schema.*] sections, a project has a single unnamed module at database/schema, applied to the connection given by --host, --ns and --db. This is the pre-1.0 layout and nothing about it changed, so an existing project keeps working untouched. Everything below is additive.

Each [schema.<name>] section names a module. Files live under database/modules/<name>/:

surrealkit.toml
[schema.core]

[schema.billing]
depends_on = ["core"]
database/
├── schema/                    # the default module, unchanged
└── modules/
    ├── core/
    │   └── schema/
    └── billing/
        └── schema/

Each module gets its own directories for the artefacts that belong to a schema. Tests and setup.surql belong to the project rather than to any one module, so they are not module-scoped:

DirectoryDefault moduleModule billing
Schemadatabase/schemadatabase/modules/billing/schema
Rolloutsdatabase/rolloutsdatabase/modules/billing/rollouts
Snapshotsdatabase/snapshotsdatabase/modules/billing/snapshots
Seeddatabase/seeddatabase/modules/billing/seed

Named modules sit under modules/ rather than inside database/schema because the default module walks its schema directory recursively. Nesting a named module inside it would make the default module collect those files and claim ownership of them. Set [schema.<name>] path to put a module somewhere else entirely.

depends_on orders application, so a module is never applied before something it depends on. Selecting a module also selects its dependencies, the way cargo build -p does. Pass --no-deps to select only what you named. Independent modules are ordered alphabetically so runs are reproducible, and dependency cycles are rejected before anything touches a database.

Each module owns its own metadata, which is what makes modules safe to mix in one database: a module only ever prunes its own objects. Without that, syncing one schema against a database another schema had populated would remove the other's tables.

SurrealKit partitions the __entity table by module. The ns field is a partition key here, unrelated to a SurrealDB namespace:

Tracked dataDefault moduleModule billing
File hashessyncsync@billing
Managed entitiesschemaschema@billing
Lockslocklock@billing

Pre-1.0 records already sit in the unqualified partitions, which is why adopting modules needs no migration: existing schema stays the default module with its metadata intact.

Warning

Do not rename a module, and do not change which module is the default, once it has been applied. A module's name determines where its metadata lives, so renaming presents the whole module as stale and the next sync drops its database objects. Create the new module and migrate to it deliberately instead.

Each [target.<name>] section names a database. A target inherits everything it does not set from the ambient connection, so it usually names only ns and db:

surrealkit.toml
[target.acme]
ns = "acme"
db = "prod"
pass_env = "ACME_DB_PASSWORD"
primary = true

[target.globex]
ns = "globex"
db = "prod"
pass_env = "GLOBEX_DB_PASSWORD"

Passwords are read from the environment through pass_env, never from the file. A literal pass or password key is rejected, and a pass_env naming an unset variable fails before any connection opens, so a misconfigured target cannot leave a fan-out half applied. A value the target sets wins over the matching CLI flag and environment variable. See Configuration for the full key reference, including per-target connect and query time limits.

A target can also point at a different instance entirely, which is the usual shape for promoting the same schema from staging to production:

surrealkit.toml
[target.staging]
host = "wss://staging-4pq7.aws-euw1.surreal.cloud"
ns = "main"
db = "main"
pass_env = "STAGING_DB_PASSWORD"

[target.production]
host = "wss://production-6xk2.aws-euw1.surreal.cloud"
ns = "main"
db = "main"
pass_env = "PRODUCTION_DB_PASSWORD"

Get the endpoint for a Cloud instance with surreal cloud instance endpoint.

schemas limits which modules apply to a target. An analytics database that needs the shared tables but not the billing ones declares only what it wants:

surrealkit.toml
[target.warehouse]
ns = "internal"
db = "analytics"
schemas = ["core", "analytics"]
pass_env = "WAREHOUSE_DB_PASSWORD"

Selection flags

NameDescription
--schema, -s<NAME>
Apply this module. Repeat for several. Accepts default for the unnamed module.
--no-deps
Apply only the modules named with --schema, without pulling in their depends_on entries.
--target, -t<NAME>
Apply to this target. Repeat for several.
--all
Apply every declared module to every declared target.
--keep-going
Continue to the next target after one fails, instead of stopping.
surrealkit sync                              # every declared module, primary target
surrealkit sync --schema billing             # billing, and core because billing depends on it
surrealkit sync --schema billing --no-deps   # billing alone
surrealkit sync --target acme                # every module, one target
surrealkit sync --all                        # the full matrix
surrealkit sync --all --keep-going           # do not stop at the first failure

Omitting --schema selects every declared module, or the default module when none are declared. Targets resolve in this order, first match winning:

  1. Every --target given.

  2. Every declared target, when --all is passed.

  3. The target marked primary = true.

  4. The only declared target, when exactly one exists.

  5. The ambient connection from --host, --ns and --db.

Targets are applied one at a time. Within a target, modules stop at the first failure, because they are dependency-ordered and the rest would build on a broken base. Targets also stop at the first failure unless you pass --keep-going.

There is no transaction across databases, so a failed run can leave some targets applied and others not. Every operation is idempotent, so re-running after a fix is safe.

When a run covers more than one pair, SurrealKit prints a summary and exits non-zero if any pair failed:

Output
  schema   target   status
  ------   ------   ------
  core     acme     ok
  billing  acme     FAILED

1 ok, 1 failed

Before opening any connection, SurrealKit resolves every selected module and refuses if one has no .surql files. A wrong working directory or --folder value would otherwise look like a schema that had been deleted, and sync would prune the database to match:

Error output
refusing filesystem sync: schema_module=billing resolved_schema_dir=./database/modules/billing/schema source_count=0; check --folder / SURREALDB_FOLDER and module selection, or pass --allow-empty-prune if the empty source set is intentional

Pass --allow-empty-prune when the empty source set is deliberate and you do want everything removed. The check also covers --dry-run, --no-prune and the first sync of a --watch session, because an empty selection cannot otherwise be told apart from a wrong path. A selection where no module applies to any target stays an error rather than a successful no-op.

--schema, --target, --all and --keep-going are accepted on every subcommand, but only some commands act on them:

Command--schema--target and --all
syncSelects modulesFans out across every selected target
rollout statusNot usedFans out across every selected target
rollout baselineSelects one moduleSelects one target
rollout start, complete, rollback, repair and upNot usedSelects one target
compatNot usedSelects one target
typegenChecks that the target accepts the moduleSelects one target
rollout plan, lint, discard and freezeNot usedIgnored with a warning, because these commands never connect
seed, status, apply and testNot usedNot used

A command that selects one target takes the --target you name, else the target marked primary = true, else the only declared target, else the ambient connection. It refuses a selection of more than one target. A rollout is a locked, resumable process on one database, and running one rollout across several databases would leave them in different phases after a partial failure.

Commands that do not use the selection take the ambient connection from --host, --ns and --db, and the default module's directories. surrealkit seed --schema billing therefore seeds database/seed rather than database/modules/billing/seed. Pass the connection explicitly for those commands.

--watch refuses a selection covering more than one module or target, because watching a whole matrix on a timer is rarely what you want.

  • Configuration: the full surrealkit.toml key reference

  • Sync: apply schema files to a database

  • Rollouts: phased migrations for shared databases

Was this page helpful?