Context
Currently the calendar app (compass repo) is deployed to app.compasscalendar.com (GCP VM) and the marketing site (compasscalendar.com repo) is deployed to www.compasscalendar.com (Vercel). Many landing page visitors never click the CTA into the app. The goal is to consolidate onto compasscalendar.com — the marketing site becomes the shell, the app lives at compasscalendar.com/app, and the two domains are eliminated in favor of one.
Architecture after full migration:
compasscalendar.com — Next.js on Vercel (marketing + blog, unchanged repo)
compasscalendar.com/app/* — React SPA, Vercel rewrites → GCP VM
compasscalendar.com/api/* — Express backend, Vercel rewrites → GCP VM
app.compasscalendar.com — unlisted internal proxy target (Vercel rewrites point here); direct browser visits 301 → compasscalendar.com
- Self-hosters — no change, continue serving at root
/
Phase 1: Deploy app to compasscalendar.com/app
Goal: Prove it works. The app becomes accessible at compasscalendar.com/app while app.compasscalendar.com continues to work.
1. Make the web build base path configurable
compass/packages/web/build.ts:63 — Change hardcoded publicPath: "/" to:
publicPath: process.env.APP_BASE_PATH ?? "/",
Also inject it into the define object so the router can consume it at runtime:
APP_BASE_PATH: JSON.stringify(process.env.APP_BASE_PATH ?? "/"),
2. Add basename to React Router
compass/packages/web/src/routers/index.tsx:31 — Add a basename to createBrowserRouter:
export const router = createBrowserRouter([...routes], {
basename: APP_BASE_PATH, // global injected at build time
future: { v7_relativeSplatPath: true },
});
APP_BASE_PATH is the build-time global defined above ("/app" in production, "/" for self-hosters and dev).
3. Update Nginx on the GCP VM
compass-scripts repo — gcp/local-vm/nginx-prod/sites-enabled/app.compasscalendar.com
Replace the current root location / with:
location /app — serves the static SPA (with try_files falling back to /app/index.html)
location = / — 301 redirect to /app (keeps existing app.compasscalendar.com links alive during transition)
/api and /socket.io blocks stay unchanged
4. Update production env vars
compass-scripts repo — gcp/.prod.env
BASEURL=https://compasscalendar.com/api
FRONTEND_URL=https://compasscalendar.com
CORS=http://localhost:3000,http://localhost:9080,https://app.compasscalendar.com,https://compasscalendar.com
APP_BASE_PATH=/app
5. Add Vercel rewrites in compasscalendar.com repo
compasscalendar.com/next.config.mjs — Add to the rewrites() return:
{ source: "/app", destination: "https://app.compasscalendar.com/app" },
{ source: "/app/:path*", destination: "https://app.compasscalendar.com/app/:path*" },
{ source: "/api/:path*", destination: "https://app.compasscalendar.com/api/:path*" },
Note: WebSocket (/socket.io) may need its own rewrite entry — confirm during testing.
6. Manual: Google OAuth Console
Add https://compasscalendar.com to authorized JavaScript origins and redirect URIs (https://compasscalendar.com/auth/google/callback).
7. Deploy & verify
bash dev/compass.sh deploy --project web --env production
bash dev/compass.sh deploy --project backend --env production
Visit compasscalendar.com/app — SPA loads, Google auth works, calendar is functional.
Phase 2: Welcome dialog
Goal: Show a brief "what is this" dialog to unauthenticated users over the app (like tweek.so).
New component: packages/web/src/components/WelcomeDialog/WelcomeDialog.tsx
- Show when user is not authenticated
- Hide once user signs in — tied to auth state (reappears on logout)
- Content: 2–3 sentences describing Compass + a sign-in CTA
- Follow existing modal conventions (
AuthModal)
Wire into packages/web/src/views/Root.tsx.
Verify: Visit compasscalendar.com/app logged out → dialog appears. Sign in → dialog disappears.
Phase 3: Decommission app.compasscalendar.com
Goal: One domain. Old subdomain demoted to unlisted internal proxy target.
-
No DNS changes — app.compasscalendar.com stays in DNS, Vercel rewrites from Phase 1 continue to target it unchanged.
-
Nginx — Keep all location blocks intact (Vercel still proxies through). Optionally add a redirect for direct browser visits:
if ($http_x_vercel_proxy != "1") {
return 301 https://compasscalendar.com;
}
Simpler alternative: leave the vhost fully open — it's never linked anywhere, so it's effectively dead to users once CTA URLs are updated.
-
Update CTA URLs in compasscalendar.com repo:
components/simplify-your-day-button.tsx — https://app.compasscalendar.com/?utm_... → https://compasscalendar.com/app?utm_...
components/quiz/Results.tsx — same change
-
Manual: Remove app.compasscalendar.com from Google OAuth Console. Update Red Sift cert monitoring.
Verify: app.compasscalendar.com/* → 301 to compasscalendar.com. compasscalendar.com/app loads and auth works end-to-end.
Self-hosting: No changes needed
APP_BASE_PATH defaults to "/" when unset. Docker Compose and self-hosting docs are unaffected.
Files changed
| File |
Phase |
Change |
compass/packages/web/build.ts |
1 |
Configurable publicPath + inject APP_BASE_PATH global |
compass/packages/web/src/routers/index.tsx |
1 |
Add basename: APP_BASE_PATH to router |
compass/packages/web/src/views/Root.tsx |
2 |
Wire WelcomeDialog |
compass/packages/web/src/components/WelcomeDialog/ |
2 |
New component |
compass-scripts/gcp/.prod.env |
1 |
Update BASEURL, FRONTEND_URL, CORS, APP_BASE_PATH |
compass-scripts/gcp/local-vm/nginx-prod/sites-enabled/app.compasscalendar.com |
1+3 |
/app location + / redirect; browser-visit 301 in Phase 3 |
compasscalendar.com/next.config.mjs |
1 |
Add /app and /api rewrites |
compasscalendar.com/components/simplify-your-day-button.tsx |
3 |
Update CTA URL |
compasscalendar.com/components/quiz/Results.tsx |
3 |
Update CTA URL |
Context
Currently the calendar app (
compassrepo) is deployed toapp.compasscalendar.com(GCP VM) and the marketing site (compasscalendar.comrepo) is deployed towww.compasscalendar.com(Vercel). Many landing page visitors never click the CTA into the app. The goal is to consolidate ontocompasscalendar.com— the marketing site becomes the shell, the app lives atcompasscalendar.com/app, and the two domains are eliminated in favor of one.Architecture after full migration:
compasscalendar.com— Next.js on Vercel (marketing + blog, unchanged repo)compasscalendar.com/app/*— React SPA, Vercel rewrites → GCP VMcompasscalendar.com/api/*— Express backend, Vercel rewrites → GCP VMapp.compasscalendar.com— unlisted internal proxy target (Vercel rewrites point here); direct browser visits 301 →compasscalendar.com/Phase 1: Deploy app to compasscalendar.com/app
Goal: Prove it works. The app becomes accessible at
compasscalendar.com/appwhileapp.compasscalendar.comcontinues to work.1. Make the web build base path configurable
compass/packages/web/build.ts:63— Change hardcodedpublicPath: "/"to:Also inject it into the
defineobject so the router can consume it at runtime:2. Add basename to React Router
compass/packages/web/src/routers/index.tsx:31— Add abasenametocreateBrowserRouter:APP_BASE_PATHis the build-time global defined above ("/app"in production,"/"for self-hosters and dev).3. Update Nginx on the GCP VM
compass-scriptsrepo —gcp/local-vm/nginx-prod/sites-enabled/app.compasscalendar.comReplace the current root
location /with:location /app— serves the static SPA (withtry_filesfalling back to/app/index.html)location = /— 301 redirect to/app(keeps existingapp.compasscalendar.comlinks alive during transition)/apiand/socket.ioblocks stay unchanged4. Update production env vars
compass-scriptsrepo —gcp/.prod.env5. Add Vercel rewrites in compasscalendar.com repo
compasscalendar.com/next.config.mjs— Add to therewrites()return:6. Manual: Google OAuth Console
Add
https://compasscalendar.comto authorized JavaScript origins and redirect URIs (https://compasscalendar.com/auth/google/callback).7. Deploy & verify
Visit
compasscalendar.com/app— SPA loads, Google auth works, calendar is functional.Phase 2: Welcome dialog
Goal: Show a brief "what is this" dialog to unauthenticated users over the app (like tweek.so).
New component:
packages/web/src/components/WelcomeDialog/WelcomeDialog.tsxAuthModal)Wire into
packages/web/src/views/Root.tsx.Verify: Visit
compasscalendar.com/applogged out → dialog appears. Sign in → dialog disappears.Phase 3: Decommission app.compasscalendar.com
Goal: One domain. Old subdomain demoted to unlisted internal proxy target.
No DNS changes —
app.compasscalendar.comstays in DNS, Vercel rewrites from Phase 1 continue to target it unchanged.Nginx — Keep all location blocks intact (Vercel still proxies through). Optionally add a redirect for direct browser visits:
Update CTA URLs in
compasscalendar.comrepo:components/simplify-your-day-button.tsx—https://app.compasscalendar.com/?utm_...→https://compasscalendar.com/app?utm_...components/quiz/Results.tsx— same changeManual: Remove
app.compasscalendar.comfrom Google OAuth Console. Update Red Sift cert monitoring.Verify:
app.compasscalendar.com/*→ 301 tocompasscalendar.com.compasscalendar.com/apploads and auth works end-to-end.Self-hosting: No changes needed
APP_BASE_PATHdefaults to"/"when unset. Docker Compose and self-hosting docs are unaffected.Files changed
compass/packages/web/build.tspublicPath+ injectAPP_BASE_PATHglobalcompass/packages/web/src/routers/index.tsxbasename: APP_BASE_PATHto routercompass/packages/web/src/views/Root.tsxWelcomeDialogcompass/packages/web/src/components/WelcomeDialog/compass-scripts/gcp/.prod.envcompass-scripts/gcp/local-vm/nginx-prod/sites-enabled/app.compasscalendar.com/applocation +/redirect; browser-visit 301 in Phase 3compasscalendar.com/next.config.mjscompasscalendar.com/components/simplify-your-day-button.tsxcompasscalendar.com/components/quiz/Results.tsx