Getting Started
Install Cerial, define your first schema, generate a type-safe client, and run your first queries against SurrealDB.
Cerial is under active development and not yet ready for production use. APIs may change between releases. Use it for experimentation and side projects — production stability is coming soon.
In this guide, you'll define a schema with two related models, generate a fully typed TypeScript client, and use it to create, query, update, and delete records in SurrealDB — all with complete type safety and zero hand-written SQL.
By the end, you'll have a working User and Post setup with relations, auto-timestamps, and type-safe queries.
Prerequisites
Cerial works with Node.js 20+, Bun 1+, and Deno. You need one of these runtimes installed, plus a running SurrealDB instance.
- Node.js 20 or later — install from nodejs.org
- Bun 1.0 or later — install with
curl -fsSL https://bun.sh/install | bash - Deno — install from deno.com
Install SurrealDB from surrealdb.com/install and start it with surreal start.
Your First Cerial Project
-
Install Cerial
Add Cerial to your project:
npm install cerialpnpm add cerialyarn add cerialbun add cerialThis gives you both the ORM runtime and the
cerialCLI for code generation. -
Define Your Schema
Create a
schemas/directory and add a.cerialfile. This is where you describe your data models — Cerial reads these files and generates TypeScript types and a client from them.Create
schemas/schema.cerial:model User { id Record @id email Email @unique name String age Int? isActive Bool @default(true) createdAt Date @createdAt updatedAt Date @updatedAt posts Relation[] @model(Post) } model Post { id Record @id title String content String? authorId Record author Relation @field(authorId) @model(User) createdAt Date @createdAt }Here's what each part means:
id Record @id— Every model needs a primary key.Record @idtells Cerial this is a SurrealDB record ID.Email— A built-in type that validates email format at the database level.Int?— The?makes a field optional. Optional fields can be omitted on create and show asundefinedwhen absent.@default(true)— Sets a default value. If you don't passisActivewhen creating a user, it defaults totrue.@createdAt/@updatedAt— Auto-managed timestamps.@createdAtsets the time on record creation,@updatedAtupdates it on every write.@unique— Creates a unique index on the field — no two users can share the same email.Relation[] @model(Post)— A virtual field representing the reverse side of a one-to-many relation. It doesn't store anything in theUsertable — Cerial queriesPostrecords that point back to this user.authorId Record+author Relation @field(authorId) @model(User)— The forward side of the relation.authorIdstores the actual foreign key, andauthoris the virtual relation field linked via@field(authorId).
-
Generate the Client
Run the
generatecommand to produce your type-safe client. You can either pass paths directly as CLI flags, or use a config file:npx cerial generate -s ./schemas -o ./db-client-ssets the schema directory,-osets the output directory.Create a
cerial.config.tsat your project root:import { defineConfig } from 'cerial'; export default defineConfig({ schema: './schemas', output: './db-client', });Then generate with no flags needed:
npx cerial generateThe config file approach is recommended for projects — it keeps your options in one place and supports advanced features like multi-schema setups and formatting preferences.
The generated output looks like this:
db-client/ ├── client.ts # CerialClient class ├── models/ # One file per model with full types │ ├── user.ts │ ├── post.ts │ └── index.ts ├── internal/ │ ├── model-registry.ts # Runtime model metadata │ ├── migrations.ts # DEFINE TABLE/FIELD statements │ └── index.ts └── index.ts # Main exportsRun
npx cerial generate --watchduring development to automatically regenerate the client whenever you change a.cerialfile. -
Connect to SurrealDB
Import the generated client and connect to your running SurrealDB instance:
import { CerialClient } from './db-client'; const client = new CerialClient(); await client.connect({ url: 'http://localhost:8000', namespace: 'main', database: 'main', auth: { username: 'root', password: 'root' }, }); // Eagerly run all migrations upfront await client.migrate();client.migrate()creates all tables, fields, and indexes in SurrealDB based on your schema. You can call it right after connecting to set everything up at once. If you skip it, Cerial still handles migrations lazily — each model's schema is applied automatically before its first query. -
Create Records
Use
client.db.User.create()to insert a new record. Every model is accessible throughclient.db:const user = await client.db.User.create({ data: { email: 'alice@example.com', name: 'Alice', age: 28, }, }); console.log(user.id); // CerialId — not a plain string console.log(user.createdAt); // Date — auto-set by @createdAt console.log(user.isActive); // true — default from @default(true)user.idis aCerialIdobject, not a plain string. It has.id(the raw value),.table(the table name),.toString()for display, and.equals()for comparison. You'll pass it towhereclauses to look up this record later.Now create a post linked to this user:
const post = await client.db.Post.create({ data: { title: 'Hello, SurrealDB!', content: 'My first post using Cerial.', authorId: user.id, }, }); -
Query Records
Cerial provides several query methods. The return types narrow automatically based on what you select:
// Find many with filtering, selection, and pagination const activeUsers = await client.db.User.findMany({ where: { isActive: true }, select: { id: true, name: true, email: true }, limit: 10, }); // activeUsers: { id: CerialId; name: string; email: string }[]Use
includeto load related records alongside the main query:// Find one user with their posts const userWithPosts = await client.db.User.findOne({ where: { id: user.id }, include: { posts: { limit: 5, orderBy: { createdAt: 'desc' } }, }, }); // userWithPosts: (User & { posts: Post[] }) | nullfindOnereturnsT | null— always check for null.findManyreturnsT[](an empty array if nothing matches). -
Update & Delete
Update a specific record by its ID:
const updated = await client.db.User.updateUnique({ where: { id: user.id }, data: { name: 'Alice Smith' }, }); // updated.updatedAt is automatically refreshed by @updatedAtDelete a record:
const deleted = await client.db.User.deleteUnique({ where: { id: user.id }, });When you're done, disconnect from the database:
await client.disconnect();
What's Next?
You've covered the basics — schema definition, client generation, and CRUD operations. Cerial has much more to offer:
- Schema — Full schema syntax — field types, decorators, arrays, optionals, and more.
- Relations — 1, 1, and N relations with nested creates, connects, and cascade deletes.
- Queries — All query methods — findOne, findMany, upsert, count, exists, and more.
- Transactions — Atomic execution with array, callback, and manual modes.
- Filtering — Comparison, string, array, logical, and nested filter operators.
- Objects & Tuples — Embedded object types and fixed-length tuple types with sub-field select.
- Type System — CerialId, NONE vs null, generated types, and dynamic return types.