Route 15 standalone domains through rSpace unified server

Add domain→module rewrite in Bun.serve fetch handler for standalone
domains (rbooks, rpubs, rchoices, rfunds, rforum, rvote, rnotes, rwork,
rcal, rtrips, rwallet, rdata, rnetwork, rtube, rmaps). Requests to
these domains get rewritten to /demo/{moduleId}/... and served by the
existing Hono module routes.

Adds Traefik labels at priority 120 for all 15 domains. Keeps rcart,
rfiles, swag, and providers on their own containers.

This retires ~25 legacy containers, freeing significant memory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-22 00:44:52 +00:00
parent 095d6e1eb9
commit d25bb3ec5e
2 changed files with 120 additions and 0 deletions

View File

@ -58,6 +58,67 @@ services:
- "traefik.http.routers.rspace-canvas.rule=HostRegexp(`{subdomain:[a-z0-9-]+}.rspace.online`) && !Host(`rspace.online`) && !Host(`www.rspace.online`) && !Host(`auth.rspace.online`)" - "traefik.http.routers.rspace-canvas.rule=HostRegexp(`{subdomain:[a-z0-9-]+}.rspace.online`) && !Host(`rspace.online`) && !Host(`www.rspace.online`) && !Host(`auth.rspace.online`)"
- "traefik.http.routers.rspace-canvas.entrypoints=web" - "traefik.http.routers.rspace-canvas.entrypoints=web"
- "traefik.http.routers.rspace-canvas.priority=100" - "traefik.http.routers.rspace-canvas.priority=100"
# ── Standalone domain routing (priority 120) ──
- "traefik.http.routers.rspace-rbooks.rule=Host(`rbooks.online`)"
- "traefik.http.routers.rspace-rbooks.entrypoints=web"
- "traefik.http.routers.rspace-rbooks.priority=120"
- "traefik.http.routers.rspace-rbooks.service=rspace-online"
- "traefik.http.routers.rspace-rpubs.rule=Host(`rpubs.online`)"
- "traefik.http.routers.rspace-rpubs.entrypoints=web"
- "traefik.http.routers.rspace-rpubs.priority=120"
- "traefik.http.routers.rspace-rpubs.service=rspace-online"
- "traefik.http.routers.rspace-rchoices.rule=Host(`rchoices.online`)"
- "traefik.http.routers.rspace-rchoices.entrypoints=web"
- "traefik.http.routers.rspace-rchoices.priority=120"
- "traefik.http.routers.rspace-rchoices.service=rspace-online"
- "traefik.http.routers.rspace-rfunds.rule=Host(`rfunds.online`)"
- "traefik.http.routers.rspace-rfunds.entrypoints=web"
- "traefik.http.routers.rspace-rfunds.priority=120"
- "traefik.http.routers.rspace-rfunds.service=rspace-online"
- "traefik.http.routers.rspace-rforum.rule=Host(`rforum.online`)"
- "traefik.http.routers.rspace-rforum.entrypoints=web"
- "traefik.http.routers.rspace-rforum.priority=120"
- "traefik.http.routers.rspace-rforum.service=rspace-online"
- "traefik.http.routers.rspace-rvote.rule=Host(`rvote.online`)"
- "traefik.http.routers.rspace-rvote.entrypoints=web"
- "traefik.http.routers.rspace-rvote.priority=120"
- "traefik.http.routers.rspace-rvote.service=rspace-online"
- "traefik.http.routers.rspace-rnotes.rule=Host(`rnotes.online`)"
- "traefik.http.routers.rspace-rnotes.entrypoints=web"
- "traefik.http.routers.rspace-rnotes.priority=120"
- "traefik.http.routers.rspace-rnotes.service=rspace-online"
- "traefik.http.routers.rspace-rwork.rule=Host(`rwork.online`)"
- "traefik.http.routers.rspace-rwork.entrypoints=web"
- "traefik.http.routers.rspace-rwork.priority=120"
- "traefik.http.routers.rspace-rwork.service=rspace-online"
- "traefik.http.routers.rspace-rcal.rule=Host(`rcal.online`)"
- "traefik.http.routers.rspace-rcal.entrypoints=web"
- "traefik.http.routers.rspace-rcal.priority=120"
- "traefik.http.routers.rspace-rcal.service=rspace-online"
- "traefik.http.routers.rspace-rtrips.rule=Host(`rtrips.online`)"
- "traefik.http.routers.rspace-rtrips.entrypoints=web"
- "traefik.http.routers.rspace-rtrips.priority=120"
- "traefik.http.routers.rspace-rtrips.service=rspace-online"
- "traefik.http.routers.rspace-rwallet.rule=Host(`rwallet.online`)"
- "traefik.http.routers.rspace-rwallet.entrypoints=web"
- "traefik.http.routers.rspace-rwallet.priority=120"
- "traefik.http.routers.rspace-rwallet.service=rspace-online"
- "traefik.http.routers.rspace-rdata.rule=Host(`rdata.online`)"
- "traefik.http.routers.rspace-rdata.entrypoints=web"
- "traefik.http.routers.rspace-rdata.priority=120"
- "traefik.http.routers.rspace-rdata.service=rspace-online"
- "traefik.http.routers.rspace-rnetwork.rule=Host(`rnetwork.online`)"
- "traefik.http.routers.rspace-rnetwork.entrypoints=web"
- "traefik.http.routers.rspace-rnetwork.priority=120"
- "traefik.http.routers.rspace-rnetwork.service=rspace-online"
- "traefik.http.routers.rspace-rtube.rule=Host(`rtube.online`)"
- "traefik.http.routers.rspace-rtube.entrypoints=web"
- "traefik.http.routers.rspace-rtube.priority=120"
- "traefik.http.routers.rspace-rtube.service=rspace-online"
- "traefik.http.routers.rspace-rmaps.rule=Host(`rmaps.online`)"
- "traefik.http.routers.rspace-rmaps.entrypoints=web"
- "traefik.http.routers.rspace-rmaps.priority=120"
- "traefik.http.routers.rspace-rmaps.service=rspace-online"
# Service configuration # Service configuration
- "traefik.http.services.rspace-online.loadbalancer.server.port=3000" - "traefik.http.services.rspace-online.loadbalancer.server.port=3000"
- "traefik.docker.network=traefik-public" - "traefik.docker.network=traefik-public"

View File

@ -420,6 +420,21 @@ async function serveStatic(path: string): Promise<Response | null> {
return null; return null;
} }
// ── Standalone domain → module lookup ──
const domainToModule = new Map<string, string>();
for (const mod of getAllModules()) {
if (mod.standaloneDomain) {
domainToModule.set(mod.standaloneDomain, mod.id);
}
}
// Domains we keep on their own containers (do NOT rewrite)
const keepStandalone = new Set([
"rcart.online",
"rfiles.online",
"swag.mycofi.earth",
"providers.mycofi.earth",
]);
// ── Bun.serve: WebSocket + fetch delegation ── // ── Bun.serve: WebSocket + fetch delegation ──
const server = Bun.serve<WSData>({ const server = Bun.serve<WSData>({
port: PORT, port: PORT,
@ -427,8 +442,52 @@ const server = Bun.serve<WSData>({
async fetch(req, server) { async fetch(req, server) {
const url = new URL(req.url); const url = new URL(req.url);
const host = req.headers.get("host"); const host = req.headers.get("host");
const hostClean = host?.split(":")[0] || "";
const subdomain = getSubdomain(host); const subdomain = getSubdomain(host);
// ── Standalone domain rewrite ──
const standaloneModuleId = domainToModule.get(hostClean);
if (standaloneModuleId && !keepStandalone.has(hostClean)) {
// WebSocket upgrade for standalone domains
if (url.pathname.startsWith("/ws/")) {
const communitySlug = url.pathname.split("/")[2];
if (communitySlug) {
const spaceConfig = await getSpaceConfig(communitySlug);
const claims = await authenticateWSUpgrade(req);
let readOnly = false;
if (spaceConfig) {
const vis = spaceConfig.visibility;
if (vis === "authenticated" || vis === "members_only") {
if (!claims) return new Response("Authentication required", { status: 401 });
} else if (vis === "public_read") {
readOnly = !claims;
}
}
const peerId = generatePeerId();
const mode = url.searchParams.get("mode") === "json" ? "json" : "automerge";
const upgraded = server.upgrade(req, {
data: { communitySlug, peerId, claims, readOnly, mode } as WSData,
});
if (upgraded) return undefined;
}
return new Response("WebSocket upgrade failed", { status: 400 });
}
// Serve static assets from dist (shell.js, shell.css, etc.)
const assetPath = url.pathname.slice(1);
if (assetPath.includes(".") && !url.pathname.startsWith("/api/")) {
const staticResponse = await serveStatic(assetPath);
if (staticResponse) return staticResponse;
}
// Rewrite: / → /demo/{moduleId}, /foo → /demo/{moduleId}/foo
const suffix = url.pathname === "/" ? "" : url.pathname;
const rewrittenPath = `/demo/${standaloneModuleId}${suffix}`;
const rewrittenUrl = new URL(rewrittenPath + url.search, `http://localhost:${PORT}`);
const rewrittenReq = new Request(rewrittenUrl, req);
return app.fetch(rewrittenReq);
}
// ── WebSocket upgrade ── // ── WebSocket upgrade ──
if (url.pathname.startsWith("/ws/")) { if (url.pathname.startsWith("/ws/")) {
const communitySlug = url.pathname.split("/")[2]; const communitySlug = url.pathname.split("/")[2];