forked from molecule-ai/molecule-core
Single-container tenant architecture: Go platform (:8080) + Canvas Node.js (:3000) in one Fly machine, with Go's NoRoute handler reverse- proxying non-API routes to the canvas. Browser only talks to :8080. Changes: platform/Dockerfile.tenant — multi-stage build (Go + Node + runtime). Bakes workspace-configs-templates/ + org-templates/ into the image. Build context: repo root. platform/entrypoint-tenant.sh — starts both processes, kills both if either exits. Fly health check on :8080 covers the Go binary; canvas health is implicit (proxy returns 502 if canvas is down). platform/internal/router/canvas_proxy.go — httputil.ReverseProxy that forwards unmatched routes to CANVAS_PROXY_URL (http://localhost:3000). Activated by NoRoute when CANVAS_PROXY_URL env is set. platform/internal/router/router.go — wire NoRoute → canvasProxy when CANVAS_PROXY_URL is present; no-op otherwise (local dev unchanged). platform/internal/middleware/securityheaders.go — relaxed CSP to allow Next.js inline scripts/styles/eval + WebSocket + data: URIs. The strict `default-src 'self'` was blocking all canvas rendering. canvas/src/lib/api.ts — changed `||` to `??` for NEXT_PUBLIC_PLATFORM_URL so empty string means "same-origin" (combined image) instead of falling back to localhost:8080. canvas/src/components/tabs/TerminalTab.tsx — same `??` fix for WS URL. Verified: tenant machine boots, canvas renders, 8 runtime templates + 4 org templates visible, API routes work through the same port. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
46 lines
1.7 KiB
TypeScript
46 lines
1.7 KiB
TypeScript
import { getTenantSlug } from "./tenant";
|
|
|
|
// When NEXT_PUBLIC_PLATFORM_URL is set to "" (empty string), the canvas
|
|
// uses relative paths — correct for the combined tenant image where Go
|
|
// platform + canvas run on the same port via reverse proxy. The `??`
|
|
// operator preserves "" as a valid value; `||` would fall through to
|
|
// the localhost default.
|
|
export const PLATFORM_URL =
|
|
process.env.NEXT_PUBLIC_PLATFORM_URL ?? "http://localhost:8080";
|
|
|
|
async function request<T>(
|
|
method: string,
|
|
path: string,
|
|
body?: unknown
|
|
): Promise<T> {
|
|
// SaaS cross-origin shape:
|
|
// - X-Molecule-Org-Slug: derived from window.location.hostname by
|
|
// getTenantSlug(). Control plane uses it for fly-replay routing.
|
|
// Empty on localhost / non-tenant hosts — safe to omit.
|
|
// - credentials:"include": sends the session cookie cross-origin.
|
|
// Cookie's Domain=.moleculesai.app attribute + cp's CORS allow this.
|
|
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
const slug = getTenantSlug();
|
|
if (slug) headers["X-Molecule-Org-Slug"] = slug;
|
|
|
|
const res = await fetch(`${PLATFORM_URL}${path}`, {
|
|
method,
|
|
headers,
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
credentials: "include",
|
|
});
|
|
if (!res.ok) {
|
|
const text = await res.text();
|
|
throw new Error(`API ${method} ${path}: ${res.status} ${text}`);
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
export const api = {
|
|
get: <T>(path: string) => request<T>("GET", path),
|
|
post: <T>(path: string, body?: unknown) => request<T>("POST", path, body),
|
|
patch: <T>(path: string, body?: unknown) => request<T>("PATCH", path, body),
|
|
put: <T>(path: string, body?: unknown) => request<T>("PUT", path, body),
|
|
del: <T>(path: string) => request<T>("DELETE", path),
|
|
};
|