Running without a domain
Vambora ships without buying a custom domain. Everything runs on free
hostnames with real HTTPS. Buying vambora.app later is a config change, not
a code change.
What runs where
| Surface | Now (free) | Later (domain bought) |
|---|---|---|
| Web PWA | https://vambora-web.pages.dev (Cloudflare Pages) | https://vambora.app |
| Docs | https://vambora-docs.pages.dev (Cloudflare Pages) | https://docs.vambora.app |
| API | https://api-<vm-ip>.sslip.io (Oracle VM + Coolify) | https://api.vambora.app |
| Map tiles | MapTiler style URL (Path A) | unchanged, or Protomaps/R2 (Path B) |
Why this works
- Cloudflare Pages gives every project a
*.pages.devsubdomain with automatic HTTPS — no domain, no cert work. - sslip.io is a free public DNS service:
api-203-0-113-7.sslip.ioresolves to203.0.113.7. No signup, no account, no DNS records. Because it's a real resolvable hostname, Coolify's built-in proxy obtains a real Let's Encrypt certificate for it over HTTP-01. Browsers see valid HTTPS, so thepages.devweb app can call the API (no mixed-content, no self-signed warnings). - The backend CORS allowlist is env-driven (
CORS_ALLOW_ORIGINS), so thepages.devorigin is allowed without editing code.
sslip.io uses dashes or dots:
api-203-0-113-7.sslip.ioorapi.203.0.113.7.sslip.ioboth resolve to203.0.113.7. Use the dashed form — some TLS tooling dislikes all-numeric dotted labels.
The two settings that make it work
- Web (Cloudflare Pages env var):
NEXT_PUBLIC_API_BASE_URL=https://api-<vm-ip>.sslip.io - Backend (Coolify env var):
ENVIRONMENT=productionandCORS_ALLOW_ORIGINS=https://vambora-web.pages.dev
If the web shows data but the browser console logs CORS errors, these two are out of sync — that's the only failure mode unique to this setup.
Deploy to Cloudflare Pages (no domain, no token)
Method: the Cloudflare dashboard "Connect to Git" integration. Cloudflare
builds and redeploys both sites on every push to main. No API token, no
Account ID, no GitHub secrets.
The project is a single monorepo (matheus-of-freitas/vambora, branch
main) with web/ + docs/ + backend/. Create two Pages projects
on the same repo, each with a different Root directory. (Commit and
push the repo first — Connect-to-Git needs commits on main.)
- dash.cloudflare.com → Workers & Pages → Create → Pages → Connect to
Git → authorise GitHub → select repo
vambora. - web project:
- Project name:
vambora-web - Production branch:
main - Root directory:
web - Framework preset: Next.js
- Build command:
npx @cloudflare/next-on-pages@1 - Build output directory:
.vercel/output/static - The Next.js App Router has on-demand SSR routes
(
/lines/[shortName],/stops/[stopId]), so it deploys via Cloudflare's official adapter, not as a plain static site.web/wrangler.toml(committed) carries the requiredcompatibility_flags = ["nodejs_compat"]; if the dashboard doesn't apply it, also setnodejs_compatunder Settings → Functions → Compatibility flags for Production + Preview. - Settings → Variables and Secrets (Production + Preview):
NEXT_PUBLIC_API_BASE_URL=https://api-<vm-ip>.sslip.ioNEXT_PUBLIC_MAP_STYLE= your MapTiler style URL (https://api.maptiler.com/maps/dataviz-dark/style.json?key=KEY)NODE_VERSION=20
- The MapTiler key lives only here and in local
web/.env.local— never committed.
- Project name:
- docs project:
- Project name:
vambora-docs - Production branch:
main - Root directory:
docs - Framework preset: Docusaurus
- Build command:
pnpm build - Build output directory:
build NODE_VERSION=20. No app variables.
- Project name:
- (Optional, monorepo hygiene) On each project set Build watch paths to
web/*/docs/*so a docs-only push doesn't rebuild web and vice versa. - Push to
main→ both build → live athttps://vambora-web.pages.devandhttps://vambora-docs.pages.dev(HTTPS automatic).
What you will NOT see (and shouldn't look for)
Because there's no domain, there's no Cloudflare zone, so:
- No "DNS" app / DNS records. The per-domain DNS editor only appears inside a zone. The dashboard's "DNS Firewall", "DNS Settings", "DNS Views" are different account-level products — not relevant here.
- No "Zone" API-token permission. With zero zones there's nothing to scope it to. You don't create an API token at all for this method.
Both are expected and only become relevant in the next section.
Switching to a real domain later (no code changes)
- Buy
vambora.app(Cloudflare Registrar is cheapest, ~$14/yr.app). - Cloudflare DNS:
vambora.app/docs.vambora.app→ the two Pages projects;api.vambora.app→ anArecord to the Oracle VM public IP. - In Coolify, set the backend service domain to
https://api.vambora.app(it re-issues Let's Encrypt for the new name automatically). - Change exactly two env vars and redeploy:
- web
NEXT_PUBLIC_API_BASE_URL=https://api.vambora.app - backend
CORS_ALLOW_ORIGINS=https://vambora.app(add the pages.dev origin too during the cutover so both work in parallel).
- web
No source edits. The sslip.io URL keeps working until you remove it.