Roadmap & Proposals
This document captures ideas for how to evolve KickJS. Each proposal carries enough detail to deliberate on it without re-deriving the motivation every time. DB-related items are deferred — we're working through the non-DB tracks first.
Last audited 2026-09-30 against the source, not from memory. Statuses below reflect what actually ships; where a proposal was delivered by a different design than the one sketched, the status says so rather than quietly matching the text to the code.
How to read this document
Each proposal uses the same template:
- Why it matters — the problem; what's broken or missing today
- What it looks like — the rough shape of the solution
- Effort — order-of-magnitude estimate (days / weeks / months)
- Open questions — things we haven't decided yet
- Status — one of:
proposed— captured, not yet discussed in depthin design— actively being shapedaccepted— we're going to build it; awaiting a phase slotbuilding— work in flightshipped— donedeferred— agreed valuable, waiting on something elserejected— decided against; kept here so we don't relitigate
Proposals are grouped into tracks by intent: the moat (differentiation), DX wins (retention), bold bets (high-risk / high-reward), and lessons from Nitro, srvx and Nuxt (Track E — gaps found by reading their source).
Track A — The Moat
Things that make a developer say "I'm using KickJS specifically because nothing else does this." These are where we win — or don't — against Nest, Fastify, Hono, tRPC.
A.1 End-to-end typed client
Status: shipped — @forinda/kickjs-client (typed api.get/post), kick typegen emitting KickRoutes.Api, and createRpc(api, kickRpc) for the tRPC-style namespace. See the typed client guide. Effort: 2–3 months
Why it matters. tRPC's whole pitch is full-stack type safety without a separate schema. KickJS already has the inputs (decorator-introspectable controllers, Zod schemas, the kick typegen command), but no consumer-side SDK. A developer using a kickjs backend with a Next/Remix/Vite frontend has to either hand-write API clients or pull in a separate schema layer (OpenAPI generator, gRPC, etc.) — and pay the schema-drift tax.
What it looks like.
// In the frontend:
import { kickClient } from '@forinda/kickjs-client'
import type { AppRouter } from '@server/types' // generated by `kick typegen`
const api = kickClient<AppRouter>({ baseUrl: '/api' })
const user = await api.users.create({ email: 'a@b.com' })
// ^? fully inferred response shape from the controller return type
// ^? input validated against the Zod schema attached to the controllerkick typegen --client generates a .d.ts describing every controller method, its input schema, and its return type. The runtime SDK is a thin proxy that uses HTTP under the hood (so any non-TS client — mobile, Go, curl — still works against the same REST endpoints).
Why this is the moat. Every other framework lets you handwrite a client OR hand-write an OpenAPI generator config. KickJS would be the only framework where the same decorator that defines your route also defines your client — automatically.
Open questions.
- Should the client be a separate published package, or part of
@forinda/kickjs-clitypegen output? - How do we handle streaming responses (SSE, WebSocket)? Different transport in the same client?
- Versioning: when an adopter bumps kickjs, the client must regenerate. CI hook? Watch mode?
- React/Vue/Svelte query-hook wrappers (
useQuery,useMutation) — first-party or community?
A.2 First-class observability
Status: shipped as re-scoped — the bring-your-own seam it called for now exists: onError / onResponse hooks on adapters and plugins, reportError(), and diagnostics_channel channels (kickjs:handler, kickjs:error, kickjs:response) that OpenTelemetry and APM agents subscribe to. See E.3, E.4 and Observing Errors and Responses. The framework-owned bootstrap({ observability }) block sketched below is not planned. Original note: premise has moved. @forinda/kickjs-otel was deprecated to a BYO recipe rather than promoted, so the sketch below (a bootstrap({ observability }) block owned by the framework) now runs against the BYO direction. What did ship is the seam: Logger.setProvider(), processHooks so an observability SDK can own SIGTERM, and adapter introspect(). Re-scope before building. Effort: 1–2 months
Why it matters. The @forinda/kickjs-otel package exists but isn't the headline experience. Production-readiness is one of the top reasons engineering teams pick a framework — and "just install us, every controller is traced" is a story Nest doesn't have.
What it looks like.
bootstrap({
modules,
observability: {
tracer: new SentryTracerProvider({ dsn: env.SENTRY_DSN }),
metrics: new PrometheusMetricsProvider({ endpoint: '/metrics' }),
},
})Auto-instrument: every controller method, every adapter call (db, queue, cron, mailer), every guard, every contributor. Spans tagged with route, status code, latency, errors. The TracerProvider interface mirrors LoggerProvider from #265 — 5 methods, plug Sentry / Honeycomb / Datadog / OTel Collector with ~20 lines.
Why this is the moat. The pitch is "bootstrap({ observability }) and you have production-grade tracing." Every other framework requires adopter-written instrumentation.
Open questions.
- Where does the abstraction end? Do we own the W3C trace context propagation, or just the integration point?
- Sampling — built-in or delegated to the provider?
- Should metrics ship as a separate
MetricsProvideror be part ofTracerProvider? - Auto-instrumenting third-party libraries (Prisma, Drizzle queries) — opt-in or opt-out?
A.3 Runtime-portable core
Status: shipped — via a different shape than sketched below. There is no kickjs-core / -hono / -edge / -bun package split. Instead the engine is a runtime seam inside @forinda/kickjs (bootstrap({ runtime }) → Express / Fastify / h3), and @forinda/kickjs/web is a fetch(Request) → Response entry running on Cloudflare Workers, Bun and Deno, kept honest by a bundle-purity test that fails on any node-only import. The open questions below were answered as: Web Streams in the runtime seam, Fetch Request/Response at the web entry, nodejs_compat for AsyncLocalStorage on Workers. Effort: 2–4 months (gradual)
Why it matters. Every decorator framework is locked to Node + Express (Nest, KickJS today). Hono runs everywhere — Bun, Deno, Cloudflare Workers, Vercel Edge, Fastly — and that's a real audience. Being the only decorator-driven framework that runs on edge runtimes is a genuine wedge.
What it looks like.
@forinda/kickjs-core ← no node:* imports, runtime-agnostic
@forinda/kickjs ← express adapter on top of core (today's main package)
@forinda/kickjs-hono ← hono adapter (new)
@forinda/kickjs-edge ← Fetch API adapter for CF Workers / Vercel Edge (new)
@forinda/kickjs-bun ← Bun.serve adapter (new)The peer-dep cleanup we already did (pino removal, logger interface, multer optional) is step zero — every node-specific dep we move to an adapter is a step toward this.
Why this is the moat. Cloudflare Workers + KickJS = "decorator-driven framework at the edge with sub-100ms cold starts." Nobody has this.
Open questions.
- File system / native modules — how do we handle adapters that need fs (assets, view engines)? Optional opt-in?
- The DI container uses class metadata via
reflect-metadata. Does that work on Workers? (Yes, but bundle size matters.) - Streaming responses are different across runtimes (Node
ReadablevsReadableStream) — core API needs to converge on Web Streams. - HTTP adapter API: do we re-invent Express's
req/res, or expose Fetch APIRequest/Response?
Track B — DX Wins
Things that don't make headlines but determine whether adopters stick around.
B.1 Scaffolder feature-overlay model
Status: shipped — kick new renders every file it can from packages/cli/templates/ layers with slots (template, runtime, schema library, optional packages, host config, the fullstack server, root and web app); kick add wires those packages into an existing app; and a scaffold matrix (pnpm scaffold:matrix, daily in CI, one scenario per CLI pull request) scaffolds, installs this commit's packed packages, typechecks, builds and boots six scenarios. package.json files, the fullstack root's per-package-manager scripts and README, and the agent docs stay code. Effort: 3–6 weeks
Why it matters. kick new is built from TS functions that return file contents as strings (packages/cli/src/generators/, about 4.6k lines). Feature choices show up as conditionals inside those functions, and the combinations keep multiplying: template × runtime × schema library × repo × optional packages × package manager × frontend. A contributor can't see what a scaffolded project looks like without running kick new, adding a feature means editing several generators, and ws / queue are installed but never wired into src/index.ts. kick add only installs packages, and nothing in CI installs, typechecks or boots a kick new output.
What others do.
- create-vite — one complete, runnable directory per template, copied as-is (dotfiles stored as
_gitignoreso npm publish keeps them). Optional features are small patches after the copy. A release script bumps the template version ranges. - TanStack CLI — a base project plus add-on directories (
info.json, apackage.jsonfragment,assets/), withdependsOn, exclusive groups and env vars. Shared files expose typed integration slots (providers, vite plugins, devtools) that add-ons fill, so add-ons never edit shared files.addon an existing project re-renders it and overwrites changed files after a confirm. A small end-to-end matrix scaffolds, builds and boots selected combinations on a schedule. - TanStack Router's route generator — fills only empty files from token templates, which users can override, and never overwrites a file changed underneath it.
What it looks like.
packages/cli/templates/
base/ # the --yes defaults
template-rest/
runtime-{express,fastify,h3}/
schema-{zod,valibot,yup}/
repo-inmemory/
feature-{swagger,devtools,ws,queue}/
host-{netlify,vercel}/- Each directory has a
feature.json(id,dependsOn,exclusive,packageAdditions,integrations,envVars), an optionalpackage.jsonfragment, and files. Files are copied (_dot_becomes.,.appendfiles append), later layers override earlier ones, andpackage.jsonfragments are merged per key, with conflicts reported. - Slots in the shared files —
adapter,middleware,contributorandruntimeinsrc/index.ts,env-fieldinsrc/env.ts,vite-plugin, andconfiginkick.config.ts. A feature declares{ type: 'adapter', import, code }and the base file renders every entry. Base files never check which features were picked. - No template engine to start: copying, merging and slots cover most of it. The few real conditionals (naming, pluralization, per-package-manager scripts, agent docs) stay TS functions next to the engine.
kick ggenerators stay code. kick add <feature>applies the same directory to an existing app. It writes new files, mergespackage.jsonand.env*, records the feature inkick.config.ts, and inserts slot entries intosrc/index.tsthrough the AST rather than re-rendering it — entry files are edited by hand and must not be overwritten. When it can't find the spot, it prints the snippet instead of guessing. It refuses on a dirty git tree unless--forceis passed.- Versions live in the fragments and are bumped at release, so scaffolding needs no
npm viewround-trips (these become an opt-in refresh). - Tests — engine tests on an in-memory filesystem; parity snapshots against today's generator during the migration; and a matrix of about six combinations that scaffold, install against the workspace packages, typecheck, boot and hit
/health— daily and on demand, with one smoke combination on every PR.
Migration.
- Build the engine and port the
--yesdefaults tobase/, with output matching today's. - Move schemas, runtimes, swagger / devtools / ws / queue (now wired) and hosts into directories, deleting each matching template function as it moves.
- Rebuild
restandfullstackfrom those directories (web/keeps its create-vite option). kick addapplies feature directories.- Turn on the scaffold-and-boot matrix, then remove the dead template code.
Open questions.
- Slot markers as comments inside real, typecheckable
.tsbase files, or a separate slot list? - Per-feature options (e.g. a queue driver): a select prompt per feature, or wait until one needs it?
- Third-party features loaded from a URL, and
kick new --from <git url>starters — later, or never?
B.2 Error messages with "here's the fix"
Status: shipped (first pass, codes still being added) — KickError carries code / summary / cause / fix, and six KICK00x codes use it today (e.g. KICK005 missing controller-or-router, KICK006 duplicate route). Adding a code to a new failure is now the pattern rather than a project. Effort: 2–3 weeks per pass; ongoing
Why it matters. NestJS errors are infamous for being cryptic. KickJS errors today are functional but rarely tell the user how to fix the problem. The framework that has the best error messages — Rust's compiler, Elm, Astro — wins on first-impressions retention.
What it looks like.
Today:
Error: No provider for UserServiceProposed:
✖ No provider for UserService
UserService is decorated with @Service() but its module isn't
registered in bootstrap({ modules }).
Add it:
modules: [
UsersModule, ← add this
OtherModule,
…
]
Or, if UserService should be in a different module, decorate
the right module class with @Module({ providers: [UserService] }).
Docs: https://kickjs.app/guide/dependency-injection#registering-servicesA centralized error catalog (packages/kickjs/src/core/error-catalog.ts) keyed by error code, with structured fields: code, summary, cause, fix, docsUrl. Each throw new HttpException(...) / framework-internal error uses the catalog.
Why this is DX. First-impression metric. A developer who hits one cryptic error in an evaluation is gone; one who hits a friendly error stays.
Open questions.
- ANSI colors / box-drawing characters in error messages — assume TTY? Detect?
- Do we localize? (Almost certainly no — but worth recording the decision.)
- Integration with the devtools dashboard — surface the same hints there?
B.3 Interactive docs / WebContainers playground
Status: proposed — nothing in the docs site references WebContainers or an embedded playground yet. Effort: 2–4 weeks
Why it matters. VitePress + markdown is great, but everything is static. Vue's docs let you tweak code in the page and see it run. SvelteKit, Astro, Hono — they all do this now. For a backend framework, the friction-to-evaluation is even higher (you need a terminal, Node, a port…) — letting visitors try bootstrap({ modules: [HelloModule] }) in the docs without leaving the page would be a real adoption boost.
What it looks like. StackBlitz WebContainers (or CodeSandbox CDE) embedded in the docs. The minimal-api example becomes "click to launch in browser." Tutorials become interactive: edit the code, see the response.
Why this is DX. Evaluation friction goes to zero. A blog post linking to the docs becomes "click here to play with kickjs in your browser."
Open questions.
- WebContainers only run Node (no native modules). Excludes better-sqlite3 examples. Acceptable trade-off if we have a SQLite-via-WASM fallback?
- Hosting cost — StackBlitz is free for embeds; CodeSandbox CDE has limits.
- Authoring overhead — each interactive example needs a working project. Auto-generate from the examples archive?
B.4 kick doctor command
Status: shipped — kick doctor runs pre-flight checks and reports each with a fix (tsconfig decorator flags, env-file layering, and more). See packages/cli/src/commands/doctor.ts. Effort: 1 week
Why it matters. "It works on my machine" debugging eats hours. Common misconfigs (env not loaded, peer dep missing, typegen stale, decorators not enabled in tsconfig, prisma not generated) are all detectable.
What it looks like.
$ kick doctor
✔ Node version: 22.7.0
✔ pnpm version: 9.1.0
✔ reflect-metadata installed
✔ tsconfig has experimentalDecorators: true
✔ tsconfig has emitDecoratorMetadata: true
⚠ src/env.ts exists but isn't imported from src/index.ts
→ Without this, @Value() works via process.env fallback but ConfigService.get() returns undefined
→ Fix: add `import './env'` to the top of src/index.ts (above any bootstrap call)
✔ .kickjs/types/ is fresh (typegen ran 2 min ago)
✖ multer is used by upload route /api/upload but isn't installed
→ Fix: pnpm add multer
✔ Prisma client generated for prisma/schema.prisma
3 checks passed, 1 warning, 1 error.Why this is DX. Reduces "the framework doesn't work" support cost by ~70%. Most "doesn't work" reports are misconfigs that this command would catch.
Open questions.
- Plugin-extensible (every adapter contributes checks) or hardcoded list?
- Run-on-bootstrap variant (warn at app start, opt-out via config)?
B.5 RFC 9457 Problem Details on ctx
Status: shipped — ctx.problem(...) plus the typed convenience methods (ctx.problem.notFound(), .unauthorized(), …). The generated guard template uses them, and they are the runtime-portable way to answer from a guard or middleware. Effort: 1–2 weeks
Why it matters. Every API has the same error-shape bikeshedding conversation: should the JSON be { error: ... } or { message: ... } or { status, message } or some custom envelope? RFC 9457 — Problem Details for HTTP APIs (July 2024, supersedes RFC 7807) is the standard answer: a single canonical shape with five fields and a known content type. Adopting it gives kickjs a "standards-compliant by default" line without forcing anything — most Node frameworks make adopters reach for a library.
What it looks like.
New helper on RequestContext:
// Canonical RFC 9457 shape — type, status, title, detail, instance, plus
// arbitrary extensions
ctx.problem({
type: 'https://api.example.com/problems/out-of-credit',
status: 403,
title: 'You do not have enough credit',
detail: 'Your current balance is 30, but that costs 50.',
instance: `/account/${ctx.user.id}/messages/${ctx.params.id}`,
// extensions per §3.2
balance: 30,
})Plus typed convenience methods on the same namespace:
ctx.problem.notFound({ detail: 'User abc not found' })
ctx.problem.badRequest({ detail: '...', errors: [...] })
ctx.problem.unauthorized()
ctx.problem.forbidden()
ctx.problem.conflict({ detail: 'Email already in use' })
ctx.problem.validation(zodIssues) // serializes Zod errors into the §3.2 "errors" extensionEach method:
- Sets
Content-Type: application/problem+json - Pre-fills
statusandtitleper RFC 9457's recommendations - Lets the caller override or extend any field
HttpException gets optional type / instance / extensions fields. The framework error handler emits application/problem+json automatically when those fields are present:
throw new HttpException('Out of credit', HttpStatus.FORBIDDEN, {
type: 'https://api.example.com/problems/out-of-credit',
detail: 'Your balance is 30, but that costs 50.',
extensions: { balance: 30 },
})Optional sibling class ProblemException for adopters who want the problem fields to be required at the type level — type-safety for the new way without forcing it on existing code.
The design decision: coexistence + passive deprecation.
Only the error-shape helpers (ctx.notFound(), ctx.badRequest()) get a @deprecated JSDoc tag pointing at the ctx.problem.* equivalent — IDEs surface the strikethrough, nothing breaks at runtime, no behavior change for any existing endpoint. Adopters migrate per call site when they next touch the file.
ctx.json(data, status?) is not deprecated — it's the generic response helper for success and any non-standard shape, and stays the canonical way to send arbitrary JSON. Same for ctx.created(), ctx.noContent(), ctx.html(), ctx.download(), ctx.render(). The deprecation is scoped narrowly to the two helpers whose entire purpose is "emit an error in our custom shape" — those are the ones RFC 9457 directly replaces.
Explicitly NOT in scope:
- No
bootstrap()config knob. Every new opt-in feature adding abootstrap({ ... })config bloats the surface area for what's ultimately a metadata-format change. Adopters opt in per call site by reaching for the new helpers. - No
kick.config.tsconfig knob either. Same reasoning. - No forced migration. The framework error handler infers behavior from the data (problem fields present → problem+json; absent → existing JSON shape). Backward compatible by detection, not by config.
ctx.jsonis not touched. It's the generic JSON response helper for success and arbitrary shapes — orthogonal to the error-format question RFC 9457 answers.
Why this is DX. Standards alignment costs adopters nothing (the old way keeps working) and gives them a real "we follow RFC 9457" line for their API docs. The framework gets uniform error shapes across the ecosystem; the typed-client work in A.1 gets a standard error type to generate against.
Open questions.
- Default
typevalue when none is provided —about:blankper RFC 9457 §4.2.1, or a kickjs-specific URI scheme likehttps://kickjs.app/problems/{status}? - How does this interact with the
validate()middleware? It currently throws a 400 with a custom shape — auto-upgrade to problem+json with theerrors[]extension, or keep current shape and add an opt-in flag? instanceis per-occurrence URI — should the framework auto-populate it withreq.url, or always require the caller to set it explicitly?- Localization of
title/detail— out of scope for v1 (RFC 9457 §6 acknowledges i18n is the application's responsibility), but worth deciding now.
B.6 Route flags — one vocabulary for per-route policy
Status: shipped — all four phases. defineRouteFlag + ctx.route + contributor skipWhen / onlyWhen; exemptWhen on csrfGuard() / rateLimitGuard(); the pre-match rateLimit() policy table (per-route @RateLimit({ rpm })); and the readers: /_debug shows resolved flags per route, and swagger's securityResolver can derive security from flags. Module-level flags and AI/MCP tool selection followed. See the route flags guide. Effort: 1–2 weeks for phase 1; phases 2–4 independently schedulable
Why it matters. "Whitelist these endpoints" is a request every API eventually makes, and today the answer depends on which subsystem is asking. Auth uses a contributor (@Public = LoadAuthUser({ on401: 'allow' })). CSRF uses ignorePaths. Rate limiting uses skipPaths / skip. One fact — this endpoint is open — stated three ways, in two places, under two notions of identity.
The path-string half has real defects: both are an exact Set.has(pathname), so /users/:id cannot be expressed; and the list keeps parsing after an apiPrefix or version change that silently voids it.
The contributor half looks better but doesn't compose. @Public wins by being a second instance of the same contributor at higher precedence — which only works if you own the key. You cannot exempt a plugin's contributor, and you cannot exempt anything that isn't a contributor at all.
What it looks like. A flag is a named, inheritable fact about a route, resolved through the existing five registration sites (method > class > module > adapter > global) and readable by every consumer:
export const Public = defineRouteFlag('auth.public')
@Public // class level — propagates to every route below
@Controller()
class WebhooksController {
@Get('/health') health(ctx: RequestContext) {} // inherits
@Public(false) // method wins
@Post('/admin')
admin(ctx: RequestContext) {}
}Consumers read them wherever they already hold a RequestContext:
ctx.route.flags.has('auth.public') // guards, @Middleware(), handlers
defineHttpContextDecorator({ skipWhen: 'auth.public', … }) // contributors
csrf({ exemptWhen: 'csrf.exempt' }) // phase 2That split is what keeps the cost down: everything after route matching gets flags from one addition (ctx.route), with no per-consumer API. Only the two pre-match middleware need further mechanism.
Phasing: 1) primitive + ctx.route + contributor skipWhen/onlyWhen; 2) CSRF; 3) rate limiting (also unlocks per-route limits, which the current shape cannot express); 4) OpenAPI security schemes + DevTools display. Phase 1 stands alone.
Open questions.
- Do flags carry values, or is presence enough?
@RateLimit({ rpm: 10 })wants values, and values turn precedence into a merge question rather than pick-one. skipWhenas a flag name only, or also a predicate? A predicate that reads anything but flags reintroduces the coupling this removes.- Pre-match consumers: per-route mounting (exact, but a 404 then skips the check) or a boot-compiled policy table (preserves ordering, but is a second implementation of routing)? Current thinking: per-route for CSRF, table for rate limiting — a token check is meaningless on an unmatched route, an abuse control is not.
Risk. kickjs-auth was deprecated for baking in an auth opinion. Mitigation: core ships defineRouteFlag and names no flags. auth.public is a string the adopter picks; nothing in core branches on it.
Track C — Bold Bets
High-risk, high-reward. Each of these could be the thing KickJS is famous for, if it works.
C.1 Schema-first via one decorator
Status: shipped as re-scoped, in two parts.
- A table as the request schema —
insertSchema/selectSchema/updateSchemafrom@forinda/kickjs-db/schema(Validation from Tables). - Class and fluent ways to declare a table — Table Forms. The same schema (an enum, a self-referencing table, two tables referencing each other, foreign keys, an index, relations) was written in each candidate and checked for identical snapshots, a typed query client, exact row types, rejected mismatched foreign keys, and whether self-references and cycles compile without an annotation:
| Candidate | Result | Why |
|---|---|---|
Object — table('users', { ... }) | Kept (the default) | Gained selfRef('id'), typed column refs (so fk() checks it), and link() for cycles — removing the (): ColumnRef => annotation it used to need. |
Builder fields — class Users { static tableName = 'users'; email = varchar() } + tableFromClass | Shipped | The fields are the builders, so every type survives, and it is the only form where self-references and cycles need no annotation or helper at all. |
Base class — class User extends TableBase('users', { ... }) {} | Shipped | Types infer as with table(), and the class is the row type — rows can carry methods (User.from(row)). Exporting the class is enough for kick db generate. |
Fluent — defineTable('users').column(...).build() | Shipped | The most checked: a self-reference names an earlier column with the right type, and a column declared twice is a type error. |
Decorators — @Table('users') class { @Column(varchar()) email!: string } | Dropped | A decorator can't pass its builder's type to the class: row types become unknown, the table name string, and the typed client, row types and foreign-key checks are all lost. Its one advantage — checking a property's type against its column — isn't needed when the field is the column. |
Also named @Table-style, not @Schema: in kick/db a schema is a Postgres namespace (pgSchema('billing'), as in Drizzle) or a request validator. The original sketch below is kept for context. Effort: 4–6 months
Why it matters. Today an entity is defined four times: Prisma/Drizzle schema, Zod validator, TypeScript type, OpenAPI doc. Each has its own syntax. Drift is constant. A single source of truth — declared once, projected to all four — is the holy grail.
What it looks like.
@Schema({ table: 'users' })
class User {
@Field({ primary: true, default: 'cuid' })
id!: string
@Field({ unique: true, email: true })
email!: string
@Field({ minLength: 8 })
password!: string
@Field({ nullable: true })
bio?: string
@Relation({ many: 'posts' })
posts!: Post[]
}From this one class:
- A Prisma / Drizzle / kickjs-db migration is generated
- A Zod validator is generated for incoming payloads
- TypeScript types are inferred natively
- OpenAPI schema is emitted for swagger
Why this is bold. Prisma + tRPC + Zod-OpenAPI compressed into a single declaration. Nobody has this. Done right, it's a moat the size of an ocean.
Open questions.
- Reconciling decorator metadata with actual ORM types — do we generate runtime objects or just types?
- How do we surface ORM features that don't have schema-first analogs (raw SQL, dialect-specific types)?
- Migration story — if the user adds
@Field({ unique: true }), does that auto-generate a migration? - Does this replace
@forinda/kickjs-db, or is it built on top of it?
C.2 TS compiler plugin for runtime types
Status: not planned as a compiler plugin. kick typegen covers the types-at-runtime need by scanning source and emitting types the user's tsc checks, and uses the TypeScript checker where it has to (the client route map). Injecting interfaces without tokens needs whole-program knowledge a per-file transform doesn't have, and the build-tool gaps (esbuild, tsx) are handled by the SWC build and explicit @Inject. The pain that remains is smaller — a class that is never imported never registers — and belongs to typegen. Typegen now warns about decorated classes no module glob loads, and kick typegen --fix adds the missing patterns (see Type Generation), which covers it without a separate registration manifest. The original sketch below is kept for context. Effort: 3–6 months
Why it matters. TypeScript's types are erased at runtime. That's why we need decorators in the first place — to recover the type info at runtime. A ts-patch / swc plugin (like ts-runtime-checks, typia, or tspl) preserves the type info, making decorators in some cases unnecessary.
What it looks like.
Today:
@Service()
class UserService {
@Autowired() private repo!: UserRepo
constructor(@Inject(LOGGER) private log: Logger) {}
}With the plugin:
class UserService {
// Type info preserved at runtime; framework infers it's @Service-able
// and that repo + log should be injected based on their declared types.
private repo: UserRepo
constructor(private log: Logger) {}
}Why this is bold. Decorator fatigue is real. A framework that automates away the decoration ceremony — while still being a decorator framework when you want to be explicit — would feel like a generational leap.
Open questions.
- Compile-time complexity. Adopters need a plugin in their
tsconfig.json/swc.config. Acceptable friction? - Build tool compatibility — does it work with Vite, esbuild, tsc, swc, Bun?
- IDE integration — do
cmd-clickand "go to definition" still work? - Source maps / debug experience?
C.3 Multi-tenant as a config flag
Status: rejected — superseded by BYO. @forinda/kickjs-multi-tenant was deprecated to a recipe built on defineContextDecorator, which is the opposite direction to a framework-owned config flag: tenancy models vary too much per project to bless one. Kept here so the idea is not relitigated. Effort: 2–3 months
Why it matters. The multi-tenant examples in the archive exist (multi-tenant-drizzle-api, multi-tenant-prisma-api, multi-tenant-mongoose-api) but they're separate templates with significant boilerplate. SaaS teams pick frameworks partly on how easy multi-tenancy is. Making it bootstrap({ multiTenant: ... }) instead of a project rewrite is a real differentiator.
What it looks like.
bootstrap({
modules,
multiTenant: {
resolver: (req) => req.headers['x-tenant-id'] as string,
scope: 'database', // 'database' | 'schema' | 'row'
audit: true,
},
})The framework wires per-request tenant resolution, scopes the DI container, switches DB connection / schema / WHERE clause automatically, and (with audit:true) logs every cross-tenant access attempt.
Why this is bold. SaaS startups flock to whatever framework makes multi-tenancy painless. Today that's nothing — Nest has nothing built-in, Prisma has Multi-schema but with caveats, Drizzle leaves it to you. KickJS could own this.
Open questions.
- How does this interact with the typed-client (#A.1)? Tenant ID baked into the client config?
- Per-tenant connection pooling — do we own that, or delegate to the ORM adapter?
- Migration story for adopters who already have a multi-tenant app — gradual adoption path?
Track E — Lessons from Nitro, srvx and Nuxt
Captured 2026-09-30 from a source-level comparison with Nitro v3 (3.0.260903-beta), srvx 1.0.5 (Nitro's server layer) and Nuxt 5 + Nuxt DevTools 4.0.0-beta.2. Every claim was read in their code and checked against ours; where we already match or beat them, it is listed at the end so it isn't chased. Each item is proposed until picked up.
E.1 API runner in the DevTools dashboard
Status: shipped (MVP) — Try on the Routes tab opens a side sheet with collapsible path-param / query / header / body sections (raw or multipart form data with file pickers — @FileUpload routes open ready to upload), an environment of default headers and (session-only, or remembered on the browser) with Save to variable from a response, a rendered curl / fetch snippet, and the response; CSRF auto-fill, a configurable list of public flags that skip the default Authorization, and a second click for DELETE / PUT / PATCH. See DevTools → API runner. Phase 2 has shipped too: OpenAPI prefill and hints, the last 30 requests as history, and open handler in editor from the dashboard (an editor URL) and the VS Code routes view. Phase 3 remains proposed. Effort: MVP ~1 week; phases 2–3 independently schedulable
What they do. Nuxt DevTools' Server Routes tab is a Postman-style runner: pick a route, fill path params / query / headers / JSON body (typed key-value rows), send, and read status, timing, content-type and a formatted body (JSON, HTML, image/video/PDF previews). "Default inputs" merge into every request; there are useFetch / $fetch snippets and open handler in editor. Requests go from the browser to the same origin — no proxy. Per-route inputs live in localStorage; the global defaults (which can hold tokens) are written to a JSON file under ~/.nuxt/devtools/.
What we have. /_debug/routes already returns fully resolved paths (API prefix + version applied) and route flags; the Routes tab is read-only. The Swagger adapter's /openapi.json has param/query/body shapes when mounted. Typegen output is .d.ts only — not readable in a browser.
What it looks like.
- MVP (no new server surface): split pane in the Routes tab — param inputs, query/header rows, raw JSON body, send; response status / time / headers / pretty body; copy as
curl/fetch; per-route inputs and globals saved. KickJS specifics: use the listed path as-is (prefix and version are applied), auto-fillx-csrf-tokenfrom the_csrfcookie for unsafe methods, skip a globalAuthorizationon routes carrying a configurable "public" flag (defaultauth.public), never forward the devtools token to app routes. - Phase 2: prefill and hints from
/openapi.jsonwhen present; last-N history; open handler in editor (the typegen scanner already records each controller's file) plus the VS Code command. - Phase 3: typed-client (
@forinda/kickjs-client) snippets; export.httpfiles or Postman collections; link a response to its request-id trace.
Open questions. Confirm before DELETE / PUT / PATCH? Default auth values to sessionStorage (Nuxt persists them in plain text — we shouldn't)? Routes mounted through a hand-built router carry no metadata and can't be listed — say so in the UI.
E.2 waitUntil for work that outlives the response
Status: shipped — ctx.waitUntil(p) and a standalone waitUntil(). The Node server's shutdown() awaits the work (within shutdownTimeout); createHandler().fetch/node and the web entry take the platform context as a last argument and hand the work to its waitUntil; createFetchHandler forwards the Workers ctx; kick build:netlify / build:vercel generate entries that pass it. See Serverless. Effort: 2–3 days
What they do. srvx gives every request waitUntil(promise), and close() awaits pending work; Nitro passes it to tasks and cache revalidation, and forwards the platform's (Workers, Vercel).
What we have. Nothing. createFetchHandler (web.ts) takes (request, env) and drops the Workers execution context; createHandler().fetch takes only a request; shutdown() drains in-flight requests but not detached promises. Fire-and-forget work (audit logs, emails, analytics) is killed on serverless and lost on SIGTERM.
What it looks like. ctx.waitUntil(p): on Node, tracked and awaited during shutdown (inside shutdownTimeout); on the web entry and serverless handlers, forwarded to the platform's waitUntil, with the Workers ctx accepted by createFetchHandler.
E.3 One error funnel, and request / response observer hooks
Status: shipped — onError / onResponse on adapters and plugins, fed by reportError() / reportResponse() from request errors (every runtime), uncaught exceptions, unhandled rejections, failed waitUntil work and failed queue jobs. See Observing Errors and Responses. Effort: 3–5 days
What they do. Nitro has runtime hooks request, response, error, close, and one captureError funnel that request errors, cache errors and unhandled rejections all go through.
What we have. Adapter and plugin hooks cover boot and shutdown only. bootstrap({ onError })replaces the error handler rather than observing it; cron, queue, contributor and uncaught errors each take their own path. Wiring Sentry means re-implementing the default handler.
What it looks like. Observer hooks on adapters/plugins — onError(err, { ctx?, source }) and onResponse(ctx, status, ms) — engine-neutral, observe-only, fed from every error source.
E.4 diagnostics_channel tracing
Status: shipped — kickjs:handler (a tracing channel around each controller handler, named by the full route pattern), kickjs:error and kickjs:response, all free without subscribers. Node server only; the web entry doesn't publish them. Middleware and contributor channels are not published. Effort: 2–3 days
What they do. srvx wraps the request and each middleware in tracingChannel (srvx.request, srvx.middleware); Nitro turns those into spans named by route template.
What we have. W3C traceparent propagation (trace-context.ts) and request logging; no channels. OpenTelemetry is a bring-your-own recipe.
What it looks like. Publish kickjs.request (with the matched route template, e.g. /users/:id), kickjs.middleware and kickjs.contributor. Zero cost when nobody subscribes; dd-trace, OpenTelemetry and DevTools can subscribe without coupling. This may be the re-scoped form of A.2 that fits the bring-your-own direction.
E.5 @Cron that survives serverless deploys
Status: shipped — @Cron gains name, overlap, enabled and meta, and handlers get a run context. On Node, the opt-in KickCronAdapter() schedules jobs (one worker per cluster). kick build:vercel writes Vercel crons that call a CRON_SECRET-guarded trigger, the web entry exports scheduled() for Workers, and kick build:netlify warns. See Scheduled Tasks. Effort: 3–5 days
What they do. Nitro runs scheduled tasks itself (croner, with per-task dedup of overlapping runs), and presets translate schedules into platform config: Vercel crons, the Workers scheduled handler.
What we have. @Cron records metadata; the runner is bring-your-own. kick build:vercel emits no crons, and the web entry has no scheduled export — jobs silently don't run.
What it looks like. At minimum, kick build:vercel emits crons for @Cron jobs and warns when a serverless build has jobs it can't schedule; later, a scheduled export on the web entry.
E.6 Response caching with stale-while-revalidate and ETag / 304
Status: proposed — after Q.8Effort: 3–5 days
What they do. defineCachedHandler and the cache route rule cache whole responses, with swr / staleMaxAge / varies, conditional-request handling, and revalidation in the background via waitUntil.
What we have. Method-level @Cacheable only (fixed in Q.8); no ETag / If-None-Match for dynamic responses.
What it looks like. @CacheResponse({ maxAge, swr, varies }) on CacheProvider, answering 304 on a matching ETag; swr needs E.2.
E.7 Compressed static assets
Status: proposedEffort: 1–2 days
What they do. srvx serves precompressed .br / .gz siblings and compresses on the fly (1 KB–10 MB) with Vary; Nitro adds zstd.
What we have. serveStatic → serve-static (ETag, ranges, Last-Modified), and SpaAdapter sets immutable caching — but nothing is compressed, so fullstack apps ship uncompressed JS unless a CDN sits in front.
E.8 Smaller items
| # | What they do | KickJS today | Effort |
|---|---|---|---|
| E.8.1 | Per-path route rules — headers, cors, redirect, proxy, cache on a pattern (/api/**) | Decorators and global middleware; no proxy helper. Lower value for a decorator-first framework | M |
| E.8.2 | mTLS: request.tls.{peerCertificate, authorized}, and 496 when no client certificate is sent | server.tls already passes requestCert / ca through; no ctx.tls and no guard | S |
| E.8.3 | Accept a file path for tls.cert / tls.key (srvx reads the file) | Callers readFileSync themselves | S |
| E.8.4 | Request body size limit — srvx/body-limit rejects early on content-length and counts streamed bytes (so length-less HTTP/2 bodies are covered), 413 | Express's JSON parser caps at 100 KB by default; the h3 runtime has no limit at all. Should apply to every runtime, as problem details | S–M |
| E.8.5 | Source-mapped, colourised dev error output (Youch) | Dev JSON with raw stack lines; framework errors already carry fix hints | S, low |
Reports for upstream
- h3 v1 —
readRawBodyreturnsundefinedfor an HTTP/2 body withoutcontent-length(src/utils/body.tson thev1branch). Worked around in our h3 runtime; Nuxt 5's h3-v1 compatibility layer re-implementsreadRawBodywithout the header check. - srvx — shutdown never closes open HTTP/2 sessions (
close()callscloseAllConnections, which HTTP/2 servers don't have), so on Node 22 and earlier a client keeps sending requests through shutdown. We track and close sessions ourselves.
Where we already match or lead (don't chase)
- Graceful shutdown with request draining and
/healthanswering503while draining. - Dev HMR: selective module invalidation, typegen on save, disposables cleaned up on swap.
- Errors: RFC 9457 problem details plus the
KICK0xxcatalogue with fix hints. - Edge-safe KV stores (
KvRateLimitStore,KvSessionStore); a full storage layer is YAGNI for now. - Request logging and W3C trace propagation.
E.9 Background jobs, bring your own runner
Status: shipped — @Job / @Process in @forinda/kickjs with listJobHandlers / listJobQueues / runJob and a JOB_DISPATCHER token; QueueAdapter runs every job through runJob and honours provider; the web entry's queue() consumes Cloudflare Queues. See Background Jobs. Effort: 1–2 weeks
What shipped. The same shape @Cron took in E.5:
@Job(queue)/@Process(name)live in@forinda/kickjsand only record metadata.listJobHandlers(container)/listJobQueues(container)return them for any runner, andrunJob(container, queue, job)runs one — failures go to the error observers (source: 'job') and are rethrown so the tool's retries still apply; a job nothing handles fails withNoJobHandlerError.- A
JOB_DISPATCHERtoken withdispatch(queue, name, data, options), so application code enqueues without naming the tool. QueueAdaptertakes Redis options (BullMQ) or aprovider(RabbitMQ, Kafka, Redis pub/sub, or your ownQueueProvider) and runs every job throughrunJob. Any other tool is a few lines overlistJobQueues+runJob— the guide has a pg-boss example.- The web entry's
queue()consumes Cloudflare Queues, next toscheduled(). - The DevTools Queues tab browses and manages jobs through a
JobInspectoran adapter exposes fromjobInspector();QueueAdapterprovides one for BullMQ.
Existing @Job / @Process code kept working.
Quick wins
Small enough to bundle into other work or do in a half-day. Listed for visibility.
| # | Idea | Effort | Status |
|---|---|---|---|
| Q.1 | @Flag('feature-name') decorator + ConfigService integration | 2 days | rejected — covered by B.6 (defineRouteFlag) |
| Q.2 | kick db:seed first-class command | 3 days | proposed (DB) |
| Q.3 | HTTP/2 + HTTP/3 support in the default adapter | 1 week | shipped for HTTP/2 — server: { tls, http2 } on Fastify / h3 (HTTPS and HTTP/2); Express cannot (KICK007). HTTP/3 deferred until Node ships stable QUIC |
| Q.4 | kick new --with swagger,db,docker preset bundles | 3 days | shipped in part — kick new --packages a,b and kick add --list; no docker or named presets |
| Q.5 | kick test:e2e wrapper around supertest + test-app | 1 week | proposed — createTestApp + supertest covers it without a command; confirm the wrapper still earns its keep |
| Q.6 | First-party SentryLoggerProvider example snippet in docs | 1 day | shipped — Sentry integration |
| Q.7 | kick info — print resolved versions, peer deps, runtime info | 1 day | shipped — kick info |
| Q.8 | @Cacheable keys without the class, and cache stampedes | 1 day | shipped — default key is {method}:{ClassName}:{args} (the method stays first, so @CacheEvict(method) still matches); concurrent misses share one call. Found in the Track E comparison |
| Q.9 | DevTools token cookie sent to every app route | ½ day | shipped — the dashboard's token cookie is scoped to the devtools base path; the old path=/ cookie is removed on next load |
What we're NOT pursuing
These came up but we decided against, with reasoning preserved so we don't relitigate.
Microservices module
rejected
NestJS has one, almost nobody uses it. Complicates the framework's identity. Adopters who need microservices reach for gRPC, NATS, RabbitMQ, or temporal — not a framework-specific abstraction. If we want a story here, it's a dedicated adapter package, not a core feature.
GraphQL as a first-class peer to REST
rejected
REST is the moat. GraphQL is a feature people sometimes want, and @forinda/kickjs-graphql exists as a BYO adapter — that's the right call. Investing in GraphQL parity with REST dilutes focus. If a developer wants GraphQL-first, there are better frameworks (Pothos, GraphQL Yoga).
Community plugin registry / curated marketplace
rejected
Registries die without a maintainer. Lean on npm's existing naming convention (@kickjs-community/*, kickjs-plugin-*) and a docs page listing known-good plugins. Cheaper to maintain, no governance overhead.
Track D — Database
kick/db already does code-first tables in four forms, snapshot → diff → migrations for Postgres / MySQL / SQLite, a typed Kysely client, nested reads without JOIN row explosion (json_agg subqueries), and request validators projected from tables. D.1–D.15 come from comparing it with a long-lived ORM (Sequelize) for the resilience a production app relies on; D.16 onward from comparing it with Knex and Drizzle. Each item was checked against kick/db's source before it was listed.
| # | Idea | Status |
|---|---|---|
| D.1 | First-class migrations CLI (kick db:migrate dev/deploy/rollback) | shipped in part — kickjs-db generate + migrate latest/up/down/rollback |
| D.2 | kick db seed — seed files run in name order, not tracked, so they're re-runnable | shipped — see CLI → seed |
| D.3 | Auto-generated repository methods (C.1 follow-on) | deferred |
| D.4 | DB connection pool observability (auto-instrumented) | deferred |
| D.5 | Typed database errors — unique, foreign-key, check, not-null, serialization, deadlock and connection errors as classes, with the constraint and columns parsed; driver errors no longer leak raw. Unlocks D.7, D.11 and automatic 409 responses | shipped — see Queries → Errors |
| D.6 | Transactions that follow the call chain — AsyncLocalStorage so code inside db.transaction(async () => …) joins it without passing trx; afterCommit hooks; nesting modes (reuse / savepoint / separate) | shipped — see Queries |
| D.7 | Retry a whole transaction on serialization failure or deadlock (40001, 40P01, MySQL 1213, SQLITE_BUSY) with backoff | shipped — see Queries |
| D.8 | Primary-key and CHECK changes in migrations — a primary-key change is detected only as a column change, so Postgres emits no primary-key operation and MySQL emits a MODIFY COLUMN that neither adds nor drops the key. Add primary-key and CHECK change kinds; SQLite rebuilds the table and verifies with foreign_key_check. Also an explicit composite key: primaryKey('name').on(t.a, t.b) | shipped — see Migrations |
| D.9 | Richer indexes — partial (where), expression indexes, using gin / gist / hnsw, operator classes, concurrently, include. Needed before vector() and tsvector() columns can be indexed from the schema | shipped — see Partial, expression and other indexes |
| D.10 | Auto-managed columns — updatedAt that updates itself, a version() column for optimistic locking, soft delete honoured by relational reads | shipped — see Columns kick/db maintains |
| D.11 | upsert and a race-safe findOrCreate — conflict target and partial-index where; re-read on a unique violation | shipped — see Queries |
| D.12 | Read-replica routing — reads outside a transaction go to the replica, with an override | shipped — see Read replicas |
| D.13 | Many-to-many relations — many(target, { through: junction }) in relational reads | shipped — see Many-to-many |
| D.14 | Database documentation on par with established ORMs — get-started per dialect, a concepts page, querying split into reading / writing / raw SQL / recipes, migration workflows, testing, errors, multi-tenancy, pooling and performance, troubleshooting, coming-from guides; see D.14 | done |
| D.15 | Test helpers — a throwaway database per test file and a transaction per test rolled back afterwards, for SQLite in memory and Postgres | shipped — see Testing with kick/db |
| D.16 | findManyAndCount — one page and the total where matches, as the { data, total } that ctx.paginate takes | shipped — see Relational Queries |
| D.17 | Rename prompts in generate — a rename is detected only when one column is dropped and one added with identical attributes; anything else, and every table rename, becomes a drop and an add, losing the data. Ask which drops are renames, as drizzle-kit does, with a flag for CI | shipped — see Renames |
| D.18 | Migrations written in TypeScript — an up.ts / down.ts that gets the client, for data backfills that need code, next to the SQL migrations | shipped — see Migrations written in TypeScript |
| D.19 | Pick fields in relational reads — columns to include or exclude, and extras for computed fields, at every level of db.query | shipped — see Choosing fields |
| D.20 | Generated and identity columns — generatedAlwaysAs(sql) (stored or virtual) and GENERATED … AS IDENTITY, through snapshot, diff, emit and introspect | shipped — see Generated columns |
| D.21 | Column names that differ from keys — a casing: 'snake_case' option and a per-column name, honoured by migrations, db.query, validators and introspection (today Kysely's CamelCasePlugin only covers the query builder) | shipped as casing: 'snake_case'; a per-column name override is still open — see Column names |
| D.22 | Migration and transaction options — read-only transactions; migrate up/down --to <name> and rollback --all; a configurable migrations table and schema; several migration folders | shipped — see Batches and rollback and Transactions |
| D.23 | Edge and serverless drivers — document running on any Kysely dialect and add an explicit dialect override (an unknown one falls back to 'sqlite' today); then tested support for Neon, D1, libsql/Turso and bun:sqlite | shipped — dialectTag; libsql/Turso, D1 and bun:sqlite tested; Neon and PlanetScale documented — see Drivers |
| D.24 | More column options — JS-side defaults ($defaultFn, $onUpdate); a mode for bigint / numeric; table and column comments; Postgres geometry, halfvec, point, macaddr; MySQL and SQLite column subpaths (mysqlEnum, unsigned integers, datetime) | shipped for Postgres and MySQL; SQLite needs no extra types (dates and booleans are already mapped) — see Tables and Columns |
| D.25 | Views and materialized views — declared in the schema, diffed and migrated, readable through the typed client, with refresh | shipped — see Views |
| D.26 | Row-level security in the schema — policies and roles declared next to the table and migrated, instead of hand-written SQL | shipped — see Row-Level Security |
| D.27 | Migrations from somewhere other than the filesystem — a migration source for bundled and serverless deploys | proposed |
| D.28 | kick db push for prototyping — sync the schema with no migration file, refused outside development | proposed |
| D.29 | Generated seed data — deterministic fake rows that follow the schema and its relations, with per-column overrides | proposed |
Not planned, after the same comparison: per-row lifecycle hooks (an extra query and rows in memory per bulk statement), app-side validators separate from the schema, scopes and virtual attributes — plugins, schema-projected validators and Kysely expressions already cover them.
Considered and left low priority after the Knex and Drizzle comparison: batchInsert in chunks, a per-query timeout, MySQL column placement (after), seed scaffolding, prepared statements, db.batch, sequences, a query cache, more dialects (SQL Server, Oracle, CockroachDB), and native Zod or Valibot schemas — kick/db's validators already implement Standard Schema, which those libraries accept.
D.14 Database documentation
Status: done — all five phases.
Why. Compared against Drizzle, Prisma, Sequelize, TypeORM, MikroORM and Kysely, kick/db's features are ahead of its documentation. Topics peers give their own page sit inside long pages (queries.md holds the builder, relational reads, transactions, errors, events and extensions), the API reference predates typed errors, check() and the transaction options, and testing, multi-tenancy and observability guides don't mention the database. A few topics are thinly covered even by established ORMs — testing with a database, an error reference, row-level security, CTEs and set operations — which makes them the cheapest places to stand out.
Target structure (Guide → Database):
- Get started — a start-here page (new project / existing database / coming from another ORM), then Postgres, MySQL, SQLite, existing database.
- Concepts — how schema, snapshot, diff, migrations and the typed client fit together.
- Schema — tables and columns, relations, indexes and constraints, Postgres types and enums, custom types, table forms, type registration.
- Querying — reading, writing (incl. upsert), relational queries, raw SQL, recipes.
- Transactions and errors.
- Migrations — how they work, generating, reviewing, applying, rollback and recovery, CI and deploy, troubleshooting, one page per CLI command.
- Integrations — validation, repositories and DI, extensions, testing, multi-tenancy and RLS, observability and DevTools.
- Operations — connections and pooling, performance.
- Reference — API, error reference, coming from Prisma / Drizzle / TypeORM / Sequelize, FAQ.
Conventions: per-dialect code tabs, copyable one-problem recipes, every error linked to a fix.
Phases:
- [x] Clean-up — refresh the API reference (typed errors,
check(),primaryKey().on(), table forms,/schemahelpers,afterCommit/retry/nested,$extends,escapeLike, SQLite / MySQL introspection and emit); retire the design notes underdocs/db/, which describe features that never shipped. - [x] Stand-out pages — testing with kick/db, errors and an error reference, raw SQL and recipes, multi-tenancy and row-level security; database sections in the testing, multi-tenancy, observability and DevTools guides.
- [x] Restructure — split
queries.mdandschema.mdalong the target structure; concepts page; sidebar. - [x] Onboarding — start-here page, get-started per dialect, coming-from guides with concept-mapping tables, and a five-part tutorial that builds one app from an empty folder to deployment — modules, the database, authentication, testing — as a story, so readers pick up the patterns by following one project. Done: Start Here, get-started pages for each dialect, guides for Prisma, Drizzle, TypeORM and Sequelize, and the Taskboard tutorial.
- [x] Migrations and operations — recovery, CI and deployment, a CLI reference with a section per command, troubleshooting and FAQ, pooling, performance;
kick db checkandkick db migrate unlockadded along the way.
Prioritization — current thinking
Already delivered, in roughly the order the list first proposed them: B.5 Problem Details, B.2 error messages with fix hints, B.4 kick doctor, A.1 typed client, A.3 runtime portability (via the runtime seam + web entry rather than the package split sketched above), B.6 route flags (all four phases), E.2 waitUntil, the E.1 API runner (MVP and phase 2), E.3 + E.4 observer hooks and tracing channels (A.2 re-scoped), E.5 @Cron on serverless, and B.1 the layered scaffolder with kick add wiring and the scaffold matrix, C.1 re-scoped (validation from tables, and table forms), E.9 background jobs with any runner, D.5–D.8 typed database errors, call-chain transactions, transaction retry, and primary-key / CHECK migrations, D.2 kick db seed, D.10 maintained columns (onUpdateNow, version, softDelete), D.11 upsert and a race-safe findOrCreate, D.12 read replicas, D.13 many-to-many relational reads, D.15 test helpers, D.16 findManyAndCount, D.9 richer indexes, D.17 rename prompts, D.20 generated and identity columns, D.19 picking fields in relational reads, D.18 TypeScript migrations, D.21 casing, D.22 migration and transaction options, D.23 edge and serverless drivers, D.24 more column options, D.25 views and materialized views, D.26 row-level security, and D.14 database documentation on par with established ORMs. The list below is what remains.
Rough order if we were optimizing for impact-per-effort:
- B.3 — Interactive docs (2–4 weeks, depends on hosting cost analysis)
Open for redirection — these are starting points, not commitments.
For the database, D.27–D.29 remain proposed, starting with D.27 (migrations from somewhere other than the filesystem).
How to propose changes
To add or amend a proposal:
- Open a PR that edits this file
- New proposals: pick the right track (A/B/C/quick win), use the template above
- Status changes: bump the
Status:line and add a one-line note explaining the bump (e.g.,Status: accepted — chosen as Phase 6 in PR #XYZ) - Rejections: move to the "What we're NOT pursuing" section with reasoning
The goal is a single document that captures what we're considering, what we decided, and why — without becoming a graveyard.