Environment variables
| Variable | Purpose | Default |
|---|---|---|
RULEFORGE_RULE_SOURCE | local or df | local |
RULEFORGE_FIXTURES_DIR | Local rule directory (when source = local) | ./fixtures/rules |
RULEFORGE_REFS_DIR | Local reference-set directory (when source = local) | ./fixtures/refs |
RULEFORGE_DF_BASE_URL | DocumentForge base URL (when source = df) | https://documentforge.onrender.com |
RULEFORGE_DF_API_KEY | DocumentForge bearer token | (required for df source) |
RULEFORGE_ENV | DF environment to read bindings from | staging |
RULEFORGE_API_KEY | Caller-side X-AERO-Key shared secret | (unset = open dev mode) |
ASPNETCORE_URLS | Listening URL(s) | http://localhost:5000 |
Docker
A multi-stage Dockerfile is the simplest path. Builds in 30 seconds on a warm cache, produces a ~110MB self-contained runtime image.
# Dockerfile FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build WORKDIR /src COPY . . RUN dotnet publish src/RuleForge.Api -c Release -o /out FROM mcr.microsoft.com/dotnet/aspnet:9.0 WORKDIR /app COPY --from=build /out ./ COPY fixtures ./fixtures EXPOSE 8080 ENV ASPNETCORE_URLS=http://+:8080 ENTRYPOINT ["dotnet", "RuleForge.Api.dll"]
Render
A render.yaml Blueprint ships at the repo root. One-click deploy gives you a Docker web service co-located with DocumentForge (oregon region) so cold-path lookups stay in-region.
One-click deploy
- In the Render dashboard: New + → Blueprint → select
tailwind-retailing/ruleforge. - Render reads
render.yamland provisions the service. - In the dashboard, populate the two
sync: falsesecrets — see the env-var table. - You'll get a
*.onrender.comURL within ~30 seconds.
What ships in the blueprint
The blueprint lists every environment variable the engine reads — it doubles as documentation. Vars marked (local mode only) are inert while RULEFORGE_RULE_SOURCE=df.
# render.yaml at the repo root services: - type: web name: ruleforge runtime: docker plan: starter region: oregon # match DocumentForge for in-region cold paths dockerfilePath: ./Dockerfile healthCheckPath: /health autoDeploy: true envVars: # 1. Rule source — `df` or `local` - key: RULEFORGE_RULE_SOURCE value: df # 2. DocumentForge connection (when source=df) - key: RULEFORGE_DF_BASE_URL value: https://documentforge.onrender.com - key: RULEFORGE_DF_API_KEY sync: false # set in dashboard - key: RULEFORGE_ENV value: staging # 3. Local file source (when source=local — inert when df) - key: RULEFORGE_FIXTURES_DIR value: /app/fixtures/rules - key: RULEFORGE_REFS_DIR value: /app/fixtures/refs # 4. API auth — caller's X-AERO-Key shared secret - key: RULEFORGE_API_KEY sync: false # set in dashboard # 5. ASP.NET Core - key: ASPNETCORE_ENVIRONMENT value: Production
First request once deployed
curl -X POST https://ruleforge.onrender.com/v1/tax/pnr \ -H 'X-AERO-Key: <your-secret>' \ -H 'content-type: application/json' \ -d '{ "orig": "LHR", "taxCode": "GB1", "pax": [ { "id": "p1", "ageCategory": "ADT" }, { "id": "p2", "ageCategory": "CHD" } ] }'
Hits the per-pax tax fixture (rule-pnr-taxes@1) shipped in the image. Once the AERO admin team has authored their own tax rules and bound them to staging, those replace the bundled fixtures automatically — no engine change needed.
Co-located DocumentForge (advanced)
For maximum cold-path performance, run a private DocumentForge alongside the engine in the same Render service. Adds a pserv service block + a persistent disk; engine points at http://documentforge:5000 over the private network. Worth it if you're at scale, overkill for a tax-team trial — cross-region public DF is already fast in steady state thanks to the per-version cache.
- type: pserv name: documentforge runtime: docker repo: https://github.com/tailwind-retailing/documentforge disk: name: data mountPath: /data sizeGB: 10
Then mirror the public DF into it once with ruleforge mirror --from https://documentforge.onrender.com --to http://documentforge:5000.
After a publish
The auto-router enumerates environment bindings once at boot. Two ways to pick up a new rule version:
- For an existing endpoint with a new version:
POST /admin/refreshdrops the source caches; the next request reads from DocumentForge. No restart. - For a brand-new endpoint: trigger a redeploy in Render (or push a no-op commit;
autoDeploy: truehandles it). Routes are registered at boot, so adding one needs the routing table to rebuild.
curl -X POST -H 'X-AERO-Key: <secret>' https://ruleforge.onrender.com/admin/refresh // → { "ok": true, "refreshedAt": "2026-04-28T09:00:00.000Z", "note": "Source caches dropped..." }
Admin endpoints
All gated by RULEFORGE_API_KEY (when configured). /health is the only bypass.
| Endpoint | Purpose |
|---|---|
GET /health | Liveness probe. Always open. Returns {ok: true} regardless of source state. |
GET /admin/bindings | Currently-bound endpoints + cache stats. Body: {bindings: [...], registeredAtBoot: [...], cache: {ruleSnapshots, refSets, refreshedAt}}. |
POST /admin/refresh | Drop in-memory caches (rule snapshots + reference sets + env bindings). Returns timestamp + note. |
Security
API auth
Setting RULEFORGE_API_KEY activates middleware that checks every request for either:
X-AERO-Key: <key>Authorization: Bearer <key>
Comparison is constant-time. Unauthenticated requests return 401 with WWW-Authenticate: AeroKey realm="aero-engine". /health is always allowed (so liveness probes don't need to ship the secret).
When the env var is unset, every request is allowed — useful for local dev and integration tests.
Network
The engine never originates outbound traffic except to DocumentForge. Lock down egress to whatever DF endpoint you've configured. No external SaaS dependencies.
Secrets
- DocumentForge bearer token — pass via
RULEFORGE_DF_API_KEY. Never bake into the image. - API key — pass via
RULEFORGE_API_KEY. Rotate by setting a new value and rolling pods.
Health checks
GET /health returns 200 {"ok": true} regardless of rule-source state. It does not probe DocumentForge — health is "the engine is up", not "every dependency is healthy". Wire DocumentForge into a separate probe if your orchestrator needs that signal.
GET /admin/bindings dumps the current binding list (auto-router state). Useful for ops sanity but, like every other endpoint, gated by the API key when configured.
Logging
Standard ASP.NET Core logging — stdout with structured fields. The auto-router logs one line per bound endpoint at boot:
info: Bound POST /v1/ancillary/bag-policy → rule-bag-policy@7 info: Bound POST /v1/ancillary/tier-bonus → rule-tier-bonus@1 info: Now listening on: http://localhost:5050
Per-request tracing is opt-in via ?debug=true on the URL or X-Debug: true on the request. Production mode skips trace allocation entirely; debug mode adds ~10× overhead but emits per-node start times, durations, ctx reads/writes and sub-rule run IDs.
Scaling
Pure horizontal scale — add pods, no coordination required. Each pod independently caches rule snapshots; on publish, restart pods (or wait the 30-second env-binding TTL) for the new version to roll out. The benchmarks show a single pod can sustain ~73K req/s on 16 cores; multi-pod scales linearly.
Co-located DocumentForge keeps the cold path ~600× faster than cross-region. If you don't want a dfdb sidecar, the next-best option is putting DF in the same VPC as the engine.