chore: move lms to myclassroomio.com and use approximated for custom

domain
This commit is contained in:
Rotimi Best
2026-05-21 11:29:47 +01:00
parent c090b7b4d6
commit 61cbb258ea
14 changed files with 114 additions and 210 deletions
+8 -19
View File
@@ -2,10 +2,10 @@
Cloudflare Worker that fronts every browser-facing ClassroomIO host:
- `*.classroomio.school/*` — free-tier tenant sites
- `classroomio.school/*` — apex (301 → `classroomio.com`)
- `*.myclassroomio.com/*` — free-tier tenant sites
- `myclassroomio.com/*` — apex (301 → `classroomio.com`)
- `app.classroomio.com/*` — admin dashboard
- BYOD customer domains via Cloudflare for SaaS
- BYOD customer domains via Approximated (their proxy forwards to `cio-api.onrender.com` directly, not through this Worker)
The Worker forwards each request to one of two Render services based on path:
@@ -23,7 +23,7 @@ the API. Example: a browser request to
It preserves the original host as `X-Forwarded-Host` so:
- The dashboard's SvelteKit adapter-node reads it via `HOST_HEADER=x-forwarded-host` and `event.url.host` becomes the tenant/admin/BYOD host.
- Better Auth on the API emits session cookies without a `Domain` attribute (see `packages/db/src/auth.ts` — `crossSubDomainCookies: { enabled: false }`), so the browser scopes each session to the exact host it called. Tenants on `acme.classroomio.school`, the admin app on `app.classroomio.com`, and BYOD `learn.acme.com` each get host-only sessions.
- Better Auth on the API emits session cookies without a `Domain` attribute (see `packages/db/src/auth.ts` — `crossSubDomainCookies: { enabled: false }`), so the browser scopes each session to the exact host it called. Tenants on `acme.myclassroomio.com`, the admin app on `app.classroomio.com`, and BYOD `learn.acme.com` each get host-only sessions.
## Deploy
@@ -43,33 +43,22 @@ pnpm --filter @cio/tenant-router tail
|---|---|
| `DASHBOARD_UPSTREAM_HOST` | `.onrender.com` host of the dashboard service (no scheme) |
| `API_UPSTREAM_HOST` | `.onrender.com` host of the API service |
| `APEX_REDIRECT_TARGET` | URL to redirect `classroomio.school` apex to (default `https://classroomio.com`) |
| `APEX_REDIRECT_TARGET` | URL to redirect `myclassroomio.com` apex to (default `https://classroomio.com`) |
Update the values in `wrangler.toml` after looking them up in the Render dashboard.
## Cloudflare DNS expected setup
`classroomio.school` zone (new):
`myclassroomio.com` zone:
- `*` CNAME → any valid target, **Proxied**
- `@` CNAME → any valid target, **Proxied** (Worker serves the 301)
- `cname` CNAME → any valid target, **Proxied** — this is the value BYOD customers point their CNAME at
`classroomio.com` zone:
- `app` CNAME → **Proxied** so the route binds
- `api` CNAME → **DNS only** (server-to-server callers and webhooks bypass the Worker)
- The old wildcard `*` CNAME on `classroomio.com` should be deleted — tenant traffic now belongs on `classroomio.school`.
## Cloudflare for SaaS (BYOD)
## BYOD custom domains
On the `classroomio.school` zone:
- **SSL/TLS → Custom Hostnames**: enabled
- **Fallback Origin**: `cname.classroomio.school` — BYOD traffic lands on the Worker route and is forwarded through the same code path as `*.classroomio.school`.
The API uses these env vars (`apps/api/src/services/org/domain.ts`):
- `CLOUDFLARE_CUSTOM_HOSTNAMES_API_TOKEN` — Cloudflare API token scoped to the `classroomio.school` zone with `Zone → SSL and Certificates → Edit` + `Zone → Zone → Read`.
- `CLOUDFLARE_CUSTOM_HOSTNAMES_ZONE_ID` — Zone ID for `classroomio.school`.
- `CLOUDFLARE_CUSTOM_HOSTNAME_CNAME_TARGET=cname.classroomio.school`.
Handled by [Approximated](https://approximated.app/), not this Worker. Cloudflare for SaaS can't front a Render origin (Render itself runs on Cloudflare), so we use Approximated's dedicated edge cluster instead. See `apps/api/src/services/org/domain.ts` for the integration. Customers point an A record at the IP Approximated returns; the proxy forwards directly to `cio-api.onrender.com`.
+4 -4
View File
@@ -2,8 +2,8 @@
* @cio/tenant-router — Cloudflare Worker that fronts every browser-facing host.
*
* Routes (see wrangler.toml):
* - *.classroomio.school/* — free-tier tenant sites (acme.classroomio.school)
* - classroomio.school/* — apex (301 → classroomio.com)
* - *.myclassroomio.com/* — free-tier tenant sites (acme.myclassroomio.com)
* - myclassroomio.com/* — apex (301 → classroomio.com)
* - app.classroomio.com/* — admin dashboard
* - <BYOD>/* — customer-owned domains via Cloudflare for SaaS
*
@@ -45,8 +45,8 @@ export default {
const url = new URL(request.url);
const originalHost = request.headers.get('host') ?? url.host;
// Apex on classroomio.school → permanent redirect to marketing apex.
if (originalHost === 'classroomio.school') {
// Apex on myclassroomio.com → permanent redirect to marketing apex.
if (originalHost === 'myclassroomio.com') {
const target = new URL(env.APEX_REDIRECT_TARGET);
target.pathname = url.pathname;
target.search = url.search;
+3 -3
View File
@@ -3,12 +3,12 @@ main = "src/index.ts"
compatibility_date = "2026-05-01"
# Production routes — bind the Worker to every browser-facing host.
# Tenant subdomains and the apex live on the `classroomio.school` zone.
# Tenant subdomains and the apex live on the `myclassroomio.com` zone.
# The admin app `app.classroomio.com` is routed through the Worker so
# it can use same-origin `/api/*` paths with host-only auth cookies.
routes = [
{ pattern = "*.classroomio.school/*", zone_name = "classroomio.school" },
{ pattern = "classroomio.school/*", zone_name = "classroomio.school" },
{ pattern = "*.myclassroomio.com/*", zone_name = "myclassroomio.com" },
{ pattern = "myclassroomio.com/*", zone_name = "myclassroomio.com" },
{ pattern = "app.classroomio.com/*", zone_name = "classroomio.com" }
]