Getting started
This page takes a small schema from an empty directory to a compiled, apply-ready script.
Install
Surform is a single Rust program with no runtime dependencies. You need the Rust toolchain (rustup installs it in one command), then clone this repository and build the compiler:
cargo install --path .That puts the surform binary on your PATH. Check it:
Declarative SurrealDB schemas — SDL to SurrealQL DDL and back
Usage: surform [OPTIONS] <COMMAND>
Commands:
check Parse an SDL file and report diagnostics
build Compile an SDL file to SurrealQL DDL
fmt Write an SDL file's canonical form
normalize Merge rendered INFO interchange into an SDL file and re-emit the canonical SDL
migrate Schema migrations — generate, commit, push, status, revert
lint Lint an SDL file — advisory findings, exit 1 when any exist
help Print this message or the help of the given subcommand(s)
Options:
--json Render diagnostics as JSON (one object per line) instead of the graphical format
-h, --help Print help
-V, --version Print versionThe schema
Here is the quick-start schema — a shop with users and posts. This is examples/shop.surdl in the docs sources; every panel on this page is produced from this file. The examples on these pages are editable: the verdict and the DDL recompute as you type, and Reset brings the verified text back.
// The quick-start schema. `//` lines are ordinary comments — they
// document the file and never reach the database.
namespace acme;
database shop;
/// People who buy things.
table user {
schemafull;
// The engine rejects anything that does not look like an email.
field email: string { assert: string::is_email($value); };
field name: string;
// No two accounts share an email — a plain comment, since doc
// comments only attach to top-level definitions.
index unique email_unique {
fields: email;
};
};
/// Anything a user publishes.
table post {
schemafull;
field title: string { assert: string::len($value) > 0; };
field body: string;
// Who wrote this post.
field author: record<user>;
index author_idx {
fields: author;
};
};
DEFINE NAMESPACE OVERWRITE acme;
USE NS acme;
DEFINE DATABASE OVERWRITE shop;
USE NS acme DB shop;
DEFINE TABLE OVERWRITE `user` TYPE NORMAL SCHEMAFULL COMMENT 'People who buy things.';
DEFINE FIELD OVERWRITE email ON `user` TYPE string ASSERT string::is_email($value);
DEFINE FIELD OVERWRITE name ON `user` TYPE string;
DEFINE INDEX OVERWRITE email_unique ON `user` FIELDS email UNIQUE;
DEFINE TABLE OVERWRITE `post` TYPE NORMAL SCHEMAFULL COMMENT 'Anything a user publishes.';
DEFINE FIELD OVERWRITE title ON `post` TYPE string ASSERT string::len($value) > 0;
DEFINE FIELD OVERWRITE body ON `post` TYPE string;
DEFINE FIELD OVERWRITE author ON `post` TYPE record<user>;
DEFINE INDEX OVERWRITE author_idx ON `post` FIELDS author;
///doc comments become the definition'sCOMMENTin SurrealDB — your documentation travels with the schema.field email: string { assert: … };— anything after:is SurrealQL, checked by SurrealDB itself. Surform does not invent a second expression language.index unique email_uniqueis nested inside its table, the way you think about it; the compiler emits the standaloneDEFINE INDEXSurrealDB expects.field author: record<user>is a graph-style reference — types pass through verbatim.
Check it
check parses the file and reports problems. On a clean file it says nothing at all — silence is success:
The same check on a file with a misspelled permissions clause:
namespace acme;
database shop;
table person {
schemafull;
field name: string;
permisions: full;
};SDL-E1003 (spec/README.md#design-rules-settled)
x unknown clause `permisions` in table definition
,-[7:3]
6 | field name: string;
7 | permisions: full;
: ^^^^^|^^^^
: `-- unknown clause key
8 | };
`----
help: did you mean `permissions`?
The diagnostic points at the offending line, names the code, and suggests a fix. The CLI reference has more on reading diagnostics.
Compile it
build prints the SurrealQL DDL to stdout — pipe it to a file, to surreal sql, or to your deployment pipeline:
DEFINE NAMESPACE OVERWRITE acme;
USE NS acme;
DEFINE DATABASE OVERWRITE shop;
USE NS acme DB shop;
DEFINE TABLE OVERWRITE `user` TYPE NORMAL SCHEMAFULL COMMENT 'People who buy things.';
DEFINE FIELD OVERWRITE email ON `user` TYPE string ASSERT string::is_email($value);
DEFINE FIELD OVERWRITE name ON `user` TYPE string;
DEFINE INDEX OVERWRITE email_unique ON `user` FIELDS email UNIQUE;
DEFINE TABLE OVERWRITE `post` TYPE NORMAL SCHEMAFULL COMMENT 'Anything a user publishes.';
DEFINE FIELD OVERWRITE title ON `post` TYPE string ASSERT string::len($value) > 0;
DEFINE FIELD OVERWRITE body ON `post` TYPE string;
DEFINE FIELD OVERWRITE author ON `post` TYPE record<user>;
DEFINE INDEX OVERWRITE author_idx ON `post` FIELDS author;
Two conventions the output relies on:
- Every emitted
DEFINEcarriesOVERWRITE(one exception: a user with no credential defines once — Users & roles). Applying the script twice — or applying it over a database that already has some of the definitions — redefines cleanly instead of erroring. Your schema file is the desired state; applying it converges the database to it. - After each
DEFINE NAMESPACE/DEFINE DATABASEthe compiler emits the matchingUSEline, so one script crosses namespace and database boundaries safely (a database with no preceding namespace omits theUSE).
Apply it
The build output is ordinary SurrealQL — run it anywhere SurrealDB 3.3 runs. For example, with the SurrealDB CLI installed:
surform build schema.surdl > schema.surql
surreal sql --conn ws://localhost:8000 --user root --pass … --ns acme --db shop < schema.surqlSurform itself never touches your database connection — it produces the script, your existing tooling applies it. (Adopting a database — the reverse trip — is one command.)
Keep it tidy
fmt prints the canonical form of a file: one clause style, one clause order, shortest spellings. Team members stop arguing about layout — the compiler owns it:
namespace acme; database shop;
table `user` { schemafull; field name:string; field email: string { readonly; }; }namespace acme;
database shop;
table `user` {
schemafull;
field name: string;
field email: string {
readonly;
};
};
Let the linter catch the traps
Some mistakes are legal but dangerous. This table compiles, applies, and silently grants everyone everything — permissions: full; lets anyone, even unauthenticated users, read and write every record:
namespace acme;
database shop;
table secrets {
schemafull;
field payload: string;
permissions: full;
};SDL-L0001 (spec/table.md)
! table `secrets` in database `acme/shop`: permissions are `full` — anyone, even unauthenticated users, can access every record
,-[4:1]
3 |
4 | ,-> table secrets {
5 | | schemafull;
6 | | field payload: string;
7 | | permissions: full;
8 | |-> };
: `---- `permissions: full` on this table
`----
help: `full` grants everyone — including unauthenticated connections — access to every record; omit the clause for the secure default (implicit `none`), or scope operations in a `permissions`
block
surform lint explains why and links to the fix. Omitting the clause keeps the database's secure default — every operation denied — and never warns. The full catalog of warnings lives on the linter page.
Where next
- How a schema file works — files, ordering, comments, names, and the apply model in depth.
- Tables is the first language page; the sidebar walks the rest.
- The surform CLI — every command and flag.