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:
parent
095d6e1eb9
commit
d25bb3ec5e
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue