Skip to content

Scaffold multi-app monorepo#1

Merged
raycashmore merged 35 commits into
mainfrom
scaffold
May 16, 2026
Merged

Scaffold multi-app monorepo#1
raycashmore merged 35 commits into
mainfrom
scaffold

Conversation

@raycashmore
Copy link
Copy Markdown
Owner

Summary

Reshapes the repo from a single TanStack Start app into a Vercel Multi-Zones layout, ready for sub-apps to be developed and deployed independently.

  • Rename apps/webapps/budget; mounts at /budget in production via Vercel rewrites.
  • Extract Convex to packages/convex (shared by all apps).
  • packages/tokens — Tailwind v4 CSS-first design tokens, framework-agnostic.
  • packages/shell — shared React Sidebar, Header, AppFrame, and Clerk-aware AuthGate.
  • Scaffold apps/home as the apex zone — owns vercel.json rewrites; serves the future summary landing.
  • Clerk auth with graceful fallback when VITE_CLERK_PUBLISHABLE_KEY is unset (apps still boot during scaffold).
  • ConvexProviderWithClerk wired conditionally so Convex queries carry the Clerk JWT once keys land.
  • PWA shells per app (vite-plugin-pwa) — each sub-app is independently installable. Offline data deferred (see docs/offline.md).
  • Cross-port dev sign-in: Sidebar uses Clerk's buildUrlWithAuth via a context provider so navigating between Home (:3000) and Budget (:3001) doesn't require signing in again.
  • Deleted apps/docs (unused create-turbo boilerplate); root design scratch scripts moved to design/.

New docs

  • docs/architecture.md — multi-zones layout + PWA scope + apps/api-* convention
  • docs/auth.md — Clerk setup (one-time)
  • docs/offline.md — what the PWA shell covers vs. doesn't
  • docs/deployment.md — step-by-step Vercel Multi-Zones deploy

Decisions made along the way

  • No module federation — Vercel rewrites at the edge instead.
  • No Caddy dev proxy — tried, abandoned; TanStack Start + Vite base collide on internal dev routes (/budget/@react-refresh etc. 404). Clerk's URL-based session sync is a simpler win.
  • Per-port dev URLs with absolute localhost:<port> links from the Sidebar; production uses paths so Vercel rewrites kick in.

Test Plan

  • pnpm install resolves cleanly
  • pnpm check-types clean across all apps + packages
  • pnpm lint clean
  • pnpm test — 14/14 passing
  • pnpm build — both apps produce .output/public with sw.js + manifest.webmanifest
  • pnpm dev:
    • Home on http://localhost:3000/ shows welcome + sidebar
    • Budget on http://localhost:3001/ shows the chart + sidebar
    • Click Budget icon on Home → land on Budget, signed in (no second Clerk screen)
  • Deployed to Vercel per docs/deployment.md:
    • <apex>/ serves Home
    • <apex>/budget reverse-proxies to Budget zone
    • Clerk session shared across zones via apex cookie
    • Convex queries authenticated end-to-end

Follow-ups (intentionally out of scope)

  • First non-React sub-app (Svelte experiment)
  • Real offline data layer for shopping list / recipes
  • PNG icons for iOS PWA install (SVG icons cover Chrome/Edge/Firefox)
  • vite-plugin-pwa glob warning — TanStack Start outputs to .output/ not dist/, plugin needs configuring for that
  • Tiny cleanup: stale convex script entry in apps/budget/package.json (now redundant)

raycashmore and others added 30 commits May 5, 2026 20:39
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_generated/ files were already current; fix TS2742 declaration errors
by overriding base.json's declaration:true in packages/convex/tsconfig.json.
Also commit the .gitignore generated by convex dev.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Task 2.3 moved convex out of budget's package.json on the theory that
@repo/convex would re-export everything needed. But budget imports
convex/react directly (subpath not covered by @repo/convex's exports
map), which broke Vite dependency resolution. convex must remain a
direct dep wherever client-side bindings (convex/react, convex/react-clerk)
are imported.
Copy Budget's TanStack Start setup, strip Budget-specific code
(Convex provider, BudgetChart components, vitest), and configure
as the apex zone with a welcome placeholder. vercel.json sets up
rewrites for /budget/* to the Budget zone — destination URL is a
placeholder that needs to be updated after Budget's Vercel project
is created.
- packages/shell AuthGate: Clerk-aware with graceful fallback when
  VITE_CLERK_PUBLISHABLE_KEY is unset (passthrough + console.warn).
  Both apps boot without Clerk during scaffold.
- apps/budget convex provider: conditional ConvexProviderWithClerk
  (Clerk-authenticated Convex) when key is set, plain ConvexProvider
  otherwise.
- packages/convex auth.config.ts: declares the Clerk JWT issuer for
  server-side verification. Reads CLERK_JWT_ISSUER_DOMAIN from the
  Convex dashboard env vars.
- docs/auth.md: one-time Clerk setup steps.

Activating Clerk requires the user to create the Clerk app and add
keys to .env.local per docs/auth.md.
vite-plugin-pwa per app, scoped to its base path. SVG-only icons for
now (apps/{app}/public/icons/icon.svg). PNG icons (apple-touch-icon,
192/512 maskable) are a follow-up — supports Chrome/Firefox install
today; iOS install will use a default icon until PNGs are added.

Convex API calls are explicitly NetworkOnly in Budget's runtime cache
rules; offline data is the deferred Phase 7b concern.
- packages/convex/eslint.config.js and packages/shell/eslint.config.js:
  add eslint configs so pnpm lint passes across the workspace.
- apps/home devtools event-bus port bumped to 42070 to avoid the
  default :42069 collision with Budget when running pnpm dev (both
  apps in parallel).
- Various linter --fix touch-ups across __root.tsx and auth.tsx
  (import reordering, redundant eslint-disable cleanup).

Verification:
- pnpm install: clean
- pnpm check-types: clean
- pnpm lint: clean (5/5 tasks)
- pnpm test: 14/14 passing
- pnpm build: both apps build (sw.js + manifest in each output)
- pnpm dev: Budget on :3000/budget/, Home on :3001/, both boot in parallel
In production, app links stay as paths ('/budget') so Vercel rewrites
on the apex domain route them to the right zone. In local dev there
are no rewrites — paths like /budget on the apex (3001) 404 because
Home doesn't have that route. Each app runs on its own port.

Add devPort to each app and a getAppHref() helper that returns an
absolute URL to the right port in dev (http://localhost:3000/budget/),
and the production path otherwise.
In dev, each app serves at the root of its own port; Vercel rewrites
only run in production. Trying to share the /budget prefix in both
modes broke React Refresh HMR (Vite prefixed /budget/@react-refresh
but the dev server didn't register it under the base).

- apps/budget/vite.config.ts: base is '/budget/' only when building;
  '/' in dev.
- apps/budget/src/router.tsx: basepath matches — '/' in dev, '/budget'
  in production.
- packages/shell/src/apps.ts: getAppHref returns http://localhost:PORT/
  in dev (each app serves at root of its own port). Production paths
  unchanged so Vercel rewrites still work.
- packages/shell/src/AppFrame.tsx: trust the appId prop for active
  sidebar state; drop pathname-based detection (would always read as
  "home" in dev now that all apps serve at /).
Running `pnpm dev:proxy` (after `brew install caddy`) starts Caddy
on :5173 reverse-proxying /budget/* to Budget's port and / to Home.
Single origin → Clerk's dev cookie covers every zone → no more
double sign-in across ports.

The Sidebar's getAppHref does runtime host detection — when the
browser sees a non-direct-dev host (i.e., the proxy), it switches
to relative paths so navigation stays on the proxy origin.

Known limitation: Caddy strips /budget before forwarding, so Budget
sees requests at /. Works for the one-route-per-app state today;
needs a tweak when Budget grows internal routes. Documented in
architecture.md.
Tailwind v4 only auto-scans the consuming app's own directory by
default. The Sidebar, Header, and AppFrame components live in
packages/shell — their class names weren't being discovered, so
the styles silently no-op'd: sidebar/header DOM was there but
invisible (no background, no layout, no positioning).

Adding `@source "../../../packages/shell/src/**/*.{ts,tsx}"` to
each app's styles.css makes Tailwind pick them up.

Same one-liner needed for any future shared package that ships
JSX with Tailwind class names.
The proxy approach turned out to fight TanStack Start at multiple layers:

- With Caddy stripping /budget before forwarding, Budget's dev server
  saw '/' but the browser URL was '/budget/' — TanStack Router's SSR
  hydrator threw "Expected to find a match below the root match in
  SPA mode."
- With Caddy NOT stripping (forwarding /budget/* unchanged) and Vite
  configured with base '/budget/', the dev server 404s on /budget/@
  vite/client, /budget/@react-refresh, /budget/@id/virtual:... The
  TanStack Start + Nitro + Vite combo doesn't honor base for those
  internal endpoints, and the page won't hydrate.

The simpler path is Clerk's own URL-based dev session sync:
buildUrlWithAuth(url) appends a short-lived __clerk_db_jwt to cross-
origin URLs so the destination port auto-rehydrates the session. The
sidebar wraps each cross-origin link.

Implementation:
- packages/shell/src/auth.tsx: AuthGate exposes UrlAuthContext when
  signed in, providing clerk.buildUrlWithAuth as the builder.
- packages/shell/src/Sidebar.tsx: useUrlAuth() pulls the builder from
  context; if absent (passthrough AuthGate, no Clerk) it falls back
  to bare URLs.
- Reverted Budget vite.config / router.tsx / shell apps.ts to the
  port-based-dev model that was working.
- Removed Caddyfile + pnpm dev:proxy script.
- Updated docs/architecture.md with the new approach and a brief note
  about why the proxy was abandoned.

End state: one click on the Budget icon in Home's sidebar lands you
on Budget already signed in. Production behaviour unchanged (same
origin, one cookie, getAppHref / buildUrlWithAuth are no-ops for
relative paths).
@raycashmore raycashmore changed the title Multi-app scaffold: rename web→budget, extract Convex/tokens/shell, scaffold home, Clerk auth, PWA shells Scaffold multi-app monorepo May 16, 2026
@raycashmore raycashmore merged commit 26d11bc into main May 16, 2026
1 check passed
@raycashmore raycashmore deleted the scaffold branch May 16, 2026 03:57
raycashmore added a commit that referenced this pull request May 19, 2026
Scaffold multi-app monorepo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant