diff --git a/AGENTS.md b/AGENTS.md index 1ebbc8c..7b15e09 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -63,6 +63,15 @@ - Types in `src/types/` (shared types) or co-located - Utils in `src/lib/` or `src/server/utils.ts` +## Subdomain Routing + +This project serves four product subdomains (`nessa.freno.me`, `lineage.freno.me`, `gaze.freno.me`, `inputhalo.freno.me`) plus the personal site on `freno.me`. See `docs/subdomain-setup.md` for DNS/Vercel configuration. + +- **Route placement:** Subdomain pages live under `src/routes//*` (e.g. `src/routes/nessa/...`). The `vercel.json` host-based rewrites map each subdomain to its prefix. +- **Site context:** Use `useSite()` (SolidJS) or `getSiteFromEvent`/`getSiteFromRequest` (server) from `src/lib/site-context.ts` to detect the current site. Never host-snoop in route files — SolidStart's router can't match on host. +- **API routes:** `/api/*` is a shared pool — subdomain API requests pass through to existing routes via vercel.json pass-through rewrites (ordering matters). +- **Auth:** Host-scoped only — no cookie domain broadening. + ## Key Differences from React See `src/lib/SOLID-PATTERNS.md` for comprehensive React→Solid conversion guide. Key gotchas: - Signals must be called with `()` to read value diff --git a/docs/subdomain-setup.md b/docs/subdomain-setup.md new file mode 100644 index 0000000..550a49f --- /dev/null +++ b/docs/subdomain-setup.md @@ -0,0 +1,89 @@ +# Subdomain Setup — freno.me + +This document records the DNS and Vercel domain configuration for the four product subdomains. + +## DNS Configuration + +**DNS Provider:** Google Domains (nameservers: `ns-cloud-a1` through `ns-cloud-a4.googledomains.com`) + +Add the following CNAME records in the [Google Domains DNS console](https://domains.google.com/registrar/freno.me/dns): + +| Subdomain | Type | Target | TTL | +|---|---|---|---| +| `nessa` | CNAME | `cname.vercel-dns.com` | Automatic | +| `lineage` | CNAME | `cname.vercel-dns.com` | Automatic | +| `gaze` | CNAME | `cname.vercel-dns.com` | Automatic | +| `inputhalo` | CNAME | `cname.vercel-dns.com` | Automatic | + +**After adding records:** Wait for DNS propagation (typically minutes) and for Vercel to auto-issue SSL certificates for each subdomain. + +## Vercel Project Domains + +Add the following domains in the [Vercel project Settings → Domains](https://vercel.com/your-team/freno-dev/settings/domains): + +| Domain | Redirects to | +|---|---| +| `nessa.freno.me` | (no redirect — serves `src/routes/nessa/*` via vercel.json rewrite) | +| `lineage.freno.me` | (no redirect — serves `src/routes/lineage/*` via vercel.json rewrite) | +| `gaze.freno.me` | (no redirect — serves `src/routes/gaze/*` via vercel.json rewrite) | +| `inputhalo.freno.me` | (no redirect — serves `src/routes/inputhalo/*` via vercel.json rewrite) | + +**Do NOT set any of these as the Production Branch domain** — `freno.me` remains the production domain. + +## Rewrite Architecture (`vercel.json`) + +The rewrites are defined in `vercel.json` with **two groups, ordered precisely**: + +### Group 1: `/api/*` pass-throughs (must come first) + +```json +{ "source": "/api/(.*)", "has": [{ "type": "host", "value": "gaze.freno.me" }], "destination": "/api/$1" } +{ "source": "/api/(.*)", "has": [{ "type": "host", "value": "inputhalo.freno.me" }], "destination": "/api/$1" } +{ "source": "/api/(.*)", "has": [{ "type": "host", "value": "nessa.freno.me" }], "destination": "/api/$1" } +{ "source": "/api/(.*)", "has": [{ "type": "host", "value": "lineage.freno.me" }], "destination": "/api/$1" } +``` + +These pass API requests on subdomains straight through to the existing `/api/*` routes. This enables **dual-host Sparkle appcast support**: `gaze.freno.me/api/Gaze/appcast.xml` hits the same `src/routes/api/Gaze/appcast.xml.ts` route as `freno.me/api/Gaze/appcast.xml`. + +### Group 2: `/(.*)` catch-all rewrites + +```json +{ "source": "/(.*)", "has": [{ "type": "host", "value": "nessa.freno.me" }], "destination": "/nessa/$1" } +{ "source": "/(.*)", "has": [{ "type": "host", "value": "lineage.freno.me" }], "destination": "/lineage/$1" } +{ "source": "/(.*)", "has": [{ "type": "host", "value": "gaze.freno.me" }], "destination": "/gaze/$1" } +{ "source": "/(.*)", "has": [{ "type": "host", "value": "inputhalo.freno.me" }], "destination": "/inputhalo/$1" } +``` + +These rewrite non-API requests on each subdomain to its internal route prefix. Vercel matches top-to-bottom, so the `/api/*` rules above catch API paths first. + +## Verification Checklist + +After DNS propagation and Vercel certificate issuance: + +- [ ] `dig nessa.freno.me` returns the Vercel CNAME +- [ ] `dig lineage.freno.me` returns the Vercel CNAME +- [ ] `dig gaze.freno.me` returns the Vercel CNAME +- [ ] `dig inputhalo.freno.me` returns the Vercel CNAME +- [ ] `curl -sI https://nessa.freno.me/ | head -1` → `HTTP/2 200` +- [ ] `curl -sI https://lineage.freno.me/ | head -1` → `HTTP/2 200` +- [ ] `curl -sI https://gaze.freno.me/ | head -1` → `HTTP/2 200` +- [ ] `curl -sI https://inputhalo.freno.me/ | head -1` → `HTTP/2 200` +- [ ] `curl -sI https://freno.me/api/Gaze/appcast.xml | head -1` → `HTTP/2 200` (regression) +- [ ] `curl -sI https://freno.me/api/InputHalo/appcast.xml | head -1` → `HTTP/2 200` (regression) +- [ ] `curl -sI https://freno.me/ | head -1` → `HTTP/2 200` (regression) +- [ ] All four subdomains have valid SSL certificates (no browser warnings) + +## Auth Boundaries + +Auth remains **host-scoped** — no cookie domain broadening: + +- `freno.me` web JWT cookies: host-only on `freno.me` +- Nessa: Clerk session tokens (independent) +- Lineage: mobile JWT (independent) +- Gaze/InputHalo: no web auth + +## Notes + +- DNS records must be added at **Google Domains** (not Vercel's DNS) since freno.me uses Google's nameservers. +- Subdomains will 404 until route files exist in `src/routes//*` (content tasks 05–11). +- The `bun run build` gate is worktree-friendly; run it before deploying. diff --git a/scripts/verify-subdomains.sh b/scripts/verify-subdomains.sh new file mode 100755 index 0000000..5762126 --- /dev/null +++ b/scripts/verify-subdomains.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# Verify subdomain DNS propagation, SSL, and rewrite routing +# Run after DNS CNAMEs are added and Vercel domains are configured. + +set -euo pipefail + +SUBDOMAINS=("nessa" "lineage" "gaze" "inputhalo") +APICAST_ROUTES=("Gaze" "InputHalo") +PASS=0 +FAIL=0 + +check() { + local desc="$1" + shift + if "$@" >/dev/null 2>&1; then + echo "✓ $desc" + PASS=$((PASS + 1)) + else + echo "✗ $desc" + FAIL=$((FAIL + 1)) + fi +} + +echo "=== DNS CNAME propagation ===" +for sub in "${SUBDOMAINS[@]}"; do + cname=$(dig +short "$sub.freno.me" 2>/dev/null | grep -i "cname.vercel-dns.com" || true) + if [[ -n "$cname" ]]; then + check "$sub.freno.me CNAME → cname.vercel-dns.com" true + else + check "$sub.freno.me CNAME → cname.vercel-dns.com" false + fi +done + +echo "" +echo "=== HTTPS landing pages ===" +for sub in "${SUBDOMAINS[@]}"; do + status=$(curl -sI -o /dev/null -w "%{http_code}" "https://${sub}.freno.me/" 2>/dev/null || echo "000") + if [[ "$status" == "200" ]]; then + check "https://${sub}.freno.me/ → 200" true + else + check "https://${sub}.freno.me/ → 200 (got ${status})" false + fi +done + +echo "" +echo "=== Appcast regression (freno.me) ===" +for route in "${APICAST_ROUTES[@]}"; do + status=$(curl -sI -o /dev/null -w "%{http_code}" "https://freno.me/api/${route}/appcast.xml" 2>/dev/null || echo "000") + if [[ "$status" == "200" ]]; then + check "https://freno.me/api/${route}/appcast.xml → 200" true + else + check "https://freno.me/api/${route}/appcast.xml → 200 (got ${status})" false + fi +done + +echo "" +echo "=== Main site regression ===" +status=$(curl -sI -o /dev/null -w "%{http_code}" "https://freno.me/" 2>/dev/null || echo "000") +if [[ "$status" == "200" ]]; then + check "https://freno.me/ → 200" true +else + check "https://freno.me/ → 200 (got ${status})" false +fi + +echo "" +echo "=== SSL certificates ===" +for sub in "${SUBDOMAINS[@]}"; do + if echo | openssl s_client -connect "${sub}.freno.me:443" -servername "${sub}.freno.me" 2>/dev/null | \ + openssl x509 -noout -checkend 0 2>/dev/null | grep -q "not expired"; then + check "${sub}.freno.me SSL valid" true + else + check "${sub}.freno.me SSL valid" false + fi +done + +echo "" +echo "=== Summary: ${PASS} passed, ${FAIL} failed ===" +if [[ $FAIL -gt 0 ]]; then + exit 1 +fi