Skip to content

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.

TierWho it’s forDNS controlTLS
1 — Managed subdomainUsers without their own domainWe control itCloudflare (automatic)
2 — Custom domain via CFUsers with their own domainUser adds a CNAMECloudflare (automatic)
3 — Self-hosted CaddySelf-hosted on your own serverUser adds an A recordLet’s Encrypt (on-demand)

project.celestialintelligence.com — you get a subdomain, we handle everything.

Terminal window
sf run provision-hosting --input tier=1 --input project=myproject

What happens:

  1. A Cloudflare for SaaS custom hostname is created for myproject.celestialintelligence.com
  2. Because we control the DNS zone, the subdomain resolves immediately
  3. Cloudflare issues a TLS certificate automatically (HTTP DCV)
  4. A starter index.html is dropped in ~/.starsystem/hosting/sites/myproject.celestialintelligence.com/
  5. The site is live

Output:

✅ Hostname registered: myproject.celestialintelligence.com
Your project URL: https://myproject.celestialintelligence.com

To 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.

Terminal window
sf run provision-hosting --input tier=2 --input domain=mycompany.com

What happens:

  1. A Cloudflare for SaaS custom hostname is created for mycompany.com on our zone
  2. The user is given a CNAME record to add at their registrar
  3. Once DNS propagates, Cloudflare issues TLS automatically
  4. A starter index.html is 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_key in vault — scoped to Zone.Custom Hostnames + SSL
  • secrets.CF_ZONE_ID — zone ID for celestialintelligence.com
  • secrets.CF_PAGES_FALLBACK — your Pages project subdomain (e.g. celestial-marketing.pages.dev)

Full self-hosting on your own server. Caddy acts as a reverse proxy with on-demand TLS.

Terminal window
sf run provision-hosting --input tier=3 --input domain=mycompany.com

What happens:

  1. mycompany.com is added to ~/.starsystem/hosting/allowed-domains.json
  2. A Caddyfile is generated from the template (first run only)
  3. The domain-check server is started if not already running
  4. Caddy is reloaded (caddy reload) to pick up the new domain
  5. 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.com

Prerequisites:

  • Caddy installed: brew install caddy
  • hosting.acme_email in vault — Let’s Encrypt registration email
  • Ports 80 and 443 open on the machine
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 HTTPS

After 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.

The generated Caddyfile at ~/.starsystem/hosting/Caddyfile:

{
on_demand_tls {
ask http://localhost:2020/hosting/check-domain
interval 2m
burst 5
}
}
: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.

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: 2020

ss 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.

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.html

Drop any static files there — HTML, CSS, JS, images. The SPA fallback (try_files {path} /index.html) means single-page apps work without extra config.