Scaffold multi-app monorepo#1
Merged
Merged
Conversation
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
added a commit
that referenced
this pull request
May 19, 2026
Scaffold multi-app monorepo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
apps/web→apps/budget; mounts at/budgetin production via Vercel rewrites.packages/convex(shared by all apps).packages/tokens— Tailwind v4 CSS-first design tokens, framework-agnostic.packages/shell— shared ReactSidebar,Header,AppFrame, and Clerk-awareAuthGate.apps/homeas the apex zone — ownsvercel.jsonrewrites; serves the future summary landing.VITE_CLERK_PUBLISHABLE_KEYis unset (apps still boot during scaffold).ConvexProviderWithClerkwired conditionally so Convex queries carry the Clerk JWT once keys land.vite-plugin-pwa) — each sub-app is independently installable. Offline data deferred (seedocs/offline.md).buildUrlWithAuthvia a context provider so navigating between Home (:3000) and Budget (:3001) doesn't require signing in again.apps/docs(unused create-turbo boilerplate); root design scratch scripts moved todesign/.New docs
docs/architecture.md— multi-zones layout + PWA scope +apps/api-*conventiondocs/auth.md— Clerk setup (one-time)docs/offline.md— what the PWA shell covers vs. doesn'tdocs/deployment.md— step-by-step Vercel Multi-Zones deployDecisions made along the way
basecollide on internal dev routes (/budget/@react-refreshetc. 404). Clerk's URL-based session sync is a simpler win.localhost:<port>links from the Sidebar; production uses paths so Vercel rewrites kick in.Test Plan
pnpm installresolves cleanlypnpm check-typesclean across all apps + packagespnpm lintcleanpnpm test— 14/14 passingpnpm build— both apps produce.output/publicwithsw.js+manifest.webmanifestpnpm dev:http://localhost:3000/shows welcome + sidebarhttp://localhost:3001/shows the chart + sidebardocs/deployment.md:<apex>/serves Home<apex>/budgetreverse-proxies to Budget zoneFollow-ups (intentionally out of scope)
vite-plugin-pwaglob warning — TanStack Start outputs to.output/notdist/, plugin needs configuring for thatconvexscript entry inapps/budget/package.json(now redundant)