← All decisions

Stack: React + Vite + Express, TypeScript everywhere, port from internalize

accepted

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-error without 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.