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 minimal render.yaml at the repo root pairs the engine with a co-located DocumentForge node, so cold-paths are loopback rather than cross-region.
# render.yaml services: - type: web name: ruleforge runtime: docker plan: starter envVars: - key: RULEFORGE_RULE_SOURCE value: df - key: RULEFORGE_DF_BASE_URL value: http://documentforge:5000 - key: RULEFORGE_ENV value: prod - key: RULEFORGE_API_KEY sync: false # set in Render dashboard - type: pserv name: documentforge runtime: docker repo: https://github.com/tailwind-retailing/documentforge disk: name: data mountPath: /data sizeGB: 1
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.