• Start

Using SurrealKit as a library

Using SurrealKit as a library

Embed SurrealKit directly in a Rust application to connect to SurrealDB, sync schema at startup, run rollouts, and seed data without the CLI.

SurrealKit is published as a Rust crate, so you can drive connections, schema sync, rollouts, and seeding from application code rather than the CLI. This suits applications that apply schema inside their own process at startup, for example with an embedded SurrealDB backend (RocksDB, SpeeDB) or when running SurrealDB in the same binary during tests.

[dependencies]
surrealkit = "0.7"

SurrealKit gives you two ways to get schema into a database. Pick based on whether the database is disposable or shared.

-SyncRollout
Mental modelDeclarative desired state: "make the database match this schema"Staged, reviewable migration with an explicit undo
AppliesAll changed files, idempotentlyOrdered steps across start / complete / rollback phases
Removes objectsAutomatically (prune)Only in the complete phase, via explicit steps
ReversibleNoYes (rollback)
Use whenDev, test, CI, single-owner or embedded databasesShared and production databases needing expand → contract and a rollback path

The two work together: use sync for everyday schema, and use a rollout when a change needs to land safely while old and new code run side by side.

DbCfg reads connection details from the same environment variables as the CLI (SURREALDB_HOST, SURREALDB_NAMESPACE, and so on), with optional overrides. connect builds the surrealdb::Surreal client and authenticates:

use surrealkit::{DbCfg, DbOverrides, connect};

let cfg = DbCfg::from_env(None, &DbOverrides::default())?;
let db = connect(&cfg).await?;

DbOverrides lets you override specific fields programmatically while leaving the rest to the environment:

let cfg = DbCfg::from_env(None, &DbOverrides {
host: Some("http://localhost:8000".to_string()),
..Default::default()
})?;

For an embedded SurrealDB engine (mem://, rocksdb://, speedb://), construct a Surreal client directly and pass it to any library function:

use surrealdb::engine::any::connect;
use surrealdb::opt::Config;
use surrealdb::opt::capabilities::Capabilities;

let db = connect(("mem://", Config::new().capabilities(Capabilities::all()))).await?;
db.use_ns("main").use_db("main").await?;

Use the [Sync] builder to apply a slice of schema files to the database. By default it prunes objects that are no longer present and stops on the first error (prune = true, fail_fast = true):

use surrealkit::{EmbeddedSchemaFile, Sync, Surreal};
use surrealkit::engine::any::Any;

static SCHEMA: &[EmbeddedSchemaFile] = &[EmbeddedSchemaFile {
path: "database/schema/person.surql",
sql: "DEFINE TABLE person SCHEMALESS;",
}];

async fn run(db: &Surreal<Any>) -> anyhow::Result<()> {
// Defaults: prune = true, fail_fast = true.
Sync::embedded(SCHEMA).run(db).await?;
Ok(())
}

The builder methods customise behaviour before calling run:

Sync::embedded(SCHEMA)
.prune(false) // don't remove objects missing from SCHEMA
.allow_all_statements(true) // permit non-DEFINE statements (INSERT/UPDATE/…)
.allow_shared_prune(true) // permit pruning on a shared database
.dry_run(true) // report what would change without applying
.run(db)
.await?;

Sync runs setup internally and reads nothing from the filesystem. To embed your .surql files at compile time instead of hand-writing the slice, use the embed_schema! macro.

The two fields serve different purposes:

  • path is a stable tracking key, not a path that must exist on disk. SurrealKit stores it in its metadata tables to identify the file, detect content changes, and prune files that disappear. Keep it stable across releases: renaming it makes SurrealKit treat the old key as deleted and the new one as added.

  • sql is the content that gets applied. Changing sql while holding path constant is exactly what triggers a re-apply on the next sync.

Rollouts are defined entirely in code, with no TOML or .surql files on disk required. Build a spec with [RolloutSpec::builder] and drive it with the [Rollout] facade.

planned → running_start → ready_to_complete → running_complete → completed

└── running_rollback → rolled_back

completed and rolled_back are terminal. failed and the running_* states are stuck states from an interrupted run; recover them with [Rollout::abandon] (or the CLI repair command). Only one rollout may be in a non-terminal state at a time.

use surrealkit::{
Rollout, RolloutSpec, RolloutStep, RolloutPhase, RolloutCompatibility,
EmbeddedSchemaFile, EntityKey, EntityKind, Surreal,
};
use surrealkit::engine::any::Any;

// The desired schema once the rollout completes (used to compute the managed
// catalog). Pass `&[]` if your steps fully describe the entity changes.
static TARGET: &[EmbeddedSchemaFile] = &[EmbeddedSchemaFile {
path: "database/schema/account.surql",
sql: "DEFINE TABLE account SCHEMAFULL;",
}];

async fn run(db: &Surreal<Any>) -> anyhow::Result<()> {
let spec = RolloutSpec::builder("20260604__add_account")
.name("Add account table")
.compatibility(RolloutCompatibility::Phased)
// Expand: add the new table (non-destructive).
.step(RolloutStep::apply_schema(
"create_account", RolloutPhase::Start,
"DEFINE TABLE account SCHEMAFULL;",
))
// Backfill during complete. run_sql must be safe to re-run.
.step(RolloutStep::run_sql(
"backfill", RolloutPhase::Complete,
"UPDATE account SET active = true WHERE active = NONE;",
))
// Undo the expand phase on rollback.
.step(RolloutStep::remove_entities(
"undo", RolloutPhase::Rollback,
vec![EntityKey { kind: EntityKind::Table, scope: None, name: "account".into() }],
))
.build();

let rollout = Rollout::new(spec, TARGET);

rollout.start(db).await?; // expand (blocks if another rollout is active)
// ... deploy new code, drain traffic ...
rollout.complete(db).await?; // contract, or call rollout.rollback(db).await?
Ok(())
}

Each [RolloutStep] carries exactly one action, built with a constructor, so invalid combinations cannot be represented:

ConstructorWhat it does
RolloutStep::apply_schema(id, phase, sql)Apply inline DDL (OVERWRITE is injected; safe to retry)
RolloutStep::run_sql(id, phase, sql)Run data-mutation SQL (must be safe to re-run)
RolloutStep::assert_sql(id, phase, sql, expect)Assert a query's output equals expect
RolloutStep::remove_entities(id, phase, entities)REMOVE … IF EXISTS the given objects

Entities are identified with EntityKey { kind: EntityKind, scope, name }, where EntityKind is an enum (Table, Field, Index, Module, and so on) rather than a string.

If a process dies mid-rollout, the rollout is left in a running_* or failed state and blocks new rollouts. Inspect the recorded state, then recover it:

use surrealkit::{Rollout, RolloutSpec, Surreal};
use surrealkit::engine::any::Any;

async fn run(db: &Surreal<Any>, spec: RolloutSpec) -> anyhow::Result<()> {
// Inspect the recorded state.
let rollout = Rollout::new(spec, &[]);
if let Some(report) = rollout.status(db).await? {
println!("{:?}: {:?}", report.status, report.last_error);
}

// Last resort: force a wedged rollout to a terminal state so a new one can
// start. This does NOT revert schema changes already applied. Reconcile
// those with a fresh sync or a follow-up rollout.
Rollout::abandon(db, "20260604__add_account").await?;
Ok(())
}

[seed] runs the .surql files in a project's seed/ directory (lexicographic order), applying template variables:

use surrealkit::{seed, TemplateVars, Surreal};
use surrealkit::engine::any::Any;

async fn run(db: &Surreal<Any>) -> anyhow::Result<()> {
seed(db, "database", &TemplateVars::default()).await?;
Ok(())
}

The second argument is the project folder (the directory containing seed/), matching the CLI's --folder / SURREALDB_FOLDER.

${VAR} placeholders in schema, seed, or rollout SQL are substituted from a [TemplateVars] map before execution. Lookups are case-insensitive, and an undefined variable is an error naming the missing key and file. Pass them via Sync::vars(...), Rollout::vars(...), or the seed argument:

use surrealkit::{Sync, TemplateVars};

let mut vars = TemplateVars::default();
vars.insert("schema_prefix", "acme");

Sync::embedded(SCHEMA).vars(vars).run(db).await?;

See Template variables for the full resolution rules.

SurrealKit maintains two internal tables in your namespace and database, created automatically:

TablePurpose
__entityTracks every schema object SurrealKit manages (content hash, tracking key)
__rolloutTracks rollout execution state (see the status lifecycle above)

Was this page helpful?