Technologies
The preferred stack. Principles are universal and patterns are conditional — these are chosen defaults. Follow them unless the project states a reason not to, and when you do deviate, record why.
Choosing a stack
Does a user interact with it?
├─ Yes → React PWA (react-pwa.md)
│ Should it work offline?
│ └─ Yes → local-first.md
└─ No → a service or job (spring-boot-kotlin.md)
Does it need a backend?
├─ CRUD + auth + realtime + storage → Supabase (supabase.md)
└─ Real business rules, batch work,
heavy integration, a domain model → Spring Boot (spring-boot-kotlin.md)
Where does it run?
├─ Static frontend, edge logic → Cloudflare Pages / Workers (cloudflare.md)
└─ JVM service → a container host, Cloudflare at the edge only
The defaults
| Layer | Choice | File |
|---|---|---|
| Language (Node/browser) | TypeScript, strict. Never JavaScript | typescript.md |
| Runtime & packages | fnm + pnpm, both version-pinned | typescript.md |
| Frontend | Vite + React + React Router, Tailwind + shadcn/ui | react-pwa.md |
| Offline | Dexie + Workbox, hand-rolled sync | local-first.md |
| Backend (default) | Supabase, RLS on every table | supabase.md |
| Backend (complex domain) | Kotlin + Spring Boot + Spring Data JDBC | spring-boot-kotlin.md |
| REST API conventions | HATEOAS, Problem Details, pagination, idempotency | spring-boot-api.md |
| Running a service | Actuator, Micrometer/OTel, timeouts, OAuth2 | spring-boot-operations.md |
| Hosting | Cloudflare Pages + Workers | cloudflare.md |
| API contracts | Contract first — OpenAPI written before code, types generated | type-contracts.md |
| Accessibility | WCAG 2.1 AA, native elements, keyboard paths | accessibility.md |
| Resilience | Error boundaries, retry GET only, four states | frontend-resilience.md |
| Time, money, language | UTC stored, money as integers, extracted strings | localization.md |
| Formatting | Prettier · ktfmt · google-java-format, on commit | formatting.md |
| Repo layout | pnpm workspace monorepo | repo-structure.md |
| Native shells | PWA first; Capacitor or Tauri only on demand | packaging.md |
Testing
| Layer | Tools |
|---|---|
| TypeScript unit | Vitest |
| Browser e2e | Playwright, Screenplay pattern (../patterns/screenplay.md) |
| Kotlin unit | JUnit 5 + MockK |
| Kotlin integration | Testcontainers (real Postgres, never H2) |
Standing rules
- TypeScript, not JavaScript. Everywhere, no exceptions in source.
- Accessibility is part of the work, not a later pass. Native elements, keyboard paths, visible focus.
- Formatting is automated, applied by a pre-commit hook and enforced in CI. Never hand-format, never debate style.
- Pin every version. Node in
.node-version, pnpm inpackageManager, dependencies exact. Reproducible builds are not optional. - Contract first for REST. The OpenAPI spec is written and reviewed before the implementation; server interfaces and clients are generated from it, never the other way round.
- Generate types, never hand-write them across a boundary that owns a schema. Validate at runtime with Zod anyway.
- Cross-platform means PWA first. A native shell is a cost, taken deliberately.
- Prefer the lower rung. Supabase before Spring Boot, PWA before Capacitor, plain pnpm before Turborepo. Escalate on evidence, not anticipation —
../principles/kiss.md,../principles/yagni.md,../principles/measure-first.md.
Deviating
These are defaults, not laws. A project may have a real reason to choose differently — an existing codebase, a client mandate, a genuine technical constraint. Record the decision and its reason in the project, then follow it consistently. What is not acceptable is drifting off the default silently.