From d25bb3ec5e212b3332f247f7363a217aad26c31f Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Sun, 22 Feb 2026 00:44:52 +0000 Subject: [PATCH] Route 15 standalone domains through rSpace unified server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docker-compose.yml | 61 ++++++++++++++++++++++++++++++++++++++++++++++ server/index.ts | 59 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 6edf913..db616da 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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.entrypoints=web" - "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 - "traefik.http.services.rspace-online.loadbalancer.server.port=3000" - "traefik.docker.network=traefik-public" diff --git a/server/index.ts b/server/index.ts index cdd9026..593089a 100644 --- a/server/index.ts +++ b/server/index.ts @@ -420,6 +420,21 @@ async function serveStatic(path: string): Promise { return null; } +// ── Standalone domain → module lookup ── +const domainToModule = new Map(); +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 ── const server = Bun.serve({ port: PORT, @@ -427,8 +442,52 @@ const server = Bun.serve({ async fetch(req, server) { const url = new URL(req.url); const host = req.headers.get("host"); + const hostClean = host?.split(":")[0] || ""; 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 ── if (url.pathname.startsWith("/ws/")) { const communitySlug = url.pathname.split("/")[2];