Operating Husaria
Read-only replicas
Read-only replicas #
HUSARIA_READ_ONLY=true runs an instance of Husaria against a Postgres read replica. Queries and subscriptions resolve normally; mutations and console writes return 503 so a load balancer can route them to the primary.
This is the standard pattern for scaling reads on a high-traffic deployment: one writer instance pointed at the primary, N reader instances pointed at standbys, traffic shaped by the read_only flag on the health endpoint.
What changes when HUSARIA_READ_ONLY=true #
GraphQL endpoint #
| Operation | Behaviour |
|---|---|
| Query | Passes through normally. RLS and the response cache work as usual. |
| Subscription | Passes through. Live queries are push-style reads against the standby. |
| Mutation | Rejected with HTTP 200 + GraphQL error: {errors:[{message:"mutations are disabled in read-only mode", extensions:{code:"read-only"}}]}. Returning 200 keeps wire-shape clients happy; the error is in the standard errors array. |
Console #
| Path | Behaviour |
|---|---|
GET /console/api/* | Passes through. |
POST/PUT/DELETE/PATCH /console/api/* | HTTP 503 + {error, read_only:true}. |
POST /console/api/graphql | Operation-type gate — admin queries resolve, only mutations get the 503. |
/api/actions/* and /v1/metadata #
| Endpoint | Behaviour |
|---|---|
/api/actions/* writes | Rejected (writes imply DB state change). |
POST /v1/metadata export_metadata | Pass-through (read). |
POST /v1/metadata reload_metadata | Pass-through (in-memory only, no DB writes). |
POST /v1/metadata replace_metadata | Rejected (writes). |
Bootstrap #
The husaria_data schema bootstrap is skipped on a read-only replica. The standby's catalog is replicated from the primary's WAL — the bootstrap CREATE SCHEMA / CREATE TABLE would fail with SQLSTATE 25006 (read-only transaction) and flood logs without changing anything. Only the primary attempts schema creation; replicas trust the replication stream.
Runtime settings #
runtimecfg.NewStore tolerates SQLSTATE 25006 on replicas. The husaria_data.runtime_settings rows are replicated from the primary, so even when the bootstrap CREATE TABLE would fail, the subsequent SELECT succeeds and the standby's in-memory cache reflects whatever overrides the primary wrote. The store also defends nil receivers — if it fails to initialise entirely, callers read it as "no override" and fall back to env defaults rather than panicking.
Health & version surface #
Both /healthz (and the readiness aliases) and /version surface read_only: true|false in their payload:
{
"status": "ok",
"read_only": true,
"version": "..."
}
A typical load balancer pattern is to route based on this flag — primary handles writes, replicas handle reads, the LB shifts traffic during a planned failover.
Deployment recipe #
Step 1. Provision a Postgres read replica #
Use whatever your platform provides — CloudNativePG, postgres-operator, AWS RDS read replica, Crunchy Bridge, plain hot-standby. The replica needs the husaria_data schema present already (replicated from the primary at first install).
Step 2. Deploy a second Husaria instance pointed at the standby #
Identical configuration to the primary except:
HUSARIA_READ_ONLY=trueHUSARIA_DB_HOST(orDATABASE_URL) pointed at the standby- A different ingress hostname / service so it's reachable independently
Step 3. Wire your load balancer #
Route mutations and console writes to the primary; route everything else to whichever instance is closer (or has spare capacity). Health-check both instances on /ready and watch the read_only flag.
Step 4. Plan for failover #
Two failover paths to think about:
- Planned failover — promote a replica to primary in Postgres; restart the corresponding Husaria with
HUSARIA_READ_ONLYunset (or=false). The bootstrap step runs idempotently. - Unplanned failover — your LB needs to recognise that the previous primary is down. The
read_onlyflag in/readyis your routing signal.
Open mode + read-only replica #
These flags are independent. A read-only replica can run in either OPEN MODE (no HUSARIA_ADMIN_SECRET) or SECURED MODE. Open-mode replicas inherit the "every caller is admin" semantics for reads only — mutations are blocked by the read-only middleware regardless.
That said: never expose an open-mode instance to untrusted networks, replica or primary. The read-only middleware blocks state-changing operations against the DB, but anyone hitting the open replica can still read every row in every table.
Configuration #
export HUSARIA_READ_ONLY=true
export HUSARIA_DB_HOST=postgres-replica.databases.svc.cluster.local
export HUSARIA_ADMIN_SECRET=admin-secret-key-change-me
export ENVIRONMENT=production
Everything else carries over from the primary's configuration. See Configuration for the full env-var surface.