|
| 1 | +# Operations |
| 2 | + |
| 3 | +## Required infrastructure |
| 4 | + |
| 5 | +- **Redis Stack** (`redis/redis-stack-server:7.4.0-v0` is the pinned |
| 6 | + test image; production `compose.db.yml` uses the same tag) — provides |
| 7 | + RedisJSON ≥ 2.0, required for HEAD storage. Plain `redis:7-alpine` / |
| 8 | + `redis:8-alpine` do not bundle the JSON module. |
| 9 | +- **Node.js ≥ 22.14**, **npm ≥ 11.1**. |
| 10 | + |
| 11 | +The server checks `MODULE LIST` at startup and asserts ReJSON is loaded; |
| 12 | +on missing module the boot fails closed (process exits non-zero) rather |
| 13 | +than serving traffic with broken versioning. |
| 14 | + |
| 15 | +## Environment variables |
| 16 | + |
| 17 | +| Variable | Default | Purpose | |
| 18 | +|---|---|---| |
| 19 | +| `HOST` | `localhost` | Express bind host | |
| 20 | +| `PORT` | `8000` | Express HTTP port | |
| 21 | +| `WS_PORT` | `4444` | WebSocket relay port | |
| 22 | +| `CORS_ORIGIN` | unset | Allowed origin (omit to allow all) | |
| 23 | +| `REDIS_URL` | `redis://localhost:6379` | Redis connection | |
| 24 | +| `OWNER_SECRET` | dev placeholder | HMAC secret for soft owner cookie | |
| 25 | +| `MAX_VERSIONS_PER_DIAGRAM` | `50` | FIFO cap on version history | |
| 26 | +| `MAX_SNAPSHOT_BYTES` | `5242880` | Body parser hard limit (5 MiB) | |
| 27 | +| `MAX_DESCRIPTION_LENGTH` | `240` | Version description char limit | |
| 28 | +| `MAX_NAME_LENGTH` | `80` | Version name char limit | |
| 29 | +| `DIAGRAM_TTL_SECONDS` | `10368000` | HEAD TTL (120 d) | |
| 30 | +| `VERSION_TTL_SECONDS` | `10454400` | Version body+meta TTL (121 d) | |
| 31 | +| `AUTO_VERSION_INTERVAL_SECONDS` | `1800` | Marker TTL = auto-version cadence (30 min) | |
| 32 | + |
| 33 | +## Durability |
| 34 | + |
| 35 | +Redis is configured with: |
| 36 | +- `appendonly yes` — AOF on |
| 37 | +- `appendfsync everysec` — at most ~1s of writes can be lost on a hard |
| 38 | + crash. Applies uniformly to autosave PUTs and snapshot/restore commits; |
| 39 | + there is no per-route fsync gate. |
| 40 | + |
| 41 | +For a stricter durability story, deploy a Redis replica and set |
| 42 | +`min-replicas-to-write 1` plus `min-replicas-max-lag 10`. The application |
| 43 | +does not call `WAIT` — that command requires configured replicas to |
| 44 | +provide any guarantee, and we deliberately don't claim a guarantee that |
| 45 | +isn't real. |
| 46 | + |
| 47 | +Recommended additional ops: |
| 48 | +- RDB BGSAVE every 6 hours |
| 49 | +- Off-site backup of the `dbdata` volume nightly |
| 50 | + |
| 51 | +## Single-replica WebSocket |
| 52 | + |
| 53 | +The relay server is in-memory (`Map<diagramId, Set<WebSocket>>`). It is |
| 54 | +**not multi-replica safe** — clients on different replicas cannot see each |
| 55 | +other. Production deploys must: |
| 56 | + |
| 57 | +1. Run a single relay replica behind a sticky load balancer, or |
| 58 | +2. Move to a Redis Pub/Sub adapter (deferred; tracked separately). |
| 59 | + |
| 60 | +## Redis Functions lifecycle |
| 61 | + |
| 62 | +The server boot-loads one Lua library named `apollon` containing three |
| 63 | +functions: `commit_snapshot`, `restore_version`, and |
| 64 | +`list_versions_before`. Verify with `redis-cli FUNCTION LIST` (the library |
| 65 | +name `apollon` should appear). Idempotent across restarts; the library |
| 66 | +survives Redis restart because Redis Functions are persisted to AOF/RDB. |
| 67 | + |
| 68 | +## Migration: legacy STRING → RedisJSON |
| 69 | + |
| 70 | +Pre-existing `diagram:{<id>}` keys stored as plain JSON STRINGs (legacy |
| 71 | +format) are upgraded by: |
| 72 | + |
| 73 | +```bash |
| 74 | +npm run migrate:string-to-json --workspace=@tumaet/server |
| 75 | +``` |
| 76 | + |
| 77 | +The script: |
| 78 | +- Scans `diagram:*` |
| 79 | +- Skips any key that's already RedisJSON-typed |
| 80 | +- Reads STRING bodies, JSON.SETs them back, preserves TTL |
| 81 | +- Refuses to leave any STRING-typed `diagram:*` HEAD keys behind |
| 82 | + (boot-gate equivalent) |
| 83 | + |
| 84 | +Idempotent. Safe to re-run. |
| 85 | + |
| 86 | +## Observability |
| 87 | + |
| 88 | +The server emits JSON logs via Pino. Every version event is structured: |
| 89 | + |
| 90 | +```json |
| 91 | +{ |
| 92 | + "level": 30, |
| 93 | + "event": "version.created", |
| 94 | + "diagramId": "...", |
| 95 | + "versionId": "01JKQ...", |
| 96 | + "kind": "user", |
| 97 | + "evictedVersionIds": [], |
| 98 | + "librarySchemaVersion": "4.4.0", |
| 99 | + "requestId": "...", |
| 100 | + "msg": "version.created" |
| 101 | +} |
| 102 | +``` |
| 103 | + |
| 104 | +`x-request-id` is propagated to all error response bodies — quote it when |
| 105 | +filing support tickets. |
| 106 | + |
| 107 | +A Prometheus `/metrics` endpoint is **not** included in this release. |
| 108 | +Add `prom-client` and a `/metrics` route if a downstream SRE consumer |
| 109 | +requires it. |
| 110 | + |
| 111 | +## See also |
| 112 | + |
| 113 | +- [Version history (developer doc)](../development/versioning.md) |
| 114 | +- [Runbook](runbook.md) |
0 commit comments