Decisions
The standing decisions the product is built on.
The decisions the product is built on, as the repository's CLAUDE.md records them (its notes on the team's own tooling and working practice are left out here). Decisions are made through planning on the issue tracker; the architecture decision records, when there are any, live in docs/adr/.
Architecture decision records
None recorded yet: docs/adr/ does not exist on this branch. It is created when the first decision is written down.
Standing decisions
- Stack: Next.js on Cloudflare Workers through vinext, with Cloudflare for data and infrastructure (D1, R2, KV, Queues, Vectorize, Cron Triggers).
- One monorepo holds the whole product: the web app now, and a mobile app later if one is built.
- Login is by email one-time code. The interface ships in Chinese and English.
- One web app serves both the member area and the admin area. It is an installable web app first; the backend is exposed through API routes so a native app can be added later.
- Notifications at launch are email, an in-app inbox and push. WhatsApp reminders come in a later phase.
Languages
- Every page lives under its language:
/zh/...and/en/..., inapps/web/app/[locale]/. An address without a language is redirected to the visitor's saved choice, else their browser's preference, else Chinese. - All interface text comes from the catalogues in
apps/web/messages/en/andzh/(one JSON file per feature, composed in eachindex.ts; a feature's own refusal messages go in itserrorskey and are merged there) throughcreateTranslator(t("signIn.title")). Never write user-facing text in a component. English is the source catalogue; a test fails if the languages differ in keys or placeholders. - Messages use ICU syntax, so write plurals and selects in the message, not in code. Format dates and numbers with the translator's
dateandnumber, which use the chamber's time zone. - A refusal code from the platform is shown through
i18n.error(code); add anerrors.<code>message for each new code. - Every action form includes
<ActionFields locale={locale} />so redirects stay in the user's language. - Do not use
proxy.tsormiddleware.ts: under this vinext and Cloudflare setup they did not run in development (checked November 2026). Language redirects are done in routes instead. - The privacy notice is always shown in Malay and English, whatever the interface language.
Design
- Tokens are in
apps/web/app/globals.css. A chamber brands the app through--chamber,--chamber-washandapps/web/lib/brand.ts, which hold placeholders until the pilot chamber supplies its identity. - Use the shared pieces:
Shell,PageTitle,Seal,Record,Field,Alarm,buttonClass,inputClass,InlineForm,SectionHeading. A decision about a person is shown with aSealbeside who decided and when. - Forms open in place with
InlineForm; there are no dialogs. Content sits in ruled rows, not cards. - Base CSS rules must stay inside
@layer base, or they override Tailwind utilities. - To restart local development cleanly:
pkill -9 -f "vite.js dev"; pkill -9 -f workerd, and deleteapps/web/.vinext/devif it reports another server running.