Skip to content
NEW

Introducing SurrealDB Studio, the new official app of SurrealDB

Learn more

1/5

Course content preview

6: SurrealKit and the first table

The previous sections treated schema as SurrealQL you typed by hand. From here we will write the same DEFINE statements into files and let SurrealKit apply them for you. The example running through the rest of this course is a project planning app, the same shape as the project planning sample in the schema design docs. We'll start small, then add graph relations, COMPUTED fields, indexes, and DEFINE EVENT as the chapters go on.

In an empty app repository (or your course practice/ folder), run:

surrealkit init --minimal


--minimal scaffolds the base SurrealKit layout without any of the optional template features (Organizations, Teams, and so on). That empty database/schema/ folder is what the rest of this course builds on.

You should see something like:

Using template: SurrealKit starter
  Optional building blocks for a new SurrealDB project

Scaffolded project in ./database

  surrealkit.toml
  ./database/
  ├── schema/
  ├── rollouts/
  ├── snapshots/
  ├── tests/
  │   ├── suites/
  │   └── fixtures/
  ├── seed/
  │   └── seed.surql
  └── setup.surql

No features selected — scaffolded a bare project.


Here is what has been created by the command, and why:

PathRole
surrealkit.tomlProject config (including optional template variables)
database/schema/Your DEFINE statements (desired catalog state); starts empty, you add files next
database/seed/Optional fixture data (init drops a stub seed.surql; replace or delete it before seeds used in part 7)
database/tests/surrealkit test suites under tests/suites/ plus fixtures. The printed tree elides two files that init does write: tests/config.toml (actors and timeouts) and a starter tests/suites/smoke.toml, both used in part 13
database/rollouts/Generated rollout manifests
database/snapshots/Schema/catalog snapshots for rollout planning
database/setup.surqlSurrealKit’s own metadata tables (see below)


Note

Plain surrealkit init (without --minimal) shows a feature checklist. Selecting items copies ready-made org/permission schema, seeds, and tests into database/. That's handy for a real app starter, but a distraction here. If you already ran the interactive init and enabled features, delete database/ and re-run surrealkit init --minimal, or remove the extra feature files by hand. See Templates in the SurrealKit README for --feature, -y, and custom --from templates.

It's now time to start our database with a single root user.

surreal start --user root --pass secret


Next, create database/schema/project.surql with a few definitions for the project table:

DEFINE TABLE project SCHEMAFULL;
DEFINE FIELD name ON project TYPE string ASSERT $value.len() > 0;
DEFINE FIELD description ON project TYPE option<string>;
DEFINE FIELD created_at ON project TYPE datetime DEFAULT time::now();


This is deliberately small, holding one SCHEMAFULL table, a required name, an optional description, and a DEFAULT timestamp from part 5.

With that file present, we are now ready to sync the schema.

The sync command is for local and other disposable databases. These are instances you're happy to wipe or reshape freely, such as a local memory database, a throwaway data directory, a CI ephemeral DB. Sync makes the live catalog match your files immediately, including REMOVE for definitions you deleted from those files. Parts 1014 cover rollouts for shared staging and production, where destructive steps wait for an explicit complete phase. Don't point everyday sync at a database you share with other people or services.

In another terminal, from the project root, apply the schema:

surrealkit sync --user root --pass secret --ns main --db main


You should see a message like this:

applied database/schema/project.surql


SurrealKit reads every file under database/schema/ and runs the DEFINE statements against your namespace and database. Check that the table came through with a one-shot query (or run the same SurrealQL in SurrealDB Studio):

echo 'INFO FOR TABLE project;' | surreal sql --user root --pass secret --ns main --db main --pretty --hide-welcome


You should see the three field definitions for this schemafull table.

Note that database/setup.surql isn't product schema. Sync applies it so SurrealKit can keep track of its own work. After your first successful sync, list the tables:

echo '(INFO FOR DB).tables;' | surreal sql --user root --pass secret --ns main --db main --pretty --hide-welcome


{
	__entity: 'DEFINE TABLE __entity TYPE NORMAL SCHEMAFULL PERMISSIONS NONE',
	__rollout: 'DEFINE TABLE __rollout TYPE NORMAL SCHEMAFULL PERMISSIONS NONE',
	__seed: 'DEFINE TABLE __seed TYPE NORMAL SCHEMAFULL PERMISSIONS NONE',
	project: 'DEFINE TABLE project TYPE NORMAL SCHEMAFULL PERMISSIONS NONE'
}


TableRole after this chapter
projectYour application table
__entityBookkeeping: file hashes, per-definition tracking, sync metadata
__rolloutRollout run state; empty until you use rollouts in part 10
__seedSeed-file hashes; empty until you run surrealkit seed in part 7


All three __-prefixed tables come from database/setup.surql, so they exist from the first sync onwards even though only __entity has anything in it yet.

Take a look at the metadata SurrealKit just wrote:

echo 'SELECT * FROM __entity; SELECT * FROM __rollout;' | surreal sql --user root --pass secret --ns main --db main --pretty --hide-welcome


__rollout should come back empty for now. __entity is where the interesting data lives at this point:

FieldMeaning
idSurrealDB record id for this metadata record
nsWhich kind of bookkeeping this is (sync, schema, or meta)
keyIdentity inside that namespace
valPayload (hash, paths, state, …)
updated_atWhen SurrealKit last wrote the record


Example (your hashes will differ):

{
	ns: 'sync',
	key: 'database/schema/project.surql',
	val: { hash: '15966cde…aa3a' },
}


PieceMeaning
keyPath of the schema file SurrealKit applied
val.hashSHA-256 of that file’s contents


On the next sync, SurrealKit re-hashes each file on disk. If the hash still matches this record, it skips re-running that file’s DEFINE statements.

Examples:

{ ns: 'schema', key: 'table::project', val: { … } }
{ ns: 'schema', key: 'field:project:name', val: { … } }
{ ns: 'schema', key: 'field:project:description', val: { … } }
{ ns: 'schema', key: 'field:project:created_at', val: { … } }


PieceMeaning
keyStable id for a catalog object (table::…, field:table:name, and later indexes, and so on)
val.source_pathWhich .surql file owns this definition
val.file_hashHash of that whole file (same as the syncrecord for the file)
val.statement_hashHash of this definition’s normalised statement. Used to detect “this field changed” even when other lines in the file didn’t
val.stateLifecycle flag; active means SurrealKit currently expects this object to exist


These records are what let prune and rollouts reason about individual tables and fields, not just whole files.

{
	ns: 'meta',
	key: 'last_sync',
	val: '2026-07-16T03:49:09.93826Z',
}


last_sync records when the last sync finished. Other meta keys (like shared-database markers) can show up later once you configure shared or multi-writer setups.

Part 3’s DIY schema:person history table was a manual version of the same idea. SurrealKit keeps that history for you in __entity: which file last won, which definitions it manages, and whether the file on disk still matches. That's why sync can skip unchanged files and still know what to remove when you delete a DEFINE from a disposable database.

Hand-written DEFINE in SurrealDB StudioSurrealKit database/schema/
Fine for explorationSame statements, committed with your app
Easy to lose track of what ran whereContent hashes and per-definition records in __entity
Hard to reproduce on stagingSame files drive every environment


As this course grows the planning schema, you'll mostly edit the .surql files and run sync again (or leave sync --watch running, which part 7 introduces). You won't re-paste every DEFINE by hand when you add activity, relations, or indexes later.

That same file tree is what rollouts will plan from in part 10: once the database is shared, you still change the files first, then generate a reviewed manifest instead of letting sync apply (and prune) immediately.

You can store local and disposable credentials in .env so you can run surrealkit sync without flags:

SURREALDB_HOST=http://localhost:8000
SURREALDB_NAMESPACE=dev
SURREALDB_NAME=planning
SURREALDB_USER=root
SURREALDB_PASSWORD=secret


Use a different host, namespace, or secret store for staging and production. The CLI flags and env vars have the same shape for sync and rollout; what changes is which database those variables point at, and which command you run. Until CI owns production secrets, keep shared-database credentials out of the same .env you use for local sync.

You should have:

  • A database/schema/project.surql file with the schema you defined

  • A database that answers INFO FOR TABLE project with your field definitions

  • A sense of sync as “make the live catalog match these files”, with __entity explaining how SurrealKit remembers what it applied

Next: add activities on a schedule, split schema across files, and use sync --watch while you iterate.

Previous

5: Automation

Next lesson

7: Activities and seed data

SurrealDB

The context layer for AI agents.

Documents, graphs, vectors, time-series, and memory.
One transaction, one query, one deployment.

Explore with AI

Stay in the loop

Tutorials, AI agent recipes, and product updates, every two weeks.

Independently verified

SOC 2 Type 2

GDPR

Cyber Essentials Plus

ISO 27001

Trust Centre

Copyright © 2026 SurrealDB Ltd. Registered in England and Wales. Company no. 13615201

Registered address: 3rd Floor 1 Ashley Road, Altrincham, Cheshire, WA14 2DT, United Kingdom

Trading address: Huckletree Oxford Circus, 213 Oxford Street, London, W1D 2LG, United Kingdom