This document lives in the repository and is maintained in English. The site does not translate it.
Read this file on GitHubThe locked stack
Settled on 2026-09-09, one question at a time. Every entry has a matching entry in
DECISIONS.md recording the options considered and what the choice costs.
Treat this as given. If you believe an entry is wrong, say so once, clearly, then follow it.
Structural
| Multi-tenancy | Only when the project is multi-tenant, because it is a third axis beside Type and Architecture, and a single-tenant project receives none of it. Then: tenant_id on every tenant-scoped table, isolated by a MikroORM filter. No schema-per-tenant. RLS is stronger and unmeasured; see docs/OPEN-QUESTIONS.md. |
| Primary keys | UUID v7, generated by the database: DEFAULT uuidv7(). Not by the application. |
| Delete | Hard delete is the default. deleted_at requires a stated reason: audit, user-facing recovery, legal retention. |
| Timezone | UTC in the database. timestamptz on every time column, without exception. Conversion at the edge only. |
| Database | PostgreSQL 18 or later. Not a variable. |
| Errors | Exceptions, handled Nest's way, with HttpException and exception filters. No Result type. |
Version floors
These accumulate. Every one is a machine a generated project will not run on.
| Floor | Required by |
|---|---|
| PostgreSQL 18 | uuidv7(), which shipped in September 2025 |
| Node 22.17 | MikroORM v7 |
| TypeScript 6.0 | nestjs/typescript-starter, which declares ^6.0.2. It supersedes the 5.8 floor MikroORM v7 required |
Core
| Area | Choice |
|---|---|
| Monorepo | pnpm workspaces, with native catalog: for versions. No Turborepo. Applies only when the architecture is monorepo. |
| Language | TypeScript 6, based on nestjs/typescript-starter: module and moduleResolution nodenext, target ES2023, strict on, strictPropertyInitialization off. Plus noUncheckedIndexedAccess. Not exactOptionalPropertyTypes. |
| Quality | Biome. Pre-commit is .githooks/pre-commit running biome check --staged --write, wired by "prepare": "node .githooks/install.mjs", which sets core.hooksPath inside a Git repository and does nothing outside one. No husky, no lint-staged. |
| API | NestJS. DTOs are classes validated by class-validator + class-transformer through ValidationPipe; @nestjs/swagger reads the same decorators. |
| ORM | MikroORM v7, with @mikro-orm/postgresql, @mikro-orm/migrations, @mikro-orm/nestjs. Entities use defineEntity with a class: the schema is in the defineEntity call and carries no ORM decorators. The class carries @Exclude and @ApiProperty, for serialization and documentation. |
| Auth | Better Auth, mounted directly on a controller with @All('*path') passing the request to toNodeHandler(auth), plus a hand-written global guard. Own Postgres schema, own connection, own migration CLI, generateId: 'uuid'. httpOnly cookie on web and mobile, kept in expo-secure-store. No community Nest bridge. |
| Observability | Structured logs and a health check. No error reporting and no tracing by default. See Documented, not installed below. |
| Testing | Vitest, Testcontainers, Testing Library. |
| Web | Vite, React, TanStack Router, TanStack Query, Tailwind, shadcn/ui, react-hook-form with Zod as the resolver. |
| Mobile | Expo with prebuild, Expo Router, NativeWind, Reanimated, MMKV, expo-secure-store. |
| Site | Next with the App Router, never the Pages Router. Tailwind and shadcn/ui. Static by default, revalidated where content changes, dynamic only with a stated reason. Data is fetched on the server. |
shadcn/ui is not a dependency, because components are copied into the project. Templates ship its
plumbing and only the components their own screens use, installed by its CLI, on Base UI, which is shadcn's
default primitive library.
Documented, not installed
Prumo never installs a tool requiring a commercial account. Such a tool is documented (how it is wired, what it needs, what it changes) so it is ready the day the developer wants it, and absent until then. Installing a service that needs an account, a DSN and eventually a bill is a commercial decision made on the developer's behalf, inside a project they own.
| Capability | Documented adapter | Default |
|---|---|---|
| Error reporting and tracing | @sentry/nestjs, see api/observability.md |
not installed |
Deliberately dropped
Sixteen entries left the previous stack. Each was removed for a stated reason, not trimmed for taste.
| Dropped | Because |
|---|---|
Result / neverthrow |
The restart removed Result. Nest's error model is exceptions, and two error models in one codebase is worse than either. |
| ts-pattern | Existed for the Result union. switch with a never default gives the same exhaustiveness for free. |
| dependency-cruiser | Enforced the hexagonal layering, which is gone. The restart measured it producing a useless file to satisfy no-circular. |
| Fastify + type provider + Scalar + Pino | Replaced by NestJS. |
| The porting rule | Removed explicitly by the restart. |
| Drizzle + drizzle-kit | Replaced by MikroORM. Still on 0.x with an unreleased breaking v1, and absent from Nest's documentation. |
| Turborepo | Its value is caching, and a two-package monorepo does not hurt yet. |
| syncpack | pnpm catalogs do the job natively. |
| knip | Finds nothing on a day-zero project. |
| husky + lint-staged | biome check --staged and core.hooksPath replace both, with no dependency. |
@t3-oss/env-core |
Zod-based, and Zod left the API. Its distinctive value, keeping server secrets out of a client bundle, does not apply to an API with no bundle. |
| Zustand | TanStack Query owns server state; what remains is usually context-sized. |
| FlashList | FlatList handles small lists, and the swap is local when they grow. |
| MSW | Mocks a network that does not exist yet. |
| Playwright | Downloads a browser to walk screens that do not exist yet. |
exactOptionalPropertyTypes |
The only strictness flag whose friction lands in library types rather than your own code. |
The two rules that decided most of this
The day-zero test. Does this have work on the day the project is born? It removed knip, MSW, Playwright, ts-pattern and Zustand.
Its limit. The test does not apply to anything that owns data, because arriving late is then a migration rather than a swap. That kept MMKV and Better Auth. Whatever owns data is decided at the start; whatever draws a screen can wait.