Constraints, defaults & permissions
A field carries more than a type. Schemic’s $-prefixed methods attach SurrealQL clauses that SurrealDB enforces on every write, regardless of whether the write came through your app. This guide covers the four you reach for most: assertions, defaults, read-only, and permissions.
All of these are clauses on a DEFINE FIELD; see from schema to DDL for how they compose.
Assert a constraint
Use .$assert(expr) to emit an ASSERT clause. The expression is raw SurrealQL written with the surql tag, and $value refers to the field’s value:
export const Product = defineTable("product", {
id: s.string(),
price: s.decimal().$assert(surql`$value > 0`),
sku: s.string().$assert(surql`string::len($value) = 12`),
});produces:
DEFINE TABLE product TYPE NORMAL SCHEMAFULL;
DEFINE FIELD price ON TABLE product TYPE decimal ASSERT $value > 0;
DEFINE FIELD sku ON TABLE product TYPE string ASSERT string::len($value) = 12;For common rules, the string formats write the assertion for you — s.email() emits ASSERT string::is_email($value), s.url() emits ASSERT string::is_url($value), and so on. Reach for .$assert when no format fits.
Zod’s own constraint helpers are also available as $-methods that translate to SurrealQL: .$min, .$max, .$length, .$regex, .$gt, .$gte, .$lt, .$lte.
Set a default
.$default(expr) emits a DEFAULT clause: a value SurrealDB applies when the field is absent on insert. It is usually a surql expression:
createdAt: s.datetime().$default(surql`time::now()`),
status: s.string().$default("draft"),A field with a $default becomes optional in the create shape, because the database fills it — so you omit it when calling encode.
Two related clauses:
.$defaultAlways()emitsDEFAULT ALWAYS, reapplying the default on update as well as insert..$value(expr)emits aVALUEclause that always computes the stored value from the expression, overriding any input.
Make a field read-only
.$readonly() emits READONLY: SurrealDB allows the field on create but rejects any later change to it. Common for createdAt:
createdAt: s.datetime().$default(surql`time::now()`).$readonly(),Restrict access with permissions
.$permissions(spec) emits a field-level PERMISSIONS clause, controlling which record-level operations may touch the field. Pass an object keyed by operation, each value a surql condition:
export const User = defineTable("user", {
id: s.string(),
email: s.email(),
ssn: s.string().$permissions({
select: surql`$auth.admin = true`,
update: false,
}),
});produces:
DEFINE TABLE user TYPE NORMAL SCHEMAFULL;
DEFINE FIELD email ON TABLE user TYPE string ASSERT string::is_email($value);
DEFINE FIELD ssn ON TABLE user TYPE string PERMISSIONS FOR select WHERE $auth.admin = true, FOR update NONE;A surql condition becomes FOR <op> WHERE <condition>; true becomes full access for that operation, false becomes NONE. Table-level permissions work the same way via .permissions(...) on the table — see the definers reference.
Verify it
Generate a migration and read the emitted clauses, or preview against a running database:
npx schemic diff --liveWhere to go next
- Field methods reference — every
$-method and its clause. - Indexes, events & functions — table-level machinery.
- Define a table — the basics this builds on.