---
phase: 05-deploy
plan: "02"
subsystem: deploy
tags: [fly, fly-toml, region-lax, persistent-volume, http-checks, otel-flycast, staging-prod-split]
dependency_graph:
  requires: []
  provides: [fly.staging.toml, fly.prod.toml]
  affects: [05-04-lint-deploy-stack, 05-05-deploy-staging-ci, 05-06-deploy-prod-ci]
tech_stack:
  added: []
  patterns: [fly-toml, flycast-6pn, persistent-volume, auto_stop-off]
key_files:
  created:
    - apps/server/fly.staging.toml
    - apps/server/fly.prod.toml
  modified: []
decisions:
  - "Two separate TOML files (staging/prod) rather than single file with env overrides — cleaner diff, lint targets each file independently, eliminates risk of prod accidentally inheriting staging keys"
  - "shared-cpu-2x@2gb chosen for argon2id memoryCost headroom (A9 risk acknowledged — bench on first staging deploy)"
  - "auto_stop_machines=off + min_machines_running=1 hard-locked on both envs to prevent WS reconnect storm on cold-start"
  - "OTLP endpoint via flycast 6PN (rebno-obs.flycast:5080/api/default) keeps telemetry traffic private"
metrics:
  duration: "~5 minutes"
  completed: "2026-05-08"
  tasks_completed: 2
  tasks_total: 2
  files_created: 2
  files_modified: 0
---

# Phase 05 Plan 02: Fly.io Per-Env Config Files Summary

Two Fly.io machine configuration files deployed for staging and production environments. Both files carry region, VM size, env vars, Fly Volume mount, and HTTP health check consuming the Phase 4 `/health` endpoint. The single security-critical distinction is `STAGING_MODE = "1"` present only in `fly.staging.toml` and absent entirely from `fly.prod.toml`.

## Tasks Completed

| Task | Name | Commit | Files |
|------|------|--------|-------|
| 1 | fly.staging.toml — staging config | 8cec1cb | apps/server/fly.staging.toml |
| 2 | fly.prod.toml — production config | b395650 | apps/server/fly.prod.toml |

## Per-Env Diff Summary

The files are identical except for these keys:

| Key | fly.staging.toml | fly.prod.toml |
|-----|-----------------|---------------|
| `app` | `rebno-staging` | `rebno-prod` |
| `STAGING_MODE` | `"1"` | **absent** |
| `LOG_LEVEL` | `"debug"` | `"info"` |
| `ALLOWED_ORIGINS` | `https://staging.rebno.decidel.com` | `https://rebno.decidel.com` |
| `OTEL_RESOURCE_ATTRIBUTES` | `deployment.environment=staging` | `deployment.environment=production` |

All other keys (volume name, VM size, health check, auto_stop, port config, OTEL endpoint host) are identical.

## VM Size Choice — A9 Risk Flag

Both files use `size = "shared-cpu-2x"` with `memory = "2gb"`. This is the minimum viable size for argon2id with `memoryCost = 65536` (64 MB). First staging deploy should run an argon2 benchmark and confirm p99 hashing latency is acceptable on this machine class. Upgrade to `performance-2x` if auth latency is unacceptable.

## Security Posture

| Threat | Mitigation in these files |
|--------|--------------------------|
| T-DEP-PROD-STAGING-LEAK | `STAGING_MODE` completely absent from fly.prod.toml — acceptance criteria negative assertion verified at creation; Plan 04 lint-deploy-stack.mjs will re-assert on every run |
| T-DEP-AUTOSTOP | `auto_stop_machines = "off"` + `min_machines_running = 1` on both files |
| T-DEP-OTLP-PUBLIC | Endpoint host is `rebno-obs.flycast` (6PN private); never a `.fly.dev` public domain |
| T-DEP-OTLP-404 | Path includes trailing `/api/default` so OTLP SDK appends signal-type suffix correctly |
| T-DEP-VOL-MISMATCH | Both files mount `rebno_data` at `/data` |
| T-DEP-PROD-DEBUG | `LOG_LEVEL = "info"` hard-coded in fly.prod.toml |

## Notes for Plan 04 (lint-deploy-stack.mjs)

The lint script must assert these invariants on both files:

**Positive (both files):**
- `auto_stop_machines = "off"` present
- `min_machines_running = 1` present
- `source = "rebno_data"` and `destination = "/data"` present
- `path = "/health"` and `interval = "10s"` present
- OTLP endpoint host matches `rebno-obs.flycast` (not `*.fly.dev`)
- OTLP endpoint path ends with `/api/default`

**Negative (fly.prod.toml only):**
- `STAGING_MODE` substring must be absent
- `LOG_LEVEL = "debug"` must be absent
- `staging.rebno.decidel.com` must be absent
- `deployment.environment=staging` must be absent

## Notes for Plan 05/06 (CI deploy workflows)

- Plan 05 (deploy-staging.yml): run `flyctl config validate -c apps/server/fly.staging.toml` as a pre-deploy step
- Plan 06 (deploy-prod.yml): run `flyctl config validate -c apps/server/fly.prod.toml` as a pre-deploy step
- Both workflows use `flyctl deploy --image <sha>` — the `[build]` block in each TOML is intentionally empty

## Deviations from Plan

None — plan executed exactly as written.

## Self-Check: PASSED

Files verified:
- apps/server/fly.staging.toml — EXISTS, all invariants confirmed by node verify script
- apps/server/fly.prod.toml — EXISTS, all invariants confirmed (including STAGING_MODE absence)
- Commits 8cec1cb (staging) and b395650 (prod) exist in git log
