Running RFX
Deployment
RFX runs on a server behind a Cloudflare tunnel: no port is opened on the host, Cloudflare provides HTTPS. docker-compose.prod.yml runs RFX, Postgres, the backup job, the tunnel and the demo's mock systems.
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
Database
Postgres is the system of record, and the only database. RFX keeps a working copy in memory, loaded at start, and writes back only the rows that changed: soon after each change, and on shutdown. One RFX process writes to a database.
Row-level security is on for every table and binds RFX's own role too: each query says on whose behalf it runs, and one that does not gets nothing and can write nothing. When a company reads its activity log, Postgres itself returns that company's rows only. To query as rfx_app by hand, open a transaction and run SELECT set_config('rfx.scope', 'system', true) first; the superuser (backups, restores) is not limited.
Encryption at rest
With RFX_DATA_KEY set (32 random bytes, base64; required on an https address), RFX encrypts with AES-256-GCM what it writes, before it leaves the process: the content of every database row, the activity log's company, agent, identity, address and arguments, the signed documents in R2 and vendors' files on disk. Postgres keeps only structure (collection names, sequence numbers, kinds, states, counts), so the database, its backups and the storage bucket hold nothing readable without the key. Each value is bound to its table and column, each document to its path: moved elsewhere, it does not open. Only the RFX container gets the key.
- Without the key, or with a wrong one, RFX refuses to start and writes nothing.
- Data from before encryption is encrypted at the first start with the key.
- To change the key: the new one in
RFX_DATA_KEY, the old one inRFX_DATA_KEY_OLD, restart. RFX encrypts everything again with the new one. - Keep a copy of the key away from the server, with
BACKUP_ENCRYPTION_KEY: without it neither the database nor its backups can be read. The restore drill checks the restored rows open with the key in.env.
Backups and point-in-time recovery
- The write-ahead log is archived continuously, at most 60 seconds behind, each file compressed: on a quiet database a minute's 16 MB file holds a few hundred bytes and shrinks to some kilobytes.
- A base backup and a SQL dump every night at 02:00 UTC; 14 days kept.
- An encrypted copy of all of it goes off the server to Cloudflare R2 every 5 minutes and after each nightly backup. File names and contents are encrypted before they leave; the key is kept outside the server too.
deploy/pitr-restore.sh list # what can be restored
deploy/pitr-restore.sh 2026-11-14T10:30:00Z # the database as it was then (UTC)
deploy/pitr-restore.sh latest # replay everything archived
The current data is kept beside the restored one, never deleted. If the server itself is lost, start the stack on a new one with the same .env (it must hold the same BACKUP_ENCRYPTION_KEY) and bring the backups back from R2 before restoring:
docker compose -f docker-compose.yml -f docker-compose.prod.yml exec backup rfx-offsite list
docker compose -f docker-compose.yml -f docker-compose.prod.yml exec backup rfx-offsite fetch
The monitor
monitor.rfx.market (or /monitor), for the operator with the admin key: uptime and memory; requests, refusals, server errors and response time for the last 24 hours, by hour and by route; the latest unexpected errors; the database (size, last write, write-ahead-log archiving); the nightly backup and the off-site copy, as the backup job reports them; mail, e-signature, the listings feed, payments; language-model calls and tokens by day and purpose; and the marketplace (listings, companies, introductions, NDAs, deals, fees due and paid). It refreshes every 15 seconds. Counts restart with the process; the activity log keeps the record.
The graph
/graph: the marketplace as a graph. The operator, with the admin key, sees every company, agent, person, product, need and step and how they connect; a person sees their own company's part, with other companies by name only. A flat view to explore (find, families, a list of every node) and a 3D view. The buying side finds vendors from it, by name, product, category or capability, and adds them to its graph: the public listing for every vendor, and which vendors claim a capability by name; whether they support it once the NDA with that vendor is signed (a dashed line until then). Finding starts no introduction and no fee. Colour shows the family and shape the kind, in one of three palettes checked for colour blindness. Its scripts are served from /js (public/js, licences in LICENSES.txt), never from another site.
Error reporting and alerts
With RFX_ALERT_EMAIL set, unexpected errors and problems with the database or the backups are mailed there through the mail service, at no cost: the same problem at most once an hour, at most 12 alerts an hour in all. RFX checks the backups itself every 10 minutes (a late or failed off-site copy, a nightly backup older than a day, failing archiving or database writes).
With SENTRY_DSN set, unexpected errors go to Sentry with their stack, and nothing about the request (no body, cookies, headers or addresses). With SENTRY_CRON_BACKUP and SENTRY_CRON_OFFSITE set to Sentry Crons check-in URLs, Sentry raises an alert when a nightly backup or an off-site copy fails or does not happen. A Sentry uptime monitor on the public address covers the site being down.
Logs
Each request is one JSON line on standard output (time, method, path, status, duration, caller); errors carry their code. docker compose logs rfx-core shows them.