Schema
In Sp00ky, you define your data model using sql schema files. This approach serves as the single source of truth for your application, from which TypeScript interfaces are automatically generated.
Defining Your Schema
You start by creating .surql files in your schema directory. Sp00ky advocates for a “Schema First” approach, ensuring your data structure is explicit, typesafe, and secure.
Tables and Fields
Here is a simple example of how to define a user table. Note the use of DEFINE TABLE and DEFINE FIELD to enforce data integrity.
Relationships
SurrealDB shines with its graph capabilities. Relationships are first-class citizens and can be modeled in several ways depending on your needs.
One-to-One (1:1)
For a one-to-one relationship, such as a User having a single Profile, you can simply use a record<table_name> type.
One-to-Many (1:N)
Models like a Thread having many Comments are classic one-to-many relationships. You define this by putting a record<parent> field on the child table.
Many-to-Many (N:M)
For complex relationships, like Users following other Users (graph), or Posts having Tags, use a Relation Table. This is a special table that stores the edges of the graph.
Schema Annotations
Sp00ky recognizes special comment descriptors in your .surql files. They look like ordinary SurrealQL comments (-- @name), so SurrealDB ignores them, but the Sp00ky CLI reads them to change how a table or field is generated and synced.
| Descriptor | Placement | Effect |
|---|---|---|
-- @nosync | above a DEFINE TABLE | Server-only table. Excluded from sync entirely (see below). |
-- @crdt text | above a DEFINE FIELD | Marks a collaborative text field backed by a Loro CRDT. Read/write it with useCrdtField. See CRDT. |
-- @cursor | above a DEFINE FIELD (with @crdt) | Stores per-session cursors alongside the CRDT snapshot. See CRDT. |
-- @parent | suffix on a DEFINE FIELD ... TYPE record<...> | Marks the parent side of a relationship; written automatically from the auth context, never by client code. |
Excluding a table from sync (@nosync)
Put -- @nosync on the line above a DEFINE TABLE to make it server-only. This is useful for audit logs, server-side bookkeeping, or any table that should live in your database but never reach clients.
A @nosync table is:
- Omitted from generated types (TypeScript / Dart / JSON Schema).
- Omitted from relations — it is not listed as a relation table, and any
record<...>field on another table that points at it is dropped from the generated relationships. - Never synced — no sync events are emitted for it, so no updates flow to the scheduler or SSP.
- Excluded from the scheduler snapshot and from SSP bootstrap.
- Still stored in the main database, and still included in backups.
@nosync is distinct from PERMISSIONS FOR select WHERE false. A permission-locked table is still synced (it shows up in generated types and the snapshot, it just can’t be read); @nosync removes the table from sync altogether. Reach for @nosync when a table should never leave the server.
Generated Types
Once your schema is defined, use the Sp00ky CLI to generate TypeScript definitions:
For example, the user table above might generate:
These types are then available directly in your client application, ensuring that your frontend code is always in sync with your database schema.