Chambers AppDeveloper documentation

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/..., in apps/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/ and zh/ (one JSON file per feature, composed in each index.ts; a feature's own refusal messages go in its errors key and are merged there) through createTranslator (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 date and number, which use the chamber's time zone.
  • A refusal code from the platform is shown through i18n.error(code); add an errors.<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.ts or middleware.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-wash and apps/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 a Seal beside 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 delete apps/web/.vinext/dev if it reports another server running.