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 PostgreSQL .

Tests Soon
Studio Soon
Schemic
schema.ts TypeScript
1 import { defineTable, s, sqlExpr } from "@schemic/postgres";
2
3 export const user = defineTable("user", {
4 email: s.text()
5 .$unique()
6 .$check(sqlExpr("email ~* '^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$'")),
7 name: s.string().optional(),
8 createdAt: s.timestamptz().$default(sqlExpr("now()")),
9 });
user.sql PostgreSQL
1 CREATE TABLE "user" (
2 "id" text PRIMARY KEY,
3 "createdAt" timestamp with time zone NOT NULL DEFAULT now(),
4 "email" text NOT NULL CHECK (email ~* '^[^@\s]+@[^@\s]+\.[^@\s]+$'),
5 "name" text
6 );
7 CREATE UNIQUE INDEX "user_email_key" ON "user" ("email");
Highlighted — one s.text().$unique().$check(…) compiles to a column + a CHECK + 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.text().$unique().$check(sqlExpr("email ~* '…'")),
name: s.string().optional(),
role: s.enum(["admin", "user"]),
createdAt: s.timestamptz().$default(sqlExpr("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>
// { email: string; name?: string; role: "admin" | "user"; createdAt: Date }
type WireUser = Wire<typeof user>
// the encoded (DB-wire) row type

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.$postgres("text",
z.codec(z.string(), z.instanceof(Money), {
decode, encode }))

Live round-trip

Diff against the running database.

~ sc diff --live
+ CREATE UNIQUE INDEX user_email_key
- DROP COLUMN 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…
+ CREATE TABLE "user" (…)
+ CREATE UNIQUE INDEX "user_email_key" ON "user" ("email")
✓ wrote migrations/0001_add_users.surql
$sc migrate
▸ applying 0001_add_users.surql → postgres
✓ 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.

Relations, checks & generated columns
schema.ts TypeScript
1 import { defineTable, s, sqlExpr } from "@schemic/postgres";
2
3 export const customer = defineTable("customer", {
4 email: s.text().$unique().$check(sqlExpr("email ~* '^[^@]+@[^@]+$'")),
5 name: s.text(),
6 });
7
8 export const order = defineTable("order", {
9 customer: customer.record({ onDelete: "cascade" }),
10 quantity: s.integer().$check(sqlExpr("quantity > 0")),
11 unitPrice: s.numeric(10, 2),
12 total: s.numeric(12, 2).$generated('quantity * "unitPrice"'),
13 createdAt: s.timestamptz().$default(sqlExpr("now()")),
14 });
schema.ddl PostgreSQL
1 CREATE TABLE "customer" (
2 "id" text PRIMARY KEY,
3 "email" text NOT NULL CHECK (email ~* '^[^@]+@[^@]+$'),
4 "name" text NOT NULL
5 );
6
7 CREATE TABLE "order" (
8 "id" text PRIMARY KEY,
9 "createdAt" timestamp with time zone NOT NULL DEFAULT now(),
10 "customer" text NOT NULL,
11 "quantity" integer NOT NULL CHECK (quantity > 0),
12 "total" numeric(12, 2) NOT NULL GENERATED ALWAYS AS (quantity * "unitPrice") STORED,
13 "unitPrice" numeric(10, 2) NOT NULL
14 );
15
16 CREATE UNIQUE INDEX "customer_email_key" ON "customer" ("email");
17 ALTER TABLE "order" ADD CONSTRAINT "order_customer_fkey"
FOREIGN KEY ("customer") REFERENCES "customer" ("id") ON DELETE CASCADE;

// 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.