Self-hosting

Pylon runs entirely on Cloudflare: Workers (app + probes), D1 (SQLite database), Durable Objects (one scheduler per monitor + WebSocket hubs), R2 (logo uploads) and Cron Triggers. A typical community fits in the free tier.

Prerequisites

  • Node.js 22+ and pnpm
  • A Cloudflare account (npx wrangler login)

Choose a plan

Pylon runs on either Cloudflare Workers plan with the same code. Pick the column that matches your account:

Workers Free ($0)Workers Paid ($5 / month)
Monitors at 60 s intervals (D1 write budget)~60~1,000
Alert deliveryinline, one attemptCloudflare Queue, 5 retries with backoff
Password hashingPBKDF2 100k iterations (plan cap)PBKDF2 600k iterations
CPU per request10 ms30 s (probe timeouts are wall-clock, so both work)
Worker size limit3 MB gzipped (Pylon is ~1 MB)10 MB
Durable Objects, R2, cron, WebSocketsincludedincluded
Everything elseidenticalidentical

Deploy

Option A — guided (recommended)

git clone https://github.com/jackh54/Pylon && cd Pylon
pnpm install
npx wrangler login
pnpm setup

pnpm setup asks paid or free, then creates the D1 database, the R2 bucket and (Paid) the alert queue, writes the ids into wrangler.jsonc, applies migrations, builds, deploys and sets SESSION_SECRET. Non-interactive:

pnpm setup --plan paid --url https://status.example.gg --yes
pnpm setup --plan free --url https://status.example.gg --yes
pnpm setup --dry-run        # show what would happen

Option B — manual, Workers Paid

npx wrangler d1 create pylon            # paste the id into wrangler.jsonc → d1_databases[0].database_id
npx wrangler r2 bucket create pylon-uploads
npx wrangler queues create pylon-notify
pnpm db:migrate                         # applies drizzle/*.sql to the remote D1
# set "APP_URL" in wrangler.jsonc to your public URL
pnpm deploy
npx wrangler secret put SESSION_SECRET  # openssl rand -hex 32

Option C — manual, Workers Free

Same as above, minus the queue, plus two edits in wrangler.jsonc:

  1. Delete the block between // BEGIN paid-only and // END paid-only (the queues section). Alerts are then sent inline.
  2. Set "PBKDF2_ITERATIONS": "100000" — the Free plan rejects higher iteration counts.
npx wrangler d1 create pylon
npx wrangler r2 bucket create pylon-uploads
pnpm db:migrate
pnpm deploy
npx wrangler secret put SESSION_SECRET

Instance settings

Var (wrangler.jsoncvars)Meaning
INSTANCE_MODEself-hosted (written by pnpm setup): no plan limits, no pricing page, / goes straight to sign-in. hosted: plan limits, pricing, admin page.
ADMIN_EMAILSComma-separated emails that can open /app/admin to see every team and change plans.
DEFAULT_PLANHosted only: free or pro for newly created teams.
BILLING_URLHosted only: where "Upgrade" buttons link once you sell Pro.
ALLOW_SIGNUPfalse makes the instance invite-only.
CUSTOM_DOMAIN_TARGET, CUSTOM_DOMAIN_IPSHostname and IPs customers point their DNS at when you run the edge proxy. Empty = Worker Custom Domains.
EDGE_SECRET (secret)Shared secret between the edge proxy and the Worker.

After the first deploy

  • Open the URL Wrangler printed, register the first account (it becomes the owner of the first team).
  • Put that URL into APP_URL if you left it empty, and redeploy.
  • Optional secrets: STEAM_API_KEY (edge A2S checks), RESEND_API_KEY + EMAIL_FROM (email alerts and subscriber emails), DISCORD_CLIENT_ID + DISCORD_CLIENT_SECRET (Discord login).
  • Make the instance invite-only by setting ALLOW_SIGNUP to "false".

Switching plans later

Upgraded to Paid? pnpm setup --plan paid adds the queue and raises the hashing cost (existing passwords keep working; they are re-hashed on next change). Downgrading: pnpm setup --plan free. Redeploy afterwards.

Local development

cp .dev.vars.example .dev.vars        # set SESSION_SECRET
pnpm db:migrate:local
pnpm dev                              # http://localhost:5173 – runs inside workerd with local D1/DO/R2

Trigger the cron locally: curl "http://localhost:5173/cdn-cgi/handler/scheduled?cron=*/5+*+*+*+*".

Architecture

Browser ──HTTP/WS──▶ Worker (React Router SSR + API)
                        │  ├─ D1: users, orgs, monitors, pages, incidents, daily_stats
                        │  ├─ R2: uploads
                        │  └─ Cache API: public pages (30 s)
                        ├─ Durable Object "MonitorRunner" ×N (alarm every interval, SQLite history)
                        │      └─ runs probe on the edge, or waits for push/relay results
                        └─ Durable Object "LiveHub" ×pages (hibernating WebSockets)
Relay (Node) ──pull config / push results──▶ Worker

Plans, limits & costs

Workers Paid (recommended, $5/month): 50M D1 writes/month included (~1,000 monitors at 60 s), Queues for retried alert delivery, 10 MB scripts, 30 s CPU per request, higher PBKDF2 cost (PBKDF2_ITERATIONS defaults to 600000 in wrangler.jsonc). This is the configuration the repository ships with.

Workers Free: works too. Remove the queues block from wrangler.jsonc (alerts are sent inline), set PBKDF2_ITERATIONS to 100000 (the Free cap), and stay under 100k D1 writes/day (~60 monitors at 60 s).

  • Each check writes one snapshot row; a daily rollup is flushed every 5 minutes.
  • Durable Objects are SQLite-backed; raw history is retained 30 days per monitor.
  • The edge cannot send UDP or reach private IPs — use a relay.
  • Custom hostnames for many tenants: Cloudflare for SaaS (100 hostnames free, then per-hostname pricing).

Custom domains for status pages

Three options, from simplest to most flexible: Worker Custom Domains for hostnames on your own Cloudflare account, the free edge proxy in deploy/edge (Caddy on any VPS, for customer-owned domains), or Cloudflare for SaaS (100 hostnames included). See [Status pages](/docs/status-pages) for details.

Updating

git pull && pnpm install && pnpm db:migrate && pnpm deploy