Introduction
Schemic lets you describe a PostgreSQL table once, in Zod, and derive three things from that single definition: the SQL schema (DDL), runtime validation, and a fully-typed JS-to-database mapping. There is no code generation step and no separate schema language. Your Zod schema is the schema.
You write this:
import { defineTable, s, sqlExpr } from "@schemic/postgres";
export const user = defineTable("user", {
email: s.varchar(255).$unique(),
name: s.text(),
age: s.smallint().optional(),
createdAt: s.timestamptz().$default(sqlExpr("now()")),
});…and the same definition gives you the DDL below, a validator, and an App type whose createdAt is a JavaScript Date even though Postgres stores it as timestamptz.
CREATE TABLE "user" (
"id" text PRIMARY KEY,
"age" smallint,
"createdAt" timestamp with time zone NOT NULL DEFAULT now(),
"email" varchar(255) NOT NULL,
"name" text NOT NULL
);
CREATE UNIQUE INDEX "user_email_key" ON "user" ("email");You never wrote the id primary key — Schemic adds an implicit "id" text PRIMARY KEY unless you declare your own.
What you get from one definition
Postgres DDL
Emit CREATE TABLE, columns, primary and foreign keys, unique and secondary indexes, defaults, and checks straight from your schema. No DDL to hand-write or keep in sync.
Runtime validation
Every field is a real Zod schema — the z.* API you already know — so parsing, refinements, formats, and error messages all behave exactly as in Zod.
Typed JS-to-DB mapping
Read and write rows through codecs that convert between your app values and the Postgres wire format, with static types for both sides.
A fourth thing falls out of the same source of truth: a declarative, reviewable migration history the CLI generates by diffing your schema against the database.
The mental model
A field is an ordinary Zod schema plus a little Postgres metadata. The mapping between your code and the database rides Zod’s two native channels:
- The app side (
z.output) is the value you work with in TypeScript: aDate, astring, aUint8Array. - The wire side (
z.input) is what Postgres stores and returns.
A codec moves a value between the two: decode reads a row into app values, and encode writes app values back. That is why a timestamptz column is a Date in your code, for free. Keeping these two sides distinct is the core idea; the encoded and decoded sides guide covers it in full.
Prerequisites
Start here
Quickstart: your first schema and migration Concept: the encoded and decoded sidesHow these docs are organized
- Quickstart walks you from an empty directory to a running migration — no Postgres server required.
- Concepts explain why the driver works the way it does: the two channels, how a schema becomes DDL, and the migration model.
- Guides are task-focused: define a table, add constraints and defaults, model foreign keys, index, and run migrations.
- Reference is the exhaustive lookup: the type mapping, every field method, the definers, the config, and the CLI.