Quickstart
This walks you from an empty directory to a running migration. Because the driver runs on embedded PGlite, you do not need to install or start a Postgres server to follow along.
Create a project
create schemic is interactive: pick the PostgreSQL driver when it asks, and it installs everything (including the embedded @electric-sql/pglite engine) and scaffolds your project. Run it in an empty directory for a new project, or inside an existing one — it merges the @schemic dependencies and a db script into your package.json without touching the rest of your setup. It scaffolds:
schemic.config.ts— apostgresConnectionpointed at a local PGlite data dirdatabase/schema/tables.ts— a sample schemadatabase/seed/index.ts— the seed scriptdatabase/migrations/meta/_snapshot.json— migration state.env.example— connection environment template
Read the generated schema
Open database/schema/tables.ts. This is the single source of truth; the DDL below is derived from it.
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()")),
});It generates this DDL:
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");A few things to notice, each of which you will use constantly:
- You never declared
id— Schemic adds an implicit"id" text PRIMARY KEY. Declare your own with.primaryKey(...)to replace it. s.varchar(255)maps to avarchar(255)column, and.$unique()adds aCREATE UNIQUE INDEX..optional()makesagenullable; required fields emitNOT NULL..$default(sqlExpr("now()"))emits a database-sideDEFAULTfrom a raw SQL expression.
Generate your first migration
schemic gen diffs your schema against the recorded snapshot and writes a migration for the difference.
The migration is plain, reviewable SQL with forward and rollback bodies. Open it before applying anything.
Apply it to the database
schemic migrate (alias up) applies every pending migration in order, against the embedded PGlite database from your config — no server to start.
Confirm what is applied with schemic status:
Evolve the schema
Change the schema and Schemic writes the next migration for just the delta. Add a column:
export const user = defineTable("user", {
email: s.varchar(255).$unique(),
name: s.text(),
age: s.smallint().optional(),
bio: s.text().optional(),
createdAt: s.timestamptz().$default(sqlExpr("now()")),
});npx schemic gen add_bio
npx schemic migrateThe second migration contains only the new column (ALTER TABLE "user" ADD COLUMN "bio" text) — it does not re-create the table.
Read and write rows
Your table definition carries codecs that bridge app values and the Postgres wire format. encode builds the payload you write; decode validates a returned row and converts it to app values (so a timestamptz comes back as a Date). See Read & write rows for the full pattern against the PGlite client.