Skip to content

SurrealKit

Schema modules and targets

Available since: v1.0

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 rows 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. See Configuration for the full key reference.

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 surrealctl 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 not every subcommand acts on them yet. In 1.0.0-beta.1:

CommandHonours --schemaHonours --target and --all
syncYesYes
rollout baselineYesNo
Other rollout verbsNoNo
seedNoNo
typegenNoNo
testNoNo

Everything in the "No" column uses 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, and surrealkit typegen --target acme introspects the ambient database. Pass the connection explicitly for those commands until this is resolved.

--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?