Skip to main content

Overview

A schema in Ship is two things in one file: the Drizzle table that defines the database shape and the Zod schemas that validate input and output at the edge. Both colocate in the resource folder:
There is no separate schemas package and no generate step that copies schemas around. The schema file is the source of truth; codegen reads it to build the typed DbService (pnpm --filter api codegen).

The Drizzle table

Every table spreads baseColumns from resources/base.schema.ts, which gives it a uuid id, timestamps, and a soft-delete column:
apps/api/src/resources/base.schema.ts
A resource table extends it with its own columns:
apps/api/src/resources/users/users.schema.ts
deletedAt is the soft-delete column. Reads filter where: { deletedAt: null }; deletes set the timestamp instead of removing the row. Every baseColumns table works this way.

The Zod schemas

Derive the validation schema from the table with createSelectSchema, then refine the fields you want stricter rules on. Export a publicSchema — the shape endpoints return to clients:
apps/api/src/resources/users/users.schema.ts
Ship is on Zod 4. Use z.email(), z.url(), z.uuid() — not Zod 3’s z.string().email().

Enums come from app-constants

Never inline a string enum. Shared constants live in the app-constants package, so the API, the web app, and your schemas agree on the same values:

Schemas are the endpoint contract

The same Zod schemas drive every endpoint. .input() and .output() validate at runtime and define the types the typed client sees — there is no second source of truth to keep in sync:
apps/api/src/resources/users/endpoints/list.ts
paginationSchema and listResultSchema(itemSchema) come from resources/base.schema.ts and standardise paged responses ({ results, count, pagesCount }).

Reusing a schema for input

Build write inputs by pick-ing, extend-ing, or partial-ing the resource schema. The avatar update endpoint takes only fullName plus an uploaded file:
apps/api/src/resources/users/endpoints/current.patch.ts

Validating elsewhere

The same Zod schema validates on the client too — feed it to useApiForm (provided by the Auth plugin) or a manual zodResolver:
Or parse imperatively with safeParse:
For more on Zod, see the Zod documentation.