> ## Documentation Index
> Fetch the complete documentation index at: https://docs.findmydata.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploy fly

# Deploy the Account portal to Fly.io

**LIVE (2026-07-16)** at **[https://account.findmydata.io](https://account.findmydata.io)** (and
`https://findmydata-account.fly.dev`) — the Account portal runs on Fly from the
**public GHCR image** in production + Clerk (idp) mode. This doc records how it was
deployed and how to redeploy.

## Provisioned (Fly account `bob@findmydata.io`)

* Fly app **`findmydata-account`**, one `shared-cpu-1x/512mb` machine in `iad`
  (`min_machines_running = 0` → scales to zero when idle).
* Persistent volume **`fmd_account_data`** (1 GB, `iad`) for the shared vendor DB.
* IPs: shared IPv4 `66.241.125.49`, dedicated IPv6.
* TLS cert **Issued** for `account.findmydata.io` (`fly certs add …`); the Cloudflare
  `account` CNAME → `findmydata-account.fly.dev` (DNS-only) is in the
  [Cloudflare runbook](cloudflare-runbook.md).
* Config: [`deploy/fly/account.fly.toml`](../deploy/fly/account.fly.toml) — runs
  `bun run --cwd packages/server account`, `FMD_ACCOUNT_ENV=production`,
  `FMD_ACCOUNT_AUTH_MODE=idp`, the Clerk public issuer + publishable key, the
  Stripe price ids, and the `/data` mount.

Verified live: portal 200 on both hostnames (TLS ok); Clerk sign-in loads; the
dev-only register route returns **501** in idp mode; boot log shows
`account_started … mode=production authMode=idp`.

## Redeploy

```bash theme={null}
fly deploy --config deploy/fly/account.fly.toml -a findmydata-account --ha=false
```

Pushing a new `ghcr.io/clarkdickens/findmydata:latest` (via the release workflow)
then redeploying picks up the new image. The Clerk instance is a *development*
instance (shows a dev banner + strict usage limits) — promote it to a Clerk
**production** instance for a clean prod domain when going GA.

## Enable Stripe billing (optional, after deploy)

Billing is OFF until the two Stripe secrets are set (the price ids are already in
the config). Set them as Fly **secrets** (never in the repo):

```bash theme={null}
fly secrets set STRIPE_SK=sk_test_… STRIPE_WH=whsec_… \
  FMD_STRIPE_SECRET_KEY_REF=STRIPE_SK FMD_STRIPE_WEBHOOK_SECRET_REF=STRIPE_WH \
  -a findmydata-account
```

Then in Stripe → Developers → Webhooks, add an endpoint at
`https://findmydata-account.fly.dev/account/v1/billing/webhook`, subscribe to
`checkout.session.completed` + `customer.subscription.deleted`, and use its
signing secret as `STRIPE_WH`.

## Plain support mirror — ENABLED

The machine-user API key **is set** as a Fly secret (`FMD_PLAIN_API_KEY_REF=PLAIN_KEY`),
so a support case opened in the portal opens a Plain thread. Only case metadata
crosses (subject, severity, opener email/name, portal case id) — never customer
content; failures never block case creation. The key is scoped to least privilege
(`customer:create`, `customer:edit`, `thread:create`) and the endpoint defaults to
`https://core-api.uk.plain.com/graphql/v1` (`FMD_PLAIN_API_URL` overrides). To
rotate: `fly secrets set PLAIN_KEY=plainApiKey_… -a findmydata-account`.

## Custom domain — DONE

`account.findmydata.io` is live: `fly certs add account.findmydata.io` (cert
**Issued**) + a Cloudflare `account` CNAME → `findmydata-account.fly.dev` (DNS-only),
recorded in the [Cloudflare runbook](cloudflare-runbook.md).

## Clerk production instance — LIVE

The portal runs on the Clerk **production** instance for `findmydata.io`. All 5
Clerk CNAMEs (`clerk`, `accounts`, `clkmail`, `clk._domainkey`, `clk2._domainkey`)
are in Cloudflare (DNS-only), Clerk verified the domain, and its Frontend API +
JWKS serve at `clerk.findmydata.io` (200). `deploy/fly/account.fly.toml` carries the
production values (`FMD_CLERK_ISSUER=https://clerk.findmydata.io`,
`FMD_CLERK_PUBLISHABLE_KEY=pk_live_…`, `FMD_CLERK_AUTHORIZED_PARTIES=https://account.findmydata.io`).

Verified live: `GET /account/v1/health` reports the `pk_live_…` key; the browser
loads `clerk.js` from `https://clerk.findmydata.io` (`window.Clerk.frontendApi =
clerk.findmydata.io`) with no dev-key banner.

**History / rollback:** because Clerk's DNS verification is asynchronous, the new
image was first deployed with a **Fly secrets override** pinning it to the Clerk
**dev** instance (secrets take precedence over `[env]`), then flipped to production
once `clerk.findmydata.io/.well-known/jwks.json` returned 200 by dropping the
override:

```bash theme={null}
fly secrets unset FMD_CLERK_ISSUER FMD_CLERK_PUBLISHABLE_KEY FMD_CLERK_AUTHORIZED_PARTIES -a findmydata-account
```

To roll back to the dev instance (e.g. during an incident), re-apply the override:

```bash theme={null}
fly secrets set \
  FMD_CLERK_ISSUER=https://funky-rabbit-54.clerk.accounts.dev \
  FMD_CLERK_PUBLISHABLE_KEY=pk_test_ZnVua3ktcmFiYml0LTU0LmNsZXJrLmFjY291bnRzLmRldiQ \
  FMD_CLERK_AUTHORIZED_PARTIES=https://account.findmydata.io,https://findmydata-account.fly.dev \
  -a findmydata-account
```

## Enable Stripe billing (optional — still OFF)

Billing stays OFF until the two Stripe secrets are set (the price ids are already in
the config):

```bash theme={null}
fly secrets set STRIPE_SK=sk_test_… STRIPE_WH=whsec_… \
  FMD_STRIPE_SECRET_KEY_REF=STRIPE_SK FMD_STRIPE_WEBHOOK_SECRET_REF=STRIPE_WH \
  -a findmydata-account
```

Then in Stripe → Developers → Webhooks add an endpoint at
`https://account.findmydata.io/account/v1/billing/webhook`, subscribe to
`checkout.session.completed` + `customer.subscription.deleted`, and use its signing
secret as `STRIPE_WH`.
