Rebuilt from original PR #1276. All Phase 30 launch content restored: - 5 blog posts (Remote Workspaces, Chrome DevTools MCP, Container vs Remote, Secure by Design) - 4 guides (Remote Workspaces, FAQ, same-origin canvas, quickstart audio) - Community copy: Discord/Slack announcements, HN launch guide - Social copy: Phase 30 (4 X versions + LinkedIn), Chrome DevTools MCP - Sales: landing copy, battlecards, one-pager, objection handlers - Press release draft - Demos: AGENTS.md auto-gen, Cloudflare Artifacts - Audio: TTS announce, VO scripts, demo narrations - Fleet diagram, asset inventory, video production package - Roadmap brief, email drip sequence, post-push checklist Closes GH#1126
6.6 KiB
Same-Origin Canvas Fetches — the /cp/* Reverse Proxy
How Molecule AI's SaaS Canvas makes browser API calls to two backends through one origin — and why the
/cp/*proxy makes multi-tenant deployment simpler and safer.
PRs: #1095 (feat/tenant-cp-proxy-same-origin) | Status: ✅ Merged
The problem: two backends, one browser origin
Canvas (Molecule AI's browser UI) makes API calls to two distinct services:
| Service | What it does | Example endpoints |
|---|---|---|
| Tenant platform | Your Molecule workspace management | /workspaces, /approvals/pending |
| Control Plane (CP) | Org-level operations, billing, auth verification | /cp/auth/me, /cp/orgs, /cp/billing/checkout |
Before this change, Canvas had to call both services directly from the browser. That meant:
- Two separate base URLs in the browser bundle (
NEXT_PUBLIC_PLATFORM_URLfor tenant, another for CP) - CORS preflight complexity — cross-origin calls need explicit
Access-Control-Allow-*headers on the CP - Cookie domain issues — WorkOS session cookies scoped to
.moleculesai.apparen't sent to a custom tenant domain
The result was a fragile configuration that complicated tenant provisioning.
The fix: server-side split, same-origin fetches
The tenant platform now runs a /cp/* reverse proxy. Canvas makes all calls to its single NEXT_PUBLIC_PLATFORM_URL (the tenant). The tenant splits the traffic:
Browser → tenant.moleculesai.app
├── /workspaces, /approvals/pending, /channels/* → handled locally
└── /cp/* → reverse-proxied upstream to CP
The browser never knows there are two backends. No CORS, no cookie domain mismatches, no extra env vars for Canvas to configure.
Architecture at a glance
Browser (Canvas)
│
│ GET /cp/auth/me (or any /cp/* path)
▼
Tenant Platform (:8080)
│
│ Reverse proxy: forward Cookie + Authorization headers
▼
Control Plane (api.moleculesai.app)
│
│ WorkOS session cookie → verify membership
▼
Response flows back through tenant → browser
The proxy:
- Does NOT strip
CookieorAuthorizationheaders — they carry the WorkOS session cookie needed by the CP - Does rewrite the
Hostheader so CP middleware (CORS checks, cookie-domain logic) sees the CP origin, not the tenant - Does NOT strip
X-Forwarded-For— upstream uses it for audit and rate limiting
Security: fail-closed allowlist
The proxy does not forward arbitrary /cp/* paths. An explicit allowlist gates every upstream route before cookies leave the tenant:
| Allowed prefix | What Canvas uses it for |
|---|---|
/cp/auth/ |
Session verification: GET /cp/auth/me, GET /cp/auth/tenant-member |
/cp/orgs |
Org listing, provision status, export |
/cp/billing/ |
Checkout and billing portal |
/cp/templates |
Template registry reads |
/cp/legal/ |
Terms of service document (served from CP) |
Every other /cp/* path returns 404, not 403. The 404 prevents leaking which CP routes exist to an attacker probing the proxy.
Why an allowlist instead of a denylist
/cp/admin/* endpoints accept WorkOS session cookies as a valid auth tier. A tenant-authed browser user could craft a request to /cp/admin/tenants/other-slug/diagnostics — without the allowlist, the tenant would happily forward their cookie upstream. The CP would see a legitimate admin session and honor the request, turning any tenant into a lateral-movement hop. The allowlist is the structural fix.
Configuration
For SaaS tenants: No configuration needed. The control plane provisioner sets CP_UPSTREAM_URL automatically at tenant launch.
# What the provisioner sets:
CP_UPSTREAM_URL=https://api.moleculesai.app
For self-hosted / local dev: CP_UPSTREAM_URL is unset. The /cp/* proxy is never mounted. Canvas connects directly to the local platform — behaviour is unchanged.
For operators investigating: If Canvas admin pages (billing, org switcher) return 502, check that CP_UPSTREAM_URL is reachable from the tenant platform's network.
What changed in the browser bundle
Canvas's Next.js build sets one base URL:
// NEXT_PUBLIC_PLATFORM_URL = https://<tenant-slug>.moleculesai.app
const res = await fetch(`${process.env.NEXT_PUBLIC_PLATFORM_URL}/cp/auth/me`, {
credentials: 'include', // send WorkOS session cookie
});
Previously Canvas needed two separate env vars and conditional logic to choose the right base URL for each call. That conditional logic is gone — one URL, server-side routing.
AdminAuth + WorkOS session verification
The /cp/* proxy enables a related improvement: browser-based admin authentication.
Canvas runs in the browser and authenticates users via a WorkOS session cookie (scoped to .moleculesai.app). It has no bearer token — the ADMIN_TOKEN scheme is for CLI and server-to-server callers, not browser users.
AdminAuth now accepts a session-verification tier that runs before the bearer check:
- If a
Cookieheader is present andCP_UPSTREAM_URLis configured → the tenant platform callsGET /cp/auth/tenant-member?slug=<tenant-slug>upstream with the same cookie. 200 +member: true→ grant admin access. - If the upstream says no, or no cookie is present → fall through to the existing bearer-token path.
Positive verifications are cached 30 seconds (keyed by sha256(slug + cookie)), so a burst of Canvas admin-page renders doesn't hammer the CP. Negative results (invalid session) are cached 5 seconds to absorb retry bursts without fan-out. Logout and role changes propagate within that window.
For self-hosted and local dev deployments, CP_UPSTREAM_URL is unset → this feature is disabled, behaviour is unchanged.
Code references
| File | What it does |
|---|---|
workspace-server/internal/router/cp_proxy.go |
/cp/* reverse proxy + allowlist |
workspace-server/internal/middleware/session_auth.go |
WorkOS session verification + 30s cache |
workspace-server/internal/router/router.go |
Mounts proxy when CP_UPSTREAM_URL set |
canvas/src/middleware.ts |
Simplified Canvas fetch base — one URL |
What this means for you
- SaaS tenants: Canvas Just Works after provisioning. No extra env vars for browser API calls.
- Self-hosted operators: No change — your Canvas talks to your local platform as before.
- Platform contributors: If a new Canvas UI fetch needs a
/cp/*path, add it tocpProxyAllowedPrefixesincp_proxy.go. The allowlist means you must opt in — no accidental exposure.