# Go live — `ohiyo.gg` > **2026-10-03:** the hosted backend moved from Fly.io to Railway > (`https://ohiyo-server-production.up.railway.app`) with a fresh database. The Fly steps > below describe the original launch; see `DEPLOY.md` ("Running on Railway") and > `LAUNCH-STATUS.md` for the current setup. You own `ohiyo.gg` (Porkbun). This is the runbook to make it *do things*: serve the landing page, and (when you're ready) power live **Instant Servers**. Steps marked **[you]** need your accounts; everything else is already in the repo. The smart move: **move DNS to Cloudflare once**, and it serves both the website (Cloudflare Pages) and the wildcard `*.ohiyo.gg` that Instant Servers needs. --- ## 1. Point DNS at Cloudflare **[you]** 1. Create a free [Cloudflare](https://dash.cloudflare.com) account → **Add a site** → `ohiyo.gg`. 2. Cloudflare shows you **two nameservers**. In **Porkbun → Domain → Authoritative Nameservers**, replace Porkbun's with Cloudflare's two. (Propagation: minutes–hours.) 3. Done — you now manage `ohiyo.gg` DNS in Cloudflare. ## 2. Ship the landing page → Cloudflare Pages **[you]** The site is static and self-contained in [`site/`](site/) (`index.html` + `site.css` + `site.js` + `assets/`). Nothing to build, and it loads nothing from other sites: the fonts are in `site/assets/fonts`. `client/test/siteLanding.test.ts` checks its links and claims. 1. Cloudflare → **Workers & Pages → Create → Pages → Connect to Git** → pick `New1Direction/ohiyo`. 2. Build settings: **Framework preset = None**, **Build command = (empty)**, **Build output directory = `site`**. 3. Deploy. You get a `*.pages.dev` URL — confirm it looks right. 4. Pages → **Custom domains → Set up a domain → `ohiyo.gg`** (and add `www` → redirect to apex if you want). Cloudflare wires the DNS automatically. ✅ `https://ohiyo.gg` is live. (To preview locally first: `cd site && python3 -m http.server`.) ## 2b. Browser sign-up client (`app.ohiyo.gg`) → Cloudflare Pages **[you]** > **Superseded.** The web app is now built from `client/Dockerfile` and deployed to Railway > with `scripts/deploy-web.sh` (see `DEPLOY.md`). The steps below are the original > Cloudflare Pages setup. Lets anyone sign up in a browser — no download. It's the same React app the desktop build wraps (verified: the e2e suite drives it in a plain browser), pointed at the live backend. The production build is verified (`ohiyo.fly.dev` baked in, CSP allows it); this is a second Cloudflare Pages project + DNS. 1. Cloudflare → **Workers & Pages → Create → Pages → Connect to Git** → `New1Direction/ohiyo`. 2. Build settings: - **Root directory** = `client` - **Framework preset** = `Vite` (or None) - **Build command** = `bun install --frozen-lockfile && bun run build` - **Build output directory** = `dist` (i.e. `client/dist`) 3. **Environment variables** → `VITE_SERVER_URL = https://ohiyo.fly.dev` (baked into the bundle at build). 4. Deploy → confirm the `*.pages.dev` URL loads the auth screen. 5. Pages → **Custom domains → `app.ohiyo.gg`** (Cloudflare wires the DNS). 6. *(Recommended, optional)* Lock CORS to the web origin: `fly secrets set CORS_ALLOWED_ORIGINS=https://app.ohiyo.gg` on the backend. Until set, the server allows any origin, so the web client already works without it. ✅ `https://app.ohiyo.gg` → browser sign-up against `ohiyo.fly.dev`. Invite links use the current origin, so invites from the web app point at `app.ohiyo.gg` (correct). Desktop app unaffected. ## 3. (When ready) Live Instant Servers **[you]** > **Off on the hosted server since 2026-10-03.** Without `FLY_API_TOKEN` the server refuses > to create Instant Servers (HTTP 503). The code is still in the repo; the steps below are > the original setup and need a Fly account. The control plane is built and tested ([`server/src/provision/`](server/src/provision/)); it just needs a cloud to talk to. It auto-switches from the in-memory fake to **Fly Machines** the moment `FLY_API_TOKEN` is present. 1. **Fly app for instances:** `fly apps create ohiyo-instances`. 2. **Push the server image** to Fly's registry so machines can boot it: ```bash # Run from the repository root (the server embeds .agents/skills/). fly auth docker docker build -f server/Dockerfile -t registry.fly.io/ohiyo-instances:latest . docker push registry.fly.io/ohiyo-instances:latest ``` 3. **Wildcard DNS** in Cloudflare: add `*.ohiyo.gg` → a `CNAME` to your Fly app's hostname (or an `A`/`AAAA` to its IPs), so `yourcrew.ohiyo.gg` resolves to a provisioned machine. (Fly's `fly certs add "*.ohiyo.gg"` issues the wildcard TLS cert.) 4. **On the control-plane host** (the main Ohiyo server), set: ```bash FLY_API_TOKEN= # `fly tokens create org` FLY_INSTANCES_APP=ohiyo-instances # FLY_APP_NAME is reserved by Fly FLY_IMAGE=registry.fly.io/ohiyo-instances:latest FLY_PRIMARY_REGION=iad # optional, defaults to iad ``` `build_state` now constructs `FlyProvisioner` instead of the fake. 5. **Smoke test:** run `scripts/instant-server-prod-smoke.sh` (defaults to `https://api.ohiyo.gg/api/v1`). It registers a temporary user, creates an authenticated instance, waits for `https://.ohiyo.gg/healthz`, and deletes the instance afterward. Or manually: `POST /api/v1/instances` (authenticated) → confirm a real machine boots, `/healthz` passes, and `https://.ohiyo.gg` answers. > Per-instance volumes are created automatically by the provisioner (3 GiB, for the SQLite > DB + uploads). Free-tier cap is 3 free instances/user (paid ones don't count), > rate-limited to 5 provisions/user/hour. ### 3b. (After 3) The paid plan — Stripe **[you]** The paid plan (about $8 per community per month) is sold only when **both** Stripe is configured and Instant Servers really run here (`FLY_API_TOKEN`). Otherwise the app shows "Paid plan unavailable" with the reason, and the server refuses checkouts (HTTP 503). **Not a launch certification:** the current hosted API is on Railway; the existing instance router uses Fly's `fly-replay`. A Fly token alone does not make Railway route community domains. Validate wildcard DNS/TLS and the real routed `/healthz` path on staging before enabling sales. Use one control-plane process per SQLite database. Follow [billing recovery and launch gates](docs/reliability-runbook.md#paid-instant-servers). Never test a real charge or deploy production as part of a local test run. 1. In Stripe, create a product "Ohiyo paid server" with a **recurring monthly price** ($8/month). Note its price id (`price_…`). 2. Add a webhook endpoint `https:///api/v1/billing/stripe/webhook` with these events: `checkout.session.completed`, `checkout.session.async_payment_succeeded`, `customer.subscription.created`, `customer.subscription.updated`, `customer.subscription.deleted`, `customer.subscription.paused`, `customer.subscription.resumed`. Note its signing secret (`whsec_…`). 3. Turn on the **customer portal** (Settings → Billing → Customer portal): allow updating the payment method, viewing invoices, and cancelling at the end of the period. Disable product/price/quantity changes. Set Stripe's failed-payment retry policy to end in `unpaid` or `canceled`; leaving a subscription `past_due` forever grants indefinite grace. Verify the exact recurring price, currency and displayed price. 4. On the control-plane host set `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `STRIPE_PRICE_ID`, and optionally `OHIYO_PAID_PRICE_LABEL` (default `$8/month`) and `OHIYO_APP_URL` (where checkout returns to, default `https://app.ohiyo.gg`). 5. Test with Stripe test-mode keys first: upgrade a server in the app, pay with `4242 4242 4242 4242`, and check that it shows **Paid · stays on** within a minute. Then cancel it in the app and check that it says when it ends. How it behaves: - The plan turns on from current Stripe subscription state, fetched after a signed webhook or by the five-minute reconciliation sweep, never from the redirect back. - Concurrent checkout requests reuse one open session. A lost response retries the persisted request under the same Stripe idempotency key. Very old uncertain requests stop for operator review rather than risk another charge. - A failed renewal (`past_due`) keeps the plan while Stripe retries; once Stripe gives up (`unpaid`/`canceled`) the server moves to the free plan. It keeps running and keeps its data either way. - Cancelling in the app or the portal ends the plan at the end of the paid period. - Deleting a server cancels any subscription (including unpaid/paused), or closes its pending checkout, before destroying its machine/volume. Account deletion does this for each server in sequence. A failure stops deletion, but earlier cancellations or server removals are not rolled back; retry is safe. No automatic refund is issued. - "Stays on" means Ohiyo never puts a paid server to sleep, and a paid server asleep at payment is woken. Sleep requests are refused while paid. Reconciliation retries failed wakeups and restarts stopped paid machines. There is no uptime SLA yet. - `PATCH /api/v1/instances/{id}/tier` (operators only) still sets a tier by hand, for comps; Stripe events don't touch servers they didn't sell. ## 4. Optional — move the API onto the brand domain **[you]** > **Superseded.** The hosted API is on Railway and `api.ohiyo.gg` has not been repointed to > it. The Fly commands below only apply if you host on Fly. Today the desktop app talks to your own self-hosted backend (e.g. `your-app.fly.dev`), set in [`client/.env.production`](client/.env.production). If you'd like `api.ohiyo.gg` instead: add a Cloudflare `CNAME api → .fly.dev`, `fly certs add api.ohiyo.gg`, set `VITE_SERVER_URL=https://api.ohiyo.gg`, and rebuild the desktop app. --- ## Status | Piece | State | |---|---| | Landing page (`site/`) | ✅ live at `https://ohiyo.gg` via GitHub Pages; `www` redirects/resolves to the same Pages host | | Browser sign-up client (`client/`) | ✅ live at `https://app.ohiyo.gg`, served from the `ohiyo-web` service on Railway (`scripts/deploy-web.sh`), pointed at the Railway backend | | Backend API | ✅ live at `https://ohiyo-server-production.up.railway.app` (Railway, fresh database since 2026-10-03). `api.ohiyo.gg` still points at the old Fly app | | Domain `ohiyo.gg` | ✅ owned. `app` points at Railway; the wildcard `*.ohiyo.gg` (and so `api`) still points at the old Fly app until it is shut down | | Instant Servers control plane | ⏸ off on the hosted server: without `FLY_API_TOKEN` it refuses to create them (HTTP 503). The code remains in the repo | | Instant Servers paid plan (Stripe) | ⏸ built, not configured: needs live Instant Servers plus the Stripe settings in §3b. Until then the app says "Paid plan unavailable" | | Old Fly apps | ⚠️ not shut down when the hosting moved (see `LAUNCH-STATUS.md`); they are the owner's to remove | | Backups | ⚠️ not verified for Railway. The Fly volume snapshots (30-day retention) were on the old Fly volume only; Litestream continuous backup remains optional/not configured | | Desktop public release | ⚠️ 0.3.0 is public for Mac (beta, not Apple verified) and Linux (untested by hand); Windows not yet |