phase-0: foundation, both apps boot end to end
Monorepo (pnpm workspaces) with two deployable apps and three pure packages. apps/api (Hono on Node): Zod validated env that fails fast and names the problem, Kysely factories for SQLite and Postgres chosen by DATABASE_URL scheme, portable migrations covering the whole SPEC section 5 schema, Better Auth with the four roles and seeded demo accounts, localized error envelope, /healthz and /readyz, graceful SIGTERM drain. Dialect specific SQL is confined to the two factories. apps/web (Next.js App Router): locale routed shell in es and en with a language switcher, sign in screen, and a runtime /api proxy so the browser only ever sees one origin and cookies stay first party. packages/i18n ships both catalogs complete; es is generated from COPY.md and a test re-derives it from the document on every run so it cannot drift. packages/contracts holds the Zod schemas and the typed client the web app uses. Verified: 43 vitest tests, 14 Playwright tests on mobile and desktop, typecheck and lint clean, migrate and seed from a clean database, sign in through the proxy with CSRF rejection of foreign origins. Not verified here: docker compose. This user has no access to the docker socket. RULES.md is absent from docs/, so packages/rules exports only RULES_VERSION and no tax rule, check digit or deadline was invented. See DECISIONS.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+131
@@ -0,0 +1,131 @@
|
||||
# DECISIONS
|
||||
|
||||
One entry per decision that is not already obvious from the specs. Newest phase last.
|
||||
|
||||
Markers used in the code:
|
||||
- `// SPEC-GAP:` the specs did not settle this and a choice was made here.
|
||||
- `// TODO-TAX-VERIFY:` a tax rule that RULES.md does not state. Never invented, always flagged.
|
||||
|
||||
---
|
||||
|
||||
## Blocking gaps in the source material
|
||||
|
||||
### RULES.md is missing
|
||||
`docs/` ships SPEC.md, FLOWS.md, COPY.md and CONTRACTS.md. RULES.md, which the prompt
|
||||
names as authoritative for every tax rule, is not present. Phase 1 is entirely RULES.md
|
||||
and phases 4 and 5 depend on it.
|
||||
|
||||
Consequence for phase 0: `packages/rules` exists and exports only `RULES_VERSION`.
|
||||
Nothing in this phase computes a tax number, a check digit or a deadline, so nothing was
|
||||
invented. The seed deliberately stops short of profiles for the same reason: CONTRACTS.md
|
||||
section 4 asks for RUC base `4123456` "with computed DV" and a `deadlineDigit`, both of
|
||||
which are RULES.md algorithms. Seeding accounts only keeps phase 0 honest.
|
||||
|
||||
**Needed before phase 1 starts.**
|
||||
|
||||
### `boneyard` and `canvas-ui` are not the packages the prompt means
|
||||
Both names resolve on npm to unrelated projects: `boneyard@0.1.4` is a 2015 Backbone
|
||||
"architectural toolkit", `canvas-ui@0.2.3` is a Mesosphere Bootstrap theme. Neither does
|
||||
skeleton loading or canvas effects. Neither is needed before phase 7.
|
||||
|
||||
Plan unless corrected: keep the *behaviour* the prompt specifies (skeletons on every
|
||||
content load and never a spinner; canvas effects in exactly two places, degrading
|
||||
gracefully) behind a single `<Skeleton name>` component and a single effect component, so
|
||||
swapping in the real library later is a one file change.
|
||||
|
||||
**Please confirm the intended packages before phase 7.**
|
||||
|
||||
### Neither named skill is installed
|
||||
`ponytail` and `ui-ux-pro-max-skill` are not available in this environment. Their stated
|
||||
intent was applied by hand: nothing speculative, no unused configuration, and FLOWS.md
|
||||
section 1 as the design constraint.
|
||||
|
||||
---
|
||||
|
||||
## Phase 0
|
||||
|
||||
### Migrations: better-auth generates its own four tables, we own the rest
|
||||
`src/db/migrator.ts` runs two ordered steps: better-auth's `getMigrations()` creates and
|
||||
updates `user`, `session`, `account` and `verification`, then the Kysely migrator applies
|
||||
`src/db/migrations`. Delegating the auth tables keeps them in step with the installed
|
||||
better-auth version and emits correct DDL for both dialects, so no hand written dialect
|
||||
SQL was needed for them. Upgrading better-auth in a way that adds a column means adding a
|
||||
migration that calls the same generator again.
|
||||
|
||||
### The whole schema ships in migration `001_core`, not phase by phase
|
||||
Every table in SPEC.md section 5 is created now. The schema is fully specified and stable;
|
||||
splitting it across phases would produce a pile of migration files and no benefit before
|
||||
release. Later phases add modules on top, not tables.
|
||||
|
||||
### Timestamps and money are portable by construction
|
||||
Timestamps are ISO-8601 text and dates are `YYYY-MM-DD` text in both dialects: they sort
|
||||
chronologically as strings, so no dialect specific date type or comparison is needed
|
||||
anywhere. Money is `bigint`, because guaranies pass int4 at about Gs. 2.100.000.000, and
|
||||
`apps/api/src/db/postgres.ts` registers an int8 parser that returns a number and throws
|
||||
outside the safe integer range.
|
||||
|
||||
### `/api` is proxied by a route handler, not a Next rewrite
|
||||
SPEC-GAP against SPEC.md section 2, which specifies Next rewrites. Next bakes rewrite
|
||||
destinations into the build manifest, so `API_INTERNAL_URL` would become a build time
|
||||
value and one image could not serve both compose and k8s. `apps/web/app/api/[...path]/route.ts`
|
||||
forwards at request time instead. The single origin model is unchanged: the browser only
|
||||
ever sees the web origin, cookies stay first party, and there is still no CORS anywhere.
|
||||
|
||||
### Workspace packages ship TypeScript source with no build step
|
||||
`packages/*` have no `dist`. The web app lists them in `transpilePackages` and tsup bundles
|
||||
them into the API. Their relative imports are extensionless, because Turbopack does not
|
||||
rewrite a `.js` specifier onto a `.ts` source file.
|
||||
|
||||
### The es catalog is split from the strings COPY.md does not define
|
||||
`catalogs/es.ts` is generated from COPY.md and is verbatim; `copy-parity.test.ts` re-derives
|
||||
it from `docs/COPY.md` on every run and fails on any drift, in either direction. Strings the
|
||||
product needs that COPY.md does not list (the seven error envelope messages, three sign in
|
||||
labels, the language switcher) live in `catalogs/es.extra.ts` and follow the tone rules in
|
||||
COPY.md section 0. `es` is the merge of the two.
|
||||
|
||||
### `decl.approve` is both a message and a namespace
|
||||
COPY.md defines `decl.approve` (the button) alongside `decl.approve.confirmTitle`. A nested
|
||||
message tree cannot hold both, and next-intl walks a nested tree. `unflatten` moves such a
|
||||
message to a reserved `_` child and `resolveKey` maps the key for callers, so components
|
||||
still address messages by their COPY.md key. `apps/web/src/i18n/t.ts` is the wrapper; it is
|
||||
computed from the catalog, so a future collision is handled without another change.
|
||||
|
||||
### Locale negotiation on `/`
|
||||
next-intl's default detection is left on: a browser asking for English lands on `/en`,
|
||||
anything else falls back to `es`. The en catalog exists for expats and international users
|
||||
(COPY.md section 0-EN), which is exactly the population whose browser is in English. An
|
||||
explicit choice through the switcher always wins and is in the URL.
|
||||
|
||||
### SQLite refuses `JOBS_INLINE=false`
|
||||
Implemented literally as SPEC.md section 15 instructs, which is narrower than the mode
|
||||
matrix in the same section: that table allows "SQLite, 1 dedicated worker" under Split
|
||||
small. Two pollers cannot be made safe against a single writer, so the boot check wins and
|
||||
SQLite stays single process. Worth reconciling in SPEC.md.
|
||||
|
||||
### Deferred to the phase that needs them, deliberately
|
||||
- `RateLimiter`: SPEC.md section 6 specifies a token bucket, but the first endpoint with a
|
||||
stated limit is `GET /lookup/ruc/:number` in phase 2. better-auth's own rate limiting
|
||||
covers the auth routes until then.
|
||||
- DTO schemas in `packages/contracts`: enums, the error envelope, the client and
|
||||
`ProfileDto` exist because phase 0 uses them. The rest arrive with their endpoints.
|
||||
- Storage in `/readyz`: the check covers the database and pending migrations. The storage
|
||||
driver probe is added in phase 3 with the driver.
|
||||
|
||||
### Smaller choices
|
||||
- TypeScript 5.9, not 7.x: `typescript-eslint@8` declares `typescript <6.1.0`.
|
||||
- `better-sqlite3` is kept out of `onlyBuiltDependencies`: it ships prebuilt binaries, so
|
||||
letting pnpm run the implicit `node-gyp rebuild` would compile it for nothing and force a
|
||||
toolchain into the image.
|
||||
- The language switcher is a native `<select>`: one dependency fewer than a popover, and
|
||||
the better mobile and keyboard experience for a two item choice.
|
||||
- `apps/web/proxy.ts`, not `middleware.ts`: Next 16 deprecates the middleware convention.
|
||||
- The dark mode palette and the `dark` variant are wired now; the toggle itself is phase 7
|
||||
polish. System preference works today.
|
||||
- `audit_log` is append only by construction: the module exposes no update or delete. A
|
||||
database trigger was written and then removed, because it would have been the only piece
|
||||
of dialect specific SQL outside the two files SPEC.md section 5 allows.
|
||||
|
||||
### Legal pages
|
||||
`/legal/privacidad` and `/legal/terminos` do not exist yet (they belong to phase 2's
|
||||
marketing routes). Per COPY.md section 13 they will ship with clearly marked placeholder
|
||||
content and no generated legal text. **TODO: human written before launch.**
|
||||
Reference in New Issue
Block a user