NEW SurrealDB and Postgres are here — more drivers on the way.
Schema as code · any database

Your schema, in the Zod you already know.

Define your schema with s.* — a drop-in for Zod. Schemic generates your database's native DDL, validates at runtime, and runs your migrations.

TypeScript-first Zod-compatible MIT

Your schema lives in four places.
None of them agree.

DDL
hand-written
Migrations
by hand
Types
drifts
Validation
bolted on

But you already describe your data in TypeScript.

// schema in, native ddl out

From s.* schema to generated SurrealQL .

Tests Soon
Studio Soon
Schemic
schema.ts TypeScript
1 export const User = defineTable("user", {
2 id: s.string(),
3 email: s.email().unique(),
4 name: s.string().optional(),
5 createdAt: s.datetime()
6 .$default(surql`time::now()`)
7 .$readonly(),
8 });
user.surql SurrealQL
1 DEFINE TABLE user TYPE NORMAL SCHEMAFULL;
2 DEFINE FIELD email ON TABLE user TYPE string
ASSERT string::is_email($value);
3 DEFINE INDEX user_email_idx ON TABLE user
FIELDS email UNIQUE;
4 DEFINE FIELD name ON TABLE user TYPE option<string>;
5 DEFINE FIELD createdAt ON TABLE user TYPE datetime
DEFAULT time::now() READONLY;
Highlighted — one s.email().unique() compiles to a typed FIELD + a UNIQUE INDEX.

Define your tables and fields in TypeScript — the native DDL is generated for you.

Open the playground

// what you get

Everything you get with Schemic.

A drop-in s.* schema, generated DDL, a migration CLI, end-to-end types, the full define* vocabulary, and a live round-trip — all from one typed file.

Drop-in Zod API

A 1:1 drop-in for z.* — refinements, defaults, coercion, and infer / input / output.

email: s.email().unique(),
name: s.string().optional(),
role: s.enum(["admin", "user"]),
createdAt: s.datetime().$default(surql`time::now()`),

Migrate an existing schema by find-replacing z.s. Nothing new to learn.

Adopt any database

Introspect a live database into typed schema files.

$ sc pull
// introspect a live DB → s.* schema

CLI toolbelt

sc status sc check sc doctor
sc rollback sc seed sc pull

End-to-end types

Infer row shapes straight from the schema.

type User = App<typeof User>
// { id: RecordId<"user">; email: string; … }
type NewUser = Create<typeof User>;
// id + $readonly fields excluded

Unsupported types are flagged in your editor — not at migration time.

Bring your own types

Store any class with a typed codec — app value in, wire type out.

s.instanceof(Money)
.$surreal(s.string(), {
encode, decode })

Live round-trip

Diff against the running database.

~ sc diff --live
+ DEFINE INDEX user_email_idx
- DEFINE FIELD legacy_id

// the cli

Migrations are first-class.

Write a migration from your schema, apply it, and check for drift against the live database — three commands, no DDL by hand.

schemic — cli
$sc generate add_users
~ diffing schema.ts against the live schema…
+ DEFINE TABLE user TYPE NORMAL SCHEMAFULL
+ DEFINE FIELD email ON TABLE user … UNIQUE
✓ wrote migrations/0001_add_users.surql
$sc migrate
▸ applying 0001_add_users.surql → surrealdb
✓ migrated user · 3 fields · 1 index
✓ schema up to date
$sc diff --live
~ comparing schema.ts ↔ running database
✓ no drift — live schema matches your code

// if you know Zod, you already know this

Schema, DDL, and migrations — one file.

Go past tables and fields. Events, functions, and access rules all live in the same typed source — and generate real DDL.

access.ts → access.surql
access.ts TypeScript
1 // events, functions & access — one file
2 User.event("welcome", {
3 when: surql`$event = 'CREATE'`,
4 then: surql`fn::welcome($after.id)`,
5 })
6
7 export const welcome = defineFunction("welcome", {
8 user: s.recordId("user"),
9 }).body(surql`CREATE log SET user = $user`)
10
11 export const account = defineAccess("account")
12 .record().signup(surql`CREATE user SET email = $email`)
13 .duration({ session: "12h" })
access.surql SurrealQL
1 DEFINE EVENT welcome ON TABLE user
2 WHEN $event = 'CREATE'
3 THEN fn::welcome($after.id);
4
5 DEFINE FUNCTION fn::welcome($user: record<user>) {
6 CREATE log SET user = $user
7 };
8
9 DEFINE ACCESS account ON DATABASE
10 TYPE RECORD
11 SIGNUP (CREATE user SET email = $email)
12 DURATION FOR SESSION 12h;

// common questions

Questions, answered.

+

Is it really just Zod?

Yes — s.* is a 1:1 drop-in for z.*. Migrate an existing schema by find-replacing z.s. If you know Zod, there's nothing new to learn.

+

Why not write the DDL by hand?

Hand-written DDL drifts from your types. Schemic keeps your schema, the generated DDL, and your TypeScript types as one source of truth — generated, never out of sync.

+

How do migrations run?

Three commands cover the whole loop: one writes a migration from the schema diff, one applies it, one checks for drift.

sc gen sc migrate sc diff --live
+

Which databases are supported?

SurrealDB and Postgres are available today; more databases are on the way through drivers. Each driver maps the full define* vocabulary — tables, fields, indexes, events, functions, and access — to that database's DDL. MIT licensed.

+

Can I adopt it on an existing database?

Yes. One command introspects the live database and generates the matching s.* schema files, so you start from the database you already have and migrate forward.

sc pull
+

Does it replace my database client?

No — it sits on top. You keep your database's official client for queries; Schemic owns the schema, the generated DDL, the migrations, and the row types.

+

Is it a query builder or an ORM?

Neither. Schemic is a schema + migrations toolkit. It owns the schema, the generated DDL, and the migrations; pair it with your database's query tools.

+

Can I drop to raw queries?

Always. Schemic owns the schema and migrations, not your queries — use your database's client directly, and compose raw expressions into defaults, events, permissions, and functions where a driver supports it.

One schema. Zero drift.

Generate the DDL, run the migrations, keep your types — straight from the schema you already wrote.