product

Build performance — analysis + the multi-zone split plan

Analysis 2026-07-27. The app is 443 page.tsx routes (214 under /dashboard), 519 total route entries, 510 dynamic / 9 static. Build time is dominated by compiling the route graph + (until now) an inline tsc + ESLint pass, with no build caching.

What the build spends time on (measured)

  • Compiling 443 route entries + shared chunks (~50s–2min for webpack alone; more on a loaded box).
  • tsc + ESLint run INSIDE next build — no ignoreBuildErrors/ignoreDuringBuilds, and there was no separate CI typecheck gate. On a 440+-route app the "checking validity of types / linting" phase is a large, serial slice of every deploy build.
  • No Turborepo / .next/cache reuse — every build is from scratch; a one-line marketing-copy change rebuilds all 519 routes.
  • Heavy WebXR/three stack (three + @react-three/fiber/drei/xr) in the graph for ~a dozen VR routes (already needs webpack-alias wrestling in next.config.js).
  • Not static-generation: only 9 static pages, ~8 build-time backend-fetch stalls. This is a compile-surface problem, not a data-fetch one.

Tier 1 — quick wins ✅ SHIPPED (2026-07-27)

  1. Moved tsc + ESLint out of the deploy buildapps/frontend/next.config.js sets typescript.ignoreBuildErrors + eslint.ignoreDuringBuilds; the safety moved earlier + parallel via the check-frontend GitHub Actions job (npm run check on every frontend PR). Errors block the PR, not the deploy. next build still runs the prerender pass, so Suspense / dynamic-params landmines are still caught. See CLAUDE.md "Preflight".
  2. TODO (Tier 1b): Turborepo + .next/cache persistence in CI so unchanged workspaces/pages skip rebuild. Pairs with the split below (Turbo caches unchanged zones).

Tier 2 — the multi-zone split (the durable fix)

Goal: stop rebuilding the whole app for every change. Split apps/frontend into independently-built Next apps so each build is a fraction of the routes and they build in parallel; a marketing-copy change never rebuilds the 214-route dashboard.

Proposed zones

ZoneRoutes (approx)VolatilityNotes
apps/marketing~150 public (/, /features, product landings, /compare, /blog, /pricing, /sitemap, /ethos, 404 …)high (copy, painterly bands)Fast build; the surface we touch most.
apps/app (rename of today's apps/frontend)~370 authed (/dashboard/**, compose, stream, studios)mediumThe product.
apps/admin (optional 3rd)/dashboard/admin/** (~30)high, super-admin onlyCarve out so admin churn never blocks user deploys.

How to wire it

  • Next Multi-Zones: each zone is its own Next app with a distinct basePath/assetPrefix; a top-level rewrites() (or a Vercel project with rewrites) stitches paths so the domain is seamless. Cross-zone links are plain <a href> (not <Link>), since they cross app boundaries.
  • Shared code → a workspace package (packages/ui or packages/shared): the design system, Navbar/SiteFooter, nav-catalog.ts, HighlightBand, AppLauncher, lib/api, supabase client, types. Both zones depend on it. This extraction is the bulk of the work and should land first, incrementally, while everything still lives in one app.
  • Auth across zones: Supabase session cookie must be scoped to the apex domain (.hivejournal.com) so a session set in apps/app is readable in apps/marketing (for the logged-in→/dashboard redirect) and vice versa. Verify cookie domain/sameSite.
  • Env + deploy: one Vercel project per zone (or one project with multiple builds), each with its own deploy-*.yml triggered on that zone's paths:. Turborepo then caches unchanged zones → a marketing change rebuilds only apps/marketing.

Sequencing (each step ships independently, app stays whole until the last)

  1. Extract shared code into packages/shared (design system, Navbar/footer, nav-catalog, api, supabase). No behavior change; just move + re-import. Biggest, safest chunk.
  2. Add Turborepo (turbo.json) across apps/* + packages/* + backend; wire CI to Turbo cache. Immediate incremental-build win even before splitting.
  3. Stand up apps/marketing as a second Next app consuming packages/shared; move the ~150 public routes into it; add rewrites so the domain is seamless; verify the logged-in→dashboard redirect + cookie domain.
  4. (Optional) carve apps/admin.
  5. Delete the moved routes from the original app; it becomes apps/app.

Cost / risk

  • Effort: days, front-loaded on step 1 (shared-package extraction) — mechanical but broad.
  • Risk: cross-zone auth/cookie edge cases; cross-zone navigation must use <a>; duplicate shared deps if the package boundary is sloppy. All manageable, none irreversible.
  • Payoff: each build ≈ half (or a third) the routes, in parallel, cached — plus deploy independence (marketing ships without waiting on the app).

Progress + a key refinement (2026-07-27)

Foundation shipped + fully validated in CI and Vercel (#1633–#1636):

  • @hivejournal/shared — platform-agnostic (types, utils, nav-catalog), prebuilt dist, also used by mobile. Added a prepare hook so dist builds on every npm ci (dist is gitignored) — validated against a clean install + Vercel.
  • packages/ui — web/Next components consumed from source via transpilePackages (so 'use client', next/image, JSX work). HighlightBand (server component) + SiteMapExplorer (client component) both proven through it.
  • check-frontend CI widened to packages/**.

So both package patterns, server + client components, the prepare hook, and the gate are all de-risked.

The coupling finding that reframes step 3. The clean, self-contained components are extracted. Everything else worth sharing converges on the heavy app chrome: LandingShell → imports Navbar + SiteFooter; SiteFooterHearThisPage → narration → api/supabase; Navbar is the 2,286-line everything-widget. Trying to hoist that chrome into a package drags the whole app infra with it.

The refinement: apps/marketing does not want the app's chrome. A public marketing page needs a lean, purpose-built Navbar + Footer (logged-out CTAs, footer links — no 25-icon bar, no HearThisPage, no auth context), and it reuses only the genuinely-shared UI already in packages/ui (painterly bands, sitemap) + packages/shared (nav-catalog). So step 3 is "scaffold a lean marketing app and move the ~150 public routes into it," not "extract Navbar/SiteFooter into a package." That's a smaller, cleaner target than it first looked — the shared surface is small; the chrome is rebuilt lean.

Revised step 3: scaffold apps/marketing (new Next app, lean chrome, transpilePackages: ['@hivejournal/ui']) → prove the multi-zone rewrite + cookie-domain auth with one moved page → then batch-move the rest. A dedicated effort (second Next app + multi-zone routing is fiddly), best on fresh context.

Migration runbook (the dedicated effort)

The pattern is proven end-to-end (apps/marketing builds in ~2s, has lean chrome, and serves a real reused route at /sitemap). What's left is a migration, and it has one non-obvious ordering rule:

Wire the routing switch and move routes TOGETHER, not routes-first. Moving a route into apps/marketing before the multi-zone routing exists just creates a duplicate, drift-prone, inert copy (the frontend still serves it in prod). Value appears only when the zone switch flips traffic. So each route moves and goes live in the same step, behind a preview.

Order of operations (each step ships behind a Vercel preview, reversible):

  1. Multi-zone routing first (production-touching — do deliberately, watch previews). Stand up apps/marketing as its own Vercel project. On the primary project, add rewrites so a fixed set of public paths (start with just /sitemap) proxy to the marketing zone; everything else stays on the app. Verify the domain is seamless on a preview before promoting.
  2. Cookie-domain auth. Confirm the Supabase session cookie is scoped to .hivejournal.com so a session set in either zone is readable in the other (needed for the logged-in //dashboard redirect + logged-in nav state). Test cross-zone before moving auth-aware pages.
  3. Migrate routes in waves, cheapest-first, adding each to the rewrite set as it moves:
    • Wave A (self-contained): pages whose only app deps are chrome (Navbar/SiteFooter, provided by the marketing layout) + already-shared UI. Strip the chrome imports, drop the page in, add its path to the rewrites. (/privacy, /terms, /about-that, /docs, /examples …)
    • Wave B (needs a shared bit): pages importing one more component/lib — move that dep to packages/ui/packages/shared first (mechanical, same as HighlightBand), then the page.
    • Wave C (landing pages): build the lean marketing equivalents of LandingShell/HeroGlow in apps/marketing (they wrap the heavy chrome today), then move the /for-* + product landings.
  4. Deploy workflow for apps/marketing (mirror deploy-frontend.yml, path-filtered to apps/marketing/** + packages/**).
  5. Delete the migrated routes from apps/frontend only once the zone is serving them in prod, so there's never a gap.

Per-route recipe (Wave A): copy the page's inner content into apps/marketing/src/app/<route>/page.tsx; delete its <Navbar/>/<SiteFooter/> wrapper (the marketing layout provides lean chrome); repoint any shared imports to @hivejournal/ui/@hivejournal/shared; cd apps/marketing && npm run build (the check-marketing gate runs this in CI); add the path to the rewrite set; verify on a preview.

Tier 3 — compile-surface trims (modest, do anytime)

  • Ensure the WebXR/three stack is fully next/dynamic ssr:false on the VR routes so three/fiber/drei/xr stays out of the shared/server compile graph for non-VR builds.
  • Prune dead one-offs (/for-ben, stale experimental routes) — marginal but real at 443 routes.

Recommendation

Tier 1 is shipped. Measure a few deploys. If builds are still too slow, do Tier 2 in the sequenced order — the shared-package extraction (step 1) + Turborepo (step 2) deliver most of the win and de-risk the actual zone split.

BUILD PERFORMANCE — Docs | HiveJournal