Self-hosted & managed hosting tiers
Starsystem ships a three-tier hosting model for static sites. Each tier is provisioned with a single sf run provision-hosting pipeline call and managed through starsystem.yaml.
| Tier | Who it’s for | DNS control | TLS |
|---|---|---|---|
| 1 — Managed subdomain | Users without their own domain | We control it | Cloudflare (automatic) |
| 2 — Custom domain via CF | Users with their own domain | User adds a CNAME | Cloudflare (automatic) |
| 3 — Self-hosted Caddy | Self-hosted on your own server | User adds an A record | Let’s Encrypt (on-demand) |
Tier 1 — Managed subdomain
Section titled “Tier 1 — Managed subdomain”project.celestialintelligence.com — you get a subdomain, we handle everything.
sf run provision-hosting --input tier=1 --input project=myprojectWhat happens:
- A Cloudflare for SaaS custom hostname is created for
myproject.celestialintelligence.com - Because we control the DNS zone, the subdomain resolves immediately
- Cloudflare issues a TLS certificate automatically (HTTP DCV)
- A starter
index.htmlis dropped in~/.starsystem/hosting/sites/myproject.celestialintelligence.com/ - The site is live
Output:
✅ Hostname registered: myproject.celestialintelligence.com Your project URL: https://myproject.celestialintelligence.comTo update site content, replace files in the site directory. Caddy serves them directly.
Tier 2 — Custom domain via Cloudflare for SaaS
Section titled “Tier 2 — Custom domain via Cloudflare for SaaS”The user brings their own domain (mycompany.com) and Cloudflare handles TLS.
sf run provision-hosting --input tier=2 --input domain=mycompany.comWhat happens:
- A Cloudflare for SaaS custom hostname is created for
mycompany.comon our zone - The user is given a CNAME record to add at their registrar
- Once DNS propagates, Cloudflare issues TLS automatically
- A starter
index.htmlis provisioned in the sites directory
Output:
✅ Hostname registered: mycompany.com
⚠️ ACTION REQUIRED: Add this DNS record at your registrar:
Type: CNAME Name: @ (or the subdomain you want, e.g. www) Target: celestial-marketing.pages.dev
TLS will be issued automatically once your DNS propagates (usually <5 min).The CNAME target is our Cloudflare Pages fallback origin. Cloudflare routes the request to your site files.
Prerequisites:
providers:cloudflare api_keyin vault — scoped to Zone.Custom Hostnames + SSLsecrets.CF_ZONE_ID— zone ID for celestialintelligence.comsecrets.CF_PAGES_FALLBACK— your Pages project subdomain (e.g.celestial-marketing.pages.dev)
Tier 3 — Self-hosted Caddy
Section titled “Tier 3 — Self-hosted Caddy”Full self-hosting on your own server. Caddy acts as a reverse proxy with on-demand TLS.
sf run provision-hosting --input tier=3 --input domain=mycompany.comWhat happens:
mycompany.comis added to~/.starsystem/hosting/allowed-domains.json- A
Caddyfileis generated from the template (first run only) - The domain-check server is started if not already running
- Caddy is reloaded (
caddy reload) to pick up the new domain - On the first HTTPS request, Caddy automatically requests a Let’s Encrypt certificate
Output:
✅ Domain registered: mycompany.com
⚠️ ACTION REQUIRED: Point your domain's A record to this machine's IP:
Type: A Name: @ Value: 203.0.113.42
Caddy will issue a Let's Encrypt certificate on the first request. Site files: ~/.starsystem/hosting/sites/mycompany.comPrerequisites:
- Caddy installed:
brew install caddy hosting.acme_emailin vault — Let’s Encrypt registration email- Ports 80 and 443 open on the machine
How on-demand TLS works
Section titled “How on-demand TLS works”Browser → HTTPS request for mycompany.com ↓Caddy receives request, checks its cert store (no cert yet for mycompany.com) ↓Caddy calls: GET http://localhost:2020/hosting/check-domain?domain=mycompany.com ↓domain-check-server reads allowed-domains.json → 200 OK (domain is registered) ↓Caddy requests cert from Let's Encrypt (HTTP-01 challenge, automatically answered by Caddy) ↓Cert issued, request served over HTTPSAfter the first request, the cert is cached. Subsequent requests are served immediately.
The domain-check server (scripts/caddy/domain-check-server.mjs) hot-reloads allowed-domains.json every 2 seconds, so newly registered domains become valid immediately without restarting the server.
Caddyfile template
Section titled “Caddyfile template”The generated Caddyfile at ~/.starsystem/hosting/Caddyfile:
{ on_demand_tls { ask http://localhost:2020/hosting/check-domain interval 2m burst 5 } email [email protected]}
:80 { redir https://{host}{uri} permanent}
:443 { tls { on_demand }
root * ~/.starsystem/hosting/sites/{host} file_server { hide .starsystem }
try_files {path} /index.html # SPA fallback
header { X-Frame-Options DENY X-Content-Type-Options nosniff Referrer-Policy strict-origin-when-cross-origin -Server }}Site files live at ~/.starsystem/hosting/sites/<domain>/. Drop an index.html there and Caddy serves it.
Starsystem topology
Section titled “Starsystem topology”Both Caddy components appear as services in starsystem.yaml:
caddy: type: process name: Caddy (Tier 3 TLS proxy) command: caddy run --config ~/.starsystem/hosting/Caddyfile port: 443
caddy-check-server: type: process name: Caddy domain-check server command: node scripts/caddy/domain-check-server.mjs port: 2020ss provision starts both in the right order. ss provision --status reports their health.
The Cloudflare for SaaS resource is also modelled:
cloudflare-for-saas: type: cdn name: Cloudflare for SaaS _cloudflare: resource: custom-hostnames zone: "{{ vault('secrets.CF_ZONE_ID') }}" fallback_origin: "{{ vault('secrets.CF_PAGES_FALLBACK') }}"ss provision calls the CF API to create custom hostnames when new users are provisioned via Tier 1 or Tier 2.
Site content
Section titled “Site content”All tiers use the same convention: site files live at <sites_dir>/<domain>/. The default sites dir is ~/.starsystem/hosting/sites/. Override with --input sites_dir=/path/to/sites.
~/.starsystem/hosting/sites/ myproject.celestialintelligence.com/ index.html assets/ mycompany.com/ index.htmlDrop any static files there — HTML, CSS, JS, images. The SPA fallback (try_files {path} /index.html) means single-page apps work without extra config.