GO-LIVE.md
Rama main · réplica de lectura del repositorio original.
# 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 auth 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://<subdomain>.ohiyo.gg/healthz`, and
deletes the instance afterward. Or manually: `POST /api/v1/instances`
(authenticated) → confirm a real machine boots, `/healthz` passes, and
`https://<subdomain>.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://<control-plane host>/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-app>.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 |