0003 — Stack: React + Vite + Express, TypeScript everywhere, port from internalize
- Status: accepted
- Date: 2026-05-06
- Deciders: Derek
Context
Internalize ships a working ~50-route React+Vite frontend on top of an ~30-route Express API. It’s proven for the use case: small org, internal collaboration, real-time features, CMS, push notifications. Switching frameworks (Next/Remix/etc.) means rewriting everything for no concrete win — and forces us to re-debug realtime, auth flows, and CMS code that already works.
Internalize is JavaScript. Ark is multi-tenant and load-bearing for a consulting practice — the cost of “I forgot to scope by org_id at the type level” is unacceptable.
Decision
- Public site: Astro 6 + React 19 islands (lifted from actualize-v2)
- Admin/member portal: React 19 + Vite + React Router (lifted and upgraded from internalize 18)
- API: Express 4 + TypeScript (ported from internalize JS)
- Database: Supabase (Postgres + Auth + Realtime)
- Tests: Vitest (already proven in internalize)
- Lint/format: Biome (replaces eslint+prettier, faster, single tool, agent-friendly config)
- TypeScript: strict mode in every package and app, no exceptions. Internalize JS is ported as we lift each module — the JS never crosses the ark boundary.
Consequences
Easier:
- Lift-and-port: most internalize code translates directly to TS; types are the documentation
- Shared types between frontend and backend via
packages/core - Schema-as-truth via Zod runs at the same boundary on both ends
- AI agents thrive on strict types — the type system prevents most “I forgot to scope by org” bugs at compile time
Harder:
- Porting JS → TS is grunt work; we don’t shortcut it. No
any, no// @ts-expect-errorwithout a written reason. - React 19 island patterns differ slightly from React 18 — rare but real porting friction
Revisit if: the bundle size of the admin portal becomes a problem (it won’t at 30-org scale; may at 300), or if React Router 7+ forces a Remix-flavored migration we want to embrace.
Alternatives considered
- Next.js / Remix. Better SSR story, but admin portal doesn’t need SSR — it’s authenticated, dynamic, and SPA-style works. Forcing the migration costs weeks for no user-visible win.
- Hono on Cloudflare Workers (instead of Express on Railway). Tempting for edge-friendliness. Loses long-running connections (realtime), file upload streaming becomes harder, and we’d give up the proven Express middleware ecosystem we already depend on.
- JS through and through (no TS port). Faster initially, slower in every subsequent month. For a stability-paramount project, the trade is one-sided.