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>
132 lines
7.4 KiB
Markdown
132 lines
7.4 KiB
Markdown
# 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.**
|