M2 Release Notes — v5.1.0
Theme: the schema is the source of truth.
M2 is the milestone where @forinda/kickjs-db stops asking adopters to maintain a parallel interface DB alongside their schema. The phantom-typed column builders, SchemaToKysely<S> distributive type, and KickDbRegister module augmentation make KickDbClient widen to the right row shape everywhere — controllers, repositories, modules — automatically. The CLI's typegen plugin contract emits the augmentation; adopters never write it by hand.
The same milestone added the day-to-day extension surfaces (customType, pgEnum, $extends({ model }), slowQueryThresholdMs), the kick CLI plugin contract that built-ins dogfood, and a refreshed DevTools panel with theme support, pagination, and per-group collapse.
Adopter-facing wins
Schema → types, no manual sync
// db/schema/users.ts
export const users = table('users', {
id: uuid().primaryKey().defaultRandom(),
email: varchar(255).notNull().unique(),
isActive: boolean().notNull().default('true'),
createdAt: timestamp().notNull().defaultNow(),
})
// db/client.ts
export const dbClient = createDbClient({ schema, dialect, events: true })
// ^^^^^^^^ KickDbClient<SchemaToKysely<typeof schema>>
// repository
@Service()
export class UsersRepository {
constructor(@Inject(DB_PRIMARY) private readonly db: KickDbClient) {}
// ^^^^^^^^^^^^
// bare KickDbClient widens via the auto-generated `KickDbRegister`
// augmentation — no hand-written register.ts, no `as Db` cast.
list() {
return this.db.selectFrom('users').selectAll().execute()
// ^^^^^^^ typechecked against the schema
}
}kick typegen (also auto-runs on kick dev) emits the augmentation into .kickjs/types/kick__db.d.ts. Disable per-plugin via typegen.disable: ['kick/db'] if you prefer hand-written augmentations.
Adopter-defined column types
const encrypted = customType<EncryptedString>({
dataType: () => 'text',
toDriver: (s) => encryptSync(s),
fromDriver: (raw) => decryptSync(String(raw)) as EncryptedString,
})
const secrets = table('secrets', {
id: serial().primaryKey(),
value: encrypted().notNull(),
})fromDriver fires automatically on select via the kick/db Kysely plugin. toDriver is stored on the builder; auto-application on insert lands in a follow-up. See DB Extensions for the full pattern.
PostgreSQL enums
export const taskStatus = pgEnum('task_status', 'todo', 'in_progress', 'done')
export const tasks = table('tasks', {
status: taskStatus().notNull().default('todo'),
})Phantom narrows the column to 'todo' | 'in_progress' | 'done'; the snapshot/diff/emit pipeline produces CREATE TYPE … AS ENUM (…) ahead of every dependent table. Adding values mid-list emits ALTER TYPE … ADD VALUE … BEFORE … so existing rows round-trip.
Per-table method extensions
const dbX = db.$extends({
model: {
users: {
async findByEmail(this: typeof dbX, email: string) {
return this.selectFrom('users').selectAll().where('email', '=', email).executeTakeFirst()
},
},
},
})
await dbX.users.findByEmail('a@b.com')Slow-query detection + lifecycle hooks
const db = createDbClient({
schema,
dialect,
events: true,
slowQueryThresholdMs: 100,
})
db.on('slowQuery', ({ sql, durationMs, thresholdMs }) => {
log.warn({ sql, durationMs, thresholdMs }, 'slow query detected')
})
db.on('queryError', ({ sql, error }) => {
log.error({ sql, err: error }, 'query failed')
})Wired through Kysely's log callback; zero-overhead path when events are off (no callback registered).
Self-referencing tables
import { type ColumnRef } from '@forinda/kickjs-db'
export const categories = table('categories', {
id: uuid().primaryKey().defaultRandom(),
parentId: uuid().references((): ColumnRef => categories.id, { onDelete: 'set_null' }),
})Lazy FK thunk + the ColumnRef annotation breaks the TS7022 inference cycle on the outer const.
CLI plugin contract
Every built-in kick command (init / generate / run / typegen / db / …) ships as a KickCliPlugin internally. Adopters extend the same surface from kick.config.ts:
export default defineConfig({
plugins: [drizzlePlugin({ schemaPath: 'src/db/schema' })],
})Plugins contribute commands, register (programmatic commander chains), typegens, and generators. The kick/db typegen plugin uses this contract to emit the KickDbRegister augmentation. kick typegen --list shows registered ids; typegen.disable opts a builtin out cleanly. kick typegen --check is the CI gate.
DevTools refresh
- Light + dark themes —
<html data-theme>flips palettes via CSS variable overrides. Toggle in the header (sun/moon icon) cyclessystem → light → dark; persists inlocalStorage. - Brand palette — gold (primary) + purple (secondary) tokens that deepen on light backgrounds for AA-clean contrast.
- Card polish — soft shadow + hover lift, larger metric values, tighter rhythm.
- Pagination — every long list (Container, Routes, Queues, Graph nodes, Topology DI tokens + Contributors) paginates at 5/page with 5-step size selector.
- Collapsible groups — Graph tab groups (controllers / services / etc.) collapse with persistence per-group.
- Reactive Container snapshots — Container now emits
'resolved'events on every resolve path; devtools subscribe via SSE so the panel reflects state changes without polling. @Autowireddependencies surface —Container.extractDependencies()was missing the property-injection branch, leaving classes that only use@Autowiredshowing empty deps. Now covered.- Application contributors with proper labels —
Application.getContributors()walks adapter / plugin / global registries with source-aware labels; topology tab shows the full set, not just adapter-attached entries. - DevtoolsRenderTab additive contract —
defineDevtoolsRenderTab({ id, name, render(el, props) })coexists with the legacy descriptor surface. Migration is per-tab; M2.D's KickEventBus completes the picture.
Real-world workload
examples/task-kickdb-api is now a 17-table port of task-prisma-api:
- 5 PG enums (
global_role,workspace_role,task_priority,channel_type,notification_type) - Self-referencing FK on
tasks.parentTaskId - Composite-key join tables (workspace_members, channel_members, task_assignees, task_labels)
- Default JSON values inline as PG cast expressions
- Multi-file barrel — 17 per-table modules + a single
relations.ts
kick db generate full-port produces an 86-change migration with CREATE TYPE ordered ahead of dependent tables and DROP TYPE ordered after dependent table drops on rollback.
Out of scope (deferred — superseded 2026-05-05)
Update: The audit on 2026-05-05 found this list stale. Most items shipped to disk after the release notes were cut, and the rest landed in M3. See
m3-release.mdfor the v5.3 follow-up.
Original deferred item Actual status $extends({ result })Shipped before the release notes were filed — packages/db/src/extend/result-plugin.tscarries the Kysely plugin.customTypetoDriverinsert pathShipped — packages/db/src/client/codec-plugin.ts:56transformQueryfor INSERT/UPDATE.db.query.X.findMany({ with })Shipped in M3.A — see m3-release.md. M2.D KickEventBus Shipped — packages/devtools-kit/src/bus/.M2.E Vite AST strip Shipped in M3.C — packages/vite/src/babel-strip-devtools.ts.Routes/env legacy generator carve-up Shipped — packages/cli/src/typegen/builtin/{routes,env,assets}.ts.Removed-enum-value handling Shipped in M3.B — -- KICK ENUM REMOVEheader +--confirm-enum-droprunner flag.
Migration notes
From v5.0 adopter projects using kick-db
Registerwas renamed toKickDbRegister. Hand-written augmentations:diff- declare module '@forinda/kickjs-db' { interface Register { db: typeof dbClient } } + declare module '@forinda/kickjs-db' { interface KickDbRegister { db: typeof dbClient } }Most adopters who follow the example app delete this file entirely once
kick typegenruns (the kick/db plugin emits the equivalent augmentation under.kickjs/types/).createDbClientinfers DB from schema by default. The previouscreateDbClient<TSchema, DB = unknown>collapsed toKickDbClient<unknown>unless adopters passed an explicit generic. NowDB = SchemaToKysely<TSchema>— the explicit generic is no longer needed:diff- export const dbClient = createDbClient<typeof schema, MyDb>({ schema, dialect }) + export const dbClient = createDbClient({ schema, dialect })kick.config.ts > db.schemaPathwith a barrel folder needs the explicit/index.tssuffix:diff- schemaPath: 'src/db/schema', + schemaPath: 'src/db/schema/index.ts',Required because Node's ESM loader doesn't auto-resolve directory imports under
--experimental-strip-types.tsconfig.jsonfor projects using a barrel schema folder needsallowImportingTsExtensions: trueandnoEmit: trueso cross-file imports inside the schema folder can use explicit.tsextensions (Node's loader requires them).
From v5.0 adopter projects using kickjs-cli
KickConfig.plugins?: KickCliPlugin[]is the new extension surface. Existingcommandsfield still works; plugin commands appear first, adoptercommandsoverrides plugin commands of the same name.package.json > kickjs.generatorsdiscovery is deprecated. Plugin authors should migrate toKickCliPlugin.generators[]shipped viakick.config.ts. The legacy discovery still runs as a fallback for one minor version; remove in v5.2.
From v5.0 adopter projects using devtools
- Theme toggle —
<html data-theme>is now set at runtime. Adopters who shipped custom DevTools tabs against raw Tailwind slate utilities should migrate to the semantic tokens (bg-app-bg,text-text-secondary,border-border, etc.) for light-mode support. Tabs using the.card/.tab/.empty@apply classes flip automatically.
Stats
- 50+ commits across packages/db, packages/cli, packages/devtools, packages/devtools-kit, packages/kickjs, examples/task-kickdb-api, and docs
- Net new: customType, pgEnum, $extends, KickCliPlugin, kickDbTypegen, kickAssetsTypegen, lifecycle hooks, light/dark theme, pagination, collapsible groups, full-port example
- Test counts (final): kickjs 317, db 199, cli 200, devtools 66