Field Types

Nanoka's t object provides a small set of field builders that map directly to SQLite column types and produce Zod validators. All modifiers are chainable and type-safe.

Field Types

t.string()

Maps to a SQLite text column. Accepts optional min, max, and email validators.

import { t } from '@nanokajs/core'

const fields = {
  name:  t.string().min(1).max(255),
  email: t.string().email(),
  bio:   t.string().optional(),
}

t.uuid()

Maps to a SQLite text column. When combined with .primary().readOnly(), Nanoka automatically sets $defaultFn(() => crypto.randomUUID()) on the Drizzle column — you never need to generate the ID manually.

const fields = {
  id: t.uuid().primary().readOnly(),
}

Because readOnly() excludes the field from create and update inputs, callers cannot supply an id. The UUID is generated server-side on every create() call.

t.integer()

Maps to a SQLite integer column. The Zod validator enforces integer values (z.number().int()).

const fields = {
  age:  t.integer().min(0).max(150),
  rank: t.integer().optional(),
}

t.number()

Maps to a SQLite real column. Accepts floating-point values.

const fields = {
  price:  t.number().min(0),
  rating: t.number().min(0).max(5),
}

t.boolean()

Maps to a SQLite integer({ mode: 'boolean' }) column. Drizzle handles the 0/1 ↔ true/false conversion automatically.

const fields = {
  isActive: t.boolean().default(true),
  isAdmin:  t.boolean().default(false),
}

t.timestamp()

Maps to a SQLite integer({ mode: 'timestamp_ms' }) column. Values are stored as milliseconds since epoch and returned as Date objects.

const fields = {
  createdAt: t.timestamp().defaultNow().readOnly(), // DB DEFAULT clause (epoch ms), no generate warning
  updatedAt: t.timestamp().optional(),
}

Use .defaultNow() instead of .default(() => new Date()) when you want the database to set the timestamp via a SQL DEFAULT clause. .default(fn) is runtime-only ($defaultFn) and produces a warning from nanoka generate. A .defaultNow() field is optional in inputSchema('create') (the DB fills the value). To prevent clients from setting the value at all, combine with .readOnly().

t.json()

Maps to a SQLite text({ mode: 'json' }) column. Two forms are available:

Type annotation only — no runtime validation beyond JSON parsing:

const fields = {
  metadata: t.json<{ tags: string[] }>(),
}

With Zod schema — runtime validation on every parse:

import { z } from 'zod'

const TagsSchema = z.object({ tags: z.array(z.string()) })

const fields = {
  metadata: t.json(TagsSchema),
}

Note: nanoka generate always emits $type<unknown>() in the generated Drizzle schema regardless of which form you use. If you need a typed Drizzle column, edit the generated file manually or add a cast after generation.

Modifiers

All field builders support the following chainable modifiers:

Modifier Effect
.primary() Marks the column as the primary key
.unique() Adds a unique constraint
.optional() Allows undefined / null; removes the NOT NULL constraint
.default(val) Sets a static default value (runtime only; no SQL DEFAULT clause)
.default(fn) Sets a dynamic default via $defaultFn (runtime only; nanoka generate warns)
.defaultNow() timestamp only: sets DB DEFAULT clause to current epoch-ms; no generate warning; optional in inputSchema('create') (DB fills the value) — combine with .readOnly() to block client writes entirely
.min(n) Minimum string length (string/uuid) or minimum value (number/integer)
.max(n) Maximum string length (string/uuid) or maximum value (number/integer)
.email() Adds email format validation (string only)
.serverOnly() Column exists in DB but is excluded from all API inputs and outputs
.writeOnly() Accepted as input but never included in responses
.readOnly() Included in responses but excluded from create/update inputs

Relations

Two relation field builders are available for defining associations between models:

import { t } from '@nanokajs/core'

// 1 → N: define on the parent model
t.hasMany(target, { foreignKey: 'userId' })

// N → 1: define on the child model
t.belongsTo(target, { foreignKey: 'userId' })

Key characteristics:

  • Relation fields have no DB columnnanoka generate skips them entirely.
  • They are excluded from validator(), inputSchema(), and outputSchema() by default.
  • The target can be a model reference or a thunk (() => Model) to handle bidirectional relations.
  • foreignKey is always required — Nanoka does not infer it.

Relation fields are defined in app.model() registration, not in model files (which should contain DB columns only).

See Relations for the full API including with query options and OpenAPI integration.