Ctrl K
ComunidadGuías…

fly.staging.toml

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Fly.io configuration for the STAGING API + WebSocket server (pqp-api-staging).
#
# Staging scales to zero when idle: auto_stop_machines = "stop",
# auto_start_machines = true, min_machines_running = 0. The price of the trade
# is only that the first request after an idle period takes a few seconds while
# the machine boots; nobody on staging is mid-call. (This file started as a
# copy of the production fly.toml, which was deleted on 2026-10-22 when
# production's Fly rollback was retired: production runs on the Vultr box,
# tools/api-host/. This is now the only Fly config for the API in the repo, and
# the one a self-host on Fly would start from; docs/deploy-fly.md.)
#
# Deployed by .github/workflows/deploy-staging.yml (push to the `staging`
# branch, or workflow_dispatch on any ref). Operator doc: docs/STAGING.md.
#
# THE ONE THING TO KNOW BEFORE EDITING THIS FILE
# -----------------------------------------------------------------
# This server is single-process BY DESIGN. WebSocket connection state, presence,
# voice-room membership and rate-limit buckets all live in in-process Maps
# (server/src/ws/, server/src/lib/rate-limit.ts). There is no pub/sub layer.
# A SECOND MACHINE SILENTLY SPLITS THE USERBASE — even on staging that makes
# every two-person test look broken. Machine count is not expressible in this
# file; the guard rail is `--ha=false` on every deploy (CI does this).

app = "pqp-api-staging"

# Same region as production and as the pqp-db cluster that holds pqp-staging.
# A cross-region DB would make staging slower than prod for no reason.
primary_region = "gru"

# The server installs SIGTERM/SIGINT handlers that close every socket with
# code 1001 ("going away") and drain the HTTP server and pg pool before exit
# (server/src/index.ts). Give it room to finish.
kill_signal = "SIGTERM"
kill_timeout = 30

[build]
  # Same multi-stage image production builds, unchanged. The runner stage sets
  # NODE_ENV=production, runs as the `node` user and CMDs
  # `node server/dist/index.js`.
  dockerfile = "Dockerfile"

[env]
  # Non-secret configuration lives here (in git, reviewable). Everything with a
  # credential in it goes through `fly secrets set -a pqp-api-staging`. Do NOT
  # also set these as secrets: a secret of the same name shadows [env].

  # Must agree with `internal_port` below; a silent disagreement presents as
  # "deploy succeeded, health check never passes".
  PORT = "3001"

  # Exactly one proxy (fly-proxy) sits in front of this process and appends the
  # real client address to X-Forwarded-For. `clientAddress()` counts in from the
  # RIGHT of that chain, so the depth has to be right: too low and the rate
  # limiter keys on an attacker-supplied value, too high and it falls back to
  # the socket address, which behind the proxy is one shared bucket for
  # everybody. One hop = "true". Raise this only if you put another proxy
  # (e.g. Cloudflare proxied DNS) in front of Fly.
  TRUST_PROXY = "true"

  # pg pool ceiling per process. Staging shares the pqp-db cluster with
  # production's database, so keep this modest.
  PG_POOL_MAX = "10"

[http_service]
  internal_port = 3001
  force_https = true

  # STAGING DIFFERENCE. Production says "off" here because a stopped machine
  # terminates every live WebSocket; on staging that is an acceptable trade for
  # not paying for an idle machine. Clients reconnect with backoff anyway
  # (client/src/lib/realtime.ts). "stop", not "suspend": a suspended machine
  # restores in-process presence/voice Maps from a snapshot that no longer
  # matches any connected client.
  auto_stop_machines = "stop"

  # STAGING DIFFERENCE. The proxy wakes the machine on the first request after
  # an idle stop. Note this only ever restarts the one machine that exists —
  # fly-proxy never creates machines, so this is not the second-machine risk.
  auto_start_machines = true

  # STAGING DIFFERENCE. Zero: idle staging costs nothing.
  min_machines_running = 0

  processes = ["app"]

  [http_service.concurrency]
    # `connections`, NOT `requests` — every signed-in client holds one
    # WebSocket for its whole session. Same numbers as prod; the defaults
    # (soft 20 / hard 25) 503 the 26th connection on an idle app.
    type = "connections"
    soft_limit = 400
    hard_limit = 600

  # Health check. `/health` does a real `SELECT 1`, so a 200 means "process up
  # AND database reachable". Same slack timings as prod: a tight interval turns
  # a two-second Postgres blip into a failed check.
  [[http_service.checks]]
    method = "GET"
    path = "/health"
    protocol = "http"
    interval = "30s"
    timeout = "5s"
    # initDb() runs the whole of server/src/schema.sql before listen(), and the
    # first boot against the empty pqp-staging database is the slow one. Do not
    # shrink this to where a cold start looks like a failure.
    grace_period = "30s"

[[vm]]
  # Same size as prod so staging behaves like prod: signalling and chat only,
  # media never touches this process.
  size = "shared-cpu-1x"
  memory = "1gb"

[deploy]
  # rolling + max_unavailable = 1 on a single machine: replace it in place.
  # Do NOT switch to "bluegreen" or "canary" — both run a second machine
  # alongside the first, the split-brain this app cannot survive.
  strategy = "rolling"
  max_unavailable = 1

  # No release_command. Schema application is initDb() at boot
  # (server/src/db.ts reads server/src/schema.sql), which is idempotent and
  # runs before listen() — it is also what creates the whole schema in
  # pqp-staging on the very first boot; a release_command would be a second
  # machine talking to the same database for no benefit.