Renoxium Docs Pricing Sign in Sign up

Contract: tenant-facing error catalogue

Version0.6 (F7, #346: RENOX_TENANT_SUSPENDED). 0.5 (F7, #318: RENOX_STORAGE_QUOTA, RENOX_STORAGE_TYPE, RENOX_STORAGE_TOO_LARGE, RENOX_STORAGE_KEY, RENOX_STORAGE_NOT_FOUND, RENOX_IMAGE_RATE, RENOX_WEBHOOK_SIGNATURE, RENOX_AI_QUOTA, RENOX_AI_UNAVAILABLE, RENOX_PDF_LIMIT for ops.md 1.7.0). 0.4 (F5, #272: RENOX_REALTIME_LIMIT, RENOX_REALTIME_BACKPRESSURE, RENOX_TOOL_INPUT, RENOX_TOOL_SCOPE, RENOX_TOOL_CONFIRM, RENOX_POLICY_INVALID; RENOX_FORBIDDEN and RENOX_DB_SCOPE completed for ops.md 1.5.0). 0.3 (F6, #251). New codes are added as rows (minor). Renaming or removing a code, or changing its HTTP status, is breaking. History: "Changelog" at the bottom
Producerscrates/renox-gateway (P3), crates/renox-cell (P5), crates/renox-runtime (P1), crates/renox-data (P2); from F3 control/api (P12, deploy-api.md) and cli/ (P11)
Consumerssdk/ (P10) error classes; cli/ (P11) and the dashboard (P13), which quote these rows; the public docs; tenants' own API clients
Relatedops.md section 13 (which codes ops throw and which the platform answers with), meter.md (status), docs/23-failure-modes.md (when each failure happens)
Checkerbun run docs/contracts/validate.ts: every code used in ops.md, deploy-api.md, schemas/deploy-api.yaml and docs/23-failure-modes.md has exactly one row here, and ctx.d.ts uses exactly the codes of ops.md section 13

Everything a tenant or an end user sees when the platform fails, refuses, or stops something is one of the codes below. It is never a stack trace, a file path, a hostname or IP of ours, an SQL statement, or a secret (docs/23-failure-modes.md rule 2; docs/24-threat-model.md). This file is the superset: ops.md defines the codes that cross the op boundary, and this catalogue adds the gateway, deploy, and availability codes. It is what the dashboard, the CLI, and the public docs quote.

1. Response body

Function (API) routes

Every error response the platform produces on a function route has a JSON body:

{ "error": { "code": "RENOX_CPU_LIMIT", "request_id": "01JBQ3Z7MA0000000000000001", "message": "The request used more CPU time than its limit." } }
  • Content-Type: application/json; charset=utf-8, Cache-Control: no-store, and x-renox-request-id with the same id as in the body.
  • code is a code from section 3 whose HTTP status is not "—". message is the fixed English text from the "Message" column, never built from tenant code, request input, or an internal error. request_id lets a developer find the full detail in their logs.
  • Retry-After is set where the row says so.
  • The TypeScript shape is ErrorResponseBody in ctx.d.ts.
  • The body never contains details, a stack, or the code of a thrown error. An uncaught RENOX_DB_AUTH reaches the client as RENOX_INTERNAL; the original code goes to the tenant's log only.

A Response that tenant code returns itself (for example a 404 or a 400 built by the handler) is passed through untouched. Only platform-produced errors follow this format.

Page (HTML) routes, F2

A platform error on a page route (artifact.md section 3, "Page routes") is answered with HTML, chosen in this order:

  1. The tenant's pages/_error.html: the template with kind: "error" in manifest.templates.
  2. The platform default page, if the app has no error template, if rendering it fails, or if it hits a limit itself: a static HTML page with the HTTP status, the message, and the request id, and no tenant content. It is never rendered with tenant code or tenant templates. It is the same page the gateway already sends for its own HTML errors (413), and the page every collateral request of a recycled isolate gets (ops.md section 10).

Inputs. The error template receives exactly four variables, and nothing else:

VariableTypeValue
statusintegerThe HTTP status from section 3
codestring or nullThe code from section 3; null only for the 404 page below
request_idstringSame as x-renox-request-id
messagestringThe fixed English text from the "Message" column

No loader runs: pages/_error.ts is a build error (renox check), and the template sees no ctx, no session, no loader data of the failed request, no params, and no detail of the cause (limit values, stack, SQL). Layouts and partials may be used, so the page can look like the rest of the app.

Render budget: a fixed error-page budget, decided here (#93 proposed "a tiny fixed fuel budget", version 0 of this file said "the normal render limits"; the fixed budget wins). The error page renders exactly when a request has already failed, often under load (RENOX_CPU_LIMIT, RENOX_BUSY). With the normal limits, a heavy _error.html would double the cost of every failure, and a flood of failures would become a flood of renders. So the error template gets the same small budget on every tier, independent of what the failed request used:

LimitValue
Fuel10,000 units proposed
Output64 KiB
Include and extends depth4
Render time10 ms

Hitting any of them falls back to the platform default page, with the original status and code (never RENOX_RENDER_LIMIT), and logs one RENOX_RENDER_LIMIT line with template: "pages/_error". #93 confirms the fuel number with a test against renox-template's fuel unit (#38) and records it here as a patch. What the error render uses is added to the failed invocation's render_fuel and render_ms (meter.md).

404 page. A GET or HEAD that matches no route, on an artifact with templates, whose Accept header prefers text/html, gets the template with kind: "not_found" (pages/_404.html), with status: 404, code: null, the request id, and the message "Page not found.", under the same fixed budget. Without a _404.html it falls back to _error.html, then to the default page. A 404 like this has no meter record (no tenant code runs, meter.md section 1). A Response with status 404 returned by load is a tenant response and passes through untouched.

When the route kind is not known

The gateway can fail a request before a route is matched (RENOX_BODY_TOO_LARGE, RENOX_HOST_MISMATCH, RENOX_UNAVAILABLE, and from 0.6 RENOX_TENANT_SUSPENDED). It answers with the platform default page when the request's Accept header prefers text/html, and with the JSON body otherwise.

2. What is logged

Every row below produces one line in the tenant's log sink (ops.md section 4) unless it says otherwise. The line carries the standard fields (ts, tenant_id, deployment_hash, request_id, module, level) plus code and the fields listed in the row. These lines are what renox logs and the dashboard show the developer. Secret values are redacted from every field (ops.md section 3). Fields that would reveal our infrastructure (node names, internal addresses, file paths) go only to the platform's operator logs, never to the tenant sink.

Thrown codes (section 3, "Thrown into tenant code") are logged when they end a request uncaught, as part of the RENOX_INTERNAL line (cause_code, cause_message, redacted stack). If tenant code catches them, logging is up to the tenant. Exceptions: RENOX_FETCH_BLOCKED is always logged by the platform (docs/24-threat-model.md rule 4), and so is RENOX_DB_AUTH, because a refused table is a security-relevant event even when caught.

3. Catalogue

Columns: HTTP = status of the platform response ("—" = never a response by itself: thrown into tenant code, and 500 RENOX_INTERNAL if uncaught). Message = the fixed text in the response body and on the default page. Logged = fields added to the tenant log line. What to do = the guidance the dashboard, CLI, and docs give the developer.

Request limits and platform responses

CodePhaseHTTPWhen it happensMessage (end user)Logged (tenant developer)What to doRefers to
RENOX_CPU_LIMITF1503The request's JavaScript used more CPU time than the tier limit (50 ms free / 500 ms paid), or module evaluation exceeded its budget. The isolate is recycled, and its other in-flight requests end with this code tooThe request used more CPU time than its limit.route, cpu_ms, limit_ms, phase (request / module_load)Move heavy work out of the request path (precompute, paginate, cache in ctx.db), check for loops over unbounded input, or upgrade the tierops.md §10; docs/04-isolation-security.md "Per-tenant limits"; failure mode "Infinite loop / CPU over limit"
RENOX_OOMF1503The isolate reached its V8 heap limit (128 MB / 512 MB). The isolate is recycled, and its other in-flight requests end with this code tooThe request ran out of memory.route, peak_heap_bytes, limit_bytesStream large bodies instead of buffering them, limit query result sizes, avoid unbounded global cachesops.md §10; failure mode "Unbounded allocation"
RENOX_DEADLINEF1504The request's wall-clock deadline (30 s / 60 s) passed. Also thrown into pending ops of that requestThe request took too long.route, wall_ms, limit_ms, pending_ops (op names)Look for slow fetch targets or long database work; set timeouts on outbound calls with ctx.deadline.remainingMs()ops.md §5, §10; failure mode "Handler never resolves"
RENOX_INTERNALF1500The handler threw (including an uncaught thrown code below), returned something that is not a Response, module evaluation failed, or a route's export is missingInternal error. Quote the request id when reporting it.route, cause_code (if the cause was a RenoxError), cause_message, stack (redacted), phaseRead the logged cause and stack; catch expected errors and return your own Responseops.md §6, §11; failure mode "Handler throws"
RENOX_MODULE_EVALF2500A route module evaluated on its first request (ops 1.4.0, lazy route modules) threw while evaluating, or lacks the route's export. Only that route fails; the isolate stays up and the failure is cached until the isolate is replacedInternal error. Quote the request id when reporting it.route, module, cause_message, stack (redacted); logged once per isolateFix the module's top-level code (it now runs inside the first request, under its deadline and limits); run renox check, which refuses a missing export before deployops.md §11, §13; failure mode "Route module fails to evaluate on first use"
RENOX_BUSYF1503, Retry-After: 1All concurrency slots of the deployment (50 / 500) stayed busy until the request's deadlineThe service is busy. Try again shortly.route, concurrency_limit, queued_ms (one summary line per second while it lasts, not one per request)Find slow handlers that hold slots; raise the tier for more concurrencyops.md §10; failure modes "Too many concurrent requests", "Flood from many IPs on one tenant"
RENOX_BODY_TOO_LARGEF1413The request body exceeded the limit for the route kind (1 MB default) at the gateway, before any tenant code ranThe request body is too large.limit_bytes, received_bytes (not metered, meter.md §1)Upload large files through the storage module; raise the limit per route when availableops.md §6; failure mode "Request body over limit"
RENOX_RENDER_LIMITF2500Template rendering ran out of fuel, iterations, output size, include depth, or render timeThe page could not be rendered.template, line, limit (fuel / iterations / output / depth / time)Paginate the data passed to the template; reduce nested loopsdocs/08-template-engine.md; failure mode "Template nested loops / huge output"
RENOX_CSRFF2403A non-GET page action arrived without a valid CSRF tokenThe request could not be verified. Reload the page and try again.route, action, reason (missing / mismatch)Submit forms through the generated markup or htmx, which send the token; exempt webhook routes are automaticdocs/04-isolation-security.md "Web-layer protections"; failure mode "CSRF token missing or wrong"
RENOX_FORBIDDENF5403A Cedar policy denied the action, or nothing permitted it (fail closed): the route guard before any handler, ctx.authz.require (thrown; uncaught it still ends the request with 403, not 500), a realtime.subscribe (thrown in open; the connection closes with 1008), the tool check before run (a JSON-RPC error on /mcp), or explain outside dev modeYou do not have permission to do this.action, resource (type and id), reason (deny / unknown_action / missing_attribute / evaluation_error / eval_limit / dev_only), policy (dev only), principal (id only); an audit row in renox__authz_audit when the action is sensitiveCheck the role assignments and policies for the action; renox authz explain names the deciding policy in dev; a missing_attribute means the resource object lacks a declared [authz.entities] attributeops.md §17; docs/11-authorization.md; failure mode "Policy denies"
RENOX_REALTIME_LIMITF5503, Retry-After: 5 (at the upgrade); — on an open connectionA ws or sse upgrade over the tenant's or the user's connection cap (503, no tenant code runs); on an open connection: subscribe over the topics per connection, publish or send with a message over 64 KB (thrown), or an inbound flood (closed with 1008)Too many realtime connections. Try again shortly.reason (tenant / user / topics / message_size / flood), route; a platform close is logged with code and reasonClose idle connections on the client; subscribe to fewer topics per connection; send smaller messages; raise the tier for more connectionsops.md §18, "Limits"; failure mode "WebSocket message flood / oversized frame"
RENOX_REALTIME_BACKPRESSUREF5— (close 1008)A connection's outbound queue (256 messages) was full: the fan-out closed the slow consumer with 1008, or send to it rejected (thrown)— (the client sees close code 1008 and reconnects; Last-Event-ID replays the recent events on SSE)conn, route, queuedNothing in the handler: a slow client is closed by design; reduce the message rate per topic, or coalesce updates into fewer, larger fragmentsops.md §18, "Limits"; failure mode "WebSocket message flood / oversized frame"
RENOX_HOST_MISMATCHF2421The TLS SNI name and the Host (or :authority) both resolve in the route table, to different tenants. Refused before routing; no cell is touchedThe request was sent to the wrong host.Not logged to any tenant sink, since the two names belong to different tenants. The operator log records both host namesNothing in the app. Clients must send the same name in SNI and Host; browsers retry a 421 on a new connection by themselvesartifact.md §5; docs/24-threat-model.md "Host/SNI confusion"
RENOX_TENANT_SUSPENDEDF7503, Retry-After: 3600The hostname's route entry is suspended (artifact.md 1.5 section 5): the tenant is suspended for billing or abuse (deploy-api.md 0.5.1 sections 5, 20, 22). The gateway answers before routing with the platform's neutral page, which shows the tenant's display_name and nothing else; no cell is touchedThis service is temporarily unavailable.Not logged to the tenant sink (no request reaches the tenant); the operator log counts it per tenantThe owner pays the open invoice, upgrades, or answers the abuse ticket; the dashboard shows whyartifact.md §5, deploy-api.md §5
RENOX_UNAVAILABLEF3503, Retry-After: 5The worker cannot load the deployment: artifact not cached and object storage or the control plane is unreachable, or the cell is marked unhealthy. In the deploy API: no healthy node can take an activation, or Postgres or object storage is unreachableThe service is temporarily unavailable.reason (artifact_unavailable / cell_unhealthy); deploy API: the deployment's last_error.stage = no_workerNothing in the app; the platform retries and alerts. Check the status page; retry the activationfailure modes "Control plane unreachable from the worker", "Object storage unreachable"; deploy-api.md §6

Thrown into tenant code

CodePhaseHTTPWhen it happensMessage (end user)Logged (tenant developer)What to doRefers to
RENOX_FETCH_BLOCKEDF1—fetch to a host that is not on the allowlist, with the wrong scheme or port, resolving to a blocked address, over 5 redirects, or with a forbidden method—Always logged: host, reason (never the resolved IP)Add the host to [fetch] allow in renox.toml; private and metadata addresses cannot be allowedops.md §2; docs/24-threat-model.md SSRF rows; failure mode "fetch to blocked destination"
RENOX_FETCH_FAILEDF1—fetch failed for a network reason: DNS, connect, TLS, reset, redirect mode error, too many concurrent fetches, or too many headers—When uncaught: host, reasonRetry with backoff for transient reasons; check the remote certificate for tlsops.md §2
RENOX_FETCH_EGRESS_LIMITF1—Outbound bytes of the request (sent + received over all fetches) passed 10 MB free / 100 MB paid; the stream is aborted—When uncaught: host, limit_bytesRequest smaller ranges or pages from the remote API; stream instead of downloading whole filesops.md §2; failure mode "fetch response too large / too slow"
RENOX_DB_AUTHF1—The SQLite authorizer refused a table, view, ATTACH, PRAGMA, or function outside the module's prefix—Always logged: table, action, moduleUse only your module's <module>__* tables and declared <dep>__public_* viewsops.md §1; docs/06-data.md; failure mode "SQL touching another module's table"
RENOX_DB_BUSYF1—The write lock stayed taken for the busy wait (2 s, or the remaining deadline if shorter)—When uncaught: wait_msKeep write transactions short; do not fetch inside db.transactionops.md §1
RENOX_DB_TIMEOUTF1—A statement ran longer than 1 s (or the remaining deadline); the statement is interrupted and its transaction rolled back—When uncaught: elapsed_ms (the SQL text is not logged)Add an index, add limit, avoid unbounded WITH RECURSIVEops.md §1; failure mode "Runaway query"
RENOX_DB_QUOTAF1—The tenant file reached its size quota (100 MB free / 10 GB paid); writes fail, reads continue—When uncaught: quota_bytes; also a dashboard warningDelete old data or upgrade the tierops.md §1; failure modes "Database file over quota", "Disk full on node"
RENOX_DB_CONSTRAINTF1—A unique, foreign key, not-null, check, or primary-key constraint failed—When uncaught: kindExpected in normal operation: catch it and return a 409 or 400ops.md §1
RENOX_DB_SQLF1—Syntax error, unknown table or column, or another SQLITE_ERROR—When uncaught: SQLite's message (no file path)Fix the statement; run renox check, which lints SQLops.md §1
RENOX_DB_PARAMF1—A parameter has an unsupported type or value (undefined, NaN, object, bigint outside i64, lone surrogate), the count does not match, named parameters were used, or the SQL text or parameter count is over its limit—When uncaught: index of the parameter, reasonPass null instead of undefined; use positional ? parametersops.md §1, §12
RENOX_DB_COLUMN_NAMEF1—Two result columns have the same name—When uncaught: columnAlias the columns (select a.id as a_id, b.id as b_id)ops.md §1
RENOX_DB_MULTI_STATEMENTF1—More than one statement was passed in one call—When uncaughtRun one statement per call; group them in db.transactionops.md §1
RENOX_DB_RESULT_LIMITF1—A query returned more than 10,000 rows or 8 MB—When uncaught: limit (rows / bytes)Paginate with limit and a cursorops.md §1
RENOX_DB_TX_STATEF1—A transaction was begun while one was already open on the request, or a handle was used after commit or rollback—When uncaughtDo not nest db.transaction; savepoints arrive in F4ops.md §1
RENOX_DB_UNAVAILABLEF6503, Retry-After: 30The tenant's SQLite file failed its integrity check on open or is being restoredThe service is temporarily unavailable.reason (integrity / restoring)Nothing in the app; the platform restores from the replica and alertsfailure mode "SQLite file corrupted, WAL unreadable"
RENOX_DB_SCOPEF5—Module code ran a statement on an owned table ([authz.ownership]) without db.scoped(), a scoped statement lacked the value the table needs, an INSERT set a different owner, or the rewriter could not prove the statement (an owned table in a subquery or CTE, INSERT … SELECT from an owned table)—Always logged, like RENOX_DB_AUTH: table, module, reason (unscoped / scope_missing / owner_mismatch / unprovable); never the SQLUse db.scoped({ user_id, org_id }) for owned tables; pass the scope the table's owner and org columns need; keep owned tables out of subqueries, or read them in a separate statementops.md §17, "db.scoped()"; docs/11-authorization.md; failure mode "Unscoped query on an owned table"
RENOX_NO_REQUESTF1—An op was called outside a live request: at module top level, or from a timer or promise that outlived its request—When uncaught at top level: as RENOX_INTERNAL with phase: module_load; later: one line with opDo I/O inside handlers; do not keep work running after the responseops.md §0
RENOX_SECRETS_CONSUMEDF1—op_secrets_get_all was called a second time. Unreachable from tenant code through the public API—opRead secrets from ctx.secretsops.md §3
RENOX_MODULE_UNDECLAREDF4—Module code called a service of a module it does not list in [dependencies], ran a hook point it does not export, or emitted an event it does not export—Always logged: module (the caller), target (the module, hook, or event)Add the dependency (and the [imports] entry) to module.toml, then renox install; renox check finds it before deployops.md §15.2–15.4; manifest.md MF7; docs/24-threat-model.md "Module calling a service or hook it did not declare"
RENOX_MODULE_SERVICEF4—A service, hook filler, or in-memory event handler of another module threw; the enclosing transaction was rolled back—When uncaught: module, service / hook / event, cause_code; the callee's own error is logged with the callee's moduleRead the callee module's log line for the cause; catch it to answer the user, knowing the transaction is goneops.md §15.2; failure mode "Module service throws"
RENOX_EVENT_PAYLOADF4—ctx.events.emit got a payload that does not match the event's exported schema, is not JSON-serialisable, or is over 64 KiB—When uncaught: event, reason (schema / not_json / too_large); never the payloadSend what the event's payload schema in module.toml says; put large data in a table and send its idops.md §15.4; manifest.md section 2

| RENOX_JOB_NOT_FOUND | F6 | — | op_jobs_retry or op_jobs_cancel named an id that is not a job row of this tenant | — | When uncaught: id, op | Reload the list; the row was cancelled or pruned | ops.md §20 (1.6.0) | | RENOX_JOB_STATE | F6 | — | op_jobs_retry on a row that is not dead, or op_jobs_cancel on a running row | — | When uncaught: id, state, op | Retry only dead letters; wait for a running job to end before cancelling | ops.md §20 (1.6.0) | | RENOX_STORAGE_QUOTA | F7 | — | op_storage_put would take the tenant past its storage bytes | — | When uncaught: limit_bytes, used_bytes | Delete files or move to the paid tier | ops.md §21 (1.7.0) | | RENOX_STORAGE_TYPE | F7 | — | A content type outside [storage] allowed_types, one the sniffed bytes contradict, or SVG that was not sanitised | — | When uncaught: declared, sniffed | Upload a file of an allowed type; send SVG through the storage module's sanitiser | ops.md §21 (1.7.0) | | RENOX_STORAGE_TOO_LARGE | F7 | — | One file is over the tier's per-file limit | — | When uncaught: limit_bytes | Split or compress the file | ops.md §21 (1.7.0) | | RENOX_STORAGE_KEY | F7 | — | A key outside the pattern, with a .. segment, or under another module's prefix | — | When uncaught: key (truncated to 64 characters) | Use the module's own keys | ops.md §21 (1.7.0) | | RENOX_STORAGE_NOT_FOUND | F7 | — | No file under that key | — | When uncaught: key (truncated) | Check the key | ops.md §21 (1.7.0) | | RENOX_IMAGE_RATE | F7 | 429 | The tenant's image transform rate is spent (Retry-After) | Too many image requests. Try again shortly. | route | Use the allowed widths; variants are cached after the first request | ops.md §21 (1.7.0) | | RENOX_WEBHOOK_SIGNATURE | F7 | 401 | A webhook's signature is missing or wrong, or its timestamp is outside the tolerance; the handler does not go on | The request could not be verified. | provider, reason (missing / mismatch / expired); never the body or a header value | Check the secret WEBHOOK_<PROVIDER>_SECRET and the provider's endpoint settings | ops.md §22 (1.7.0) | | RENOX_AI_QUOTA | F7 | 429 | The tenant's monthly AI tokens are spent | The service is temporarily unavailable. | used_tokens, limit_tokens | Wait for the next month or move to the paid tier; use a key of your own ([ai] mode = "direct") | ops.md §23 (1.7.0) | | RENOX_AI_UNAVAILABLE | F7 | 503 | The model provider (or the platform gateway) did not answer or failed | The service is temporarily unavailable. | provider, status; never the provider's body | Retry later, or handle the error in the app | ops.md §23 (1.7.0) | | RENOX_PDF_LIMIT | F7 | — | A PDF would be over 200 pages or 20 MB | — | When uncaught: pages, bytes | Page the report, or produce several files | ops.md §24 (1.7.0) |

RENOX_DB_UNAVAILABLE has an HTTP status because it fails the request before any tenant code runs (the cell cannot open the file). When it happens during a request it is thrown like the other RENOX_DB_* codes.

App MCP (/mcp, F5)

Answered as JSON-RPC errors by the cell's /mcp endpoint (ops.md section 19): the HTTP status below and a body { "jsonrpc": "2.0", "id": …, "error": { "code": -32000, "message": <the fixed message>, "data": { "code": "RENOX_…", "request_id": … } } }. None reaches tenant code; none writes a meter record (the tool's run did not start). A denied Cedar action on a tool is RENOX_FORBIDDEN (above), in the same JSON-RPC form.

CodePhaseHTTPWhen it happensMessage (end user)Logged (tenant developer)What to doRefers to
RENOX_TOOL_INPUTF5400tools/call arguments fail the tool's declared input schemaThe tool input is not valid.tool, details [{ field, reason }] (never the argument values); client.nameThe agent reads details and retries; the tool's input schema is what tools/list publishedops.md §19
RENOX_TOOL_SCOPEF5403The agent token's scopes do not cover the tool (tools:* or tools:<name>)The token does not allow this tool.tool, token (id only), client.nameThe user grants the tool's scope to the agent in the auth module's consent screen; tools/list shows only covered toolsops.md §19; docs/20-ai-agents-mcp.md section 5
RENOX_TOOL_CONFIRMF5409A confirm: true tool was called without the confirmation token of its preview, or with one that expired (10 minutes), was already used, or belongs to other arguments or another agent tokenThe action needs a fresh confirmation.tool, reason (missing / expired / replayed / mismatch), client.nameCall the tool without a confirmation to get a preview and a new token, show the preview to the user, then call again with the token and the same argumentsops.md §19, "Confirmation"; docs/20-ai-agents-mcp.md section 2, "Confirmation"

Client events (dev overlay only)

CodePhaseHTTPWhen it happensMessage (end user)Logged (tenant developer)What to doRefers to
RENOX_CLIENT_EVENTS_DROPPEDF2—The gateway dropped browser events sent to /__renox/events: body or batch over its limit, per-IP or per-tenant rate, sampling, or the daily cap. The endpoint still answers 204, so the browser never sees it— (production never shows it)One summary line per minute while it lasts: reason (body_size / batch_size / ip_rate / tenant_rate / sampled / daily_cap / invalid), dropped (count). Shown in the dev overlay's "Browser" tabFix the error that floods the endpoint; lower extra sizes; disable client events with [client] events = falseclient-events.md; failure mode "Client-event flood (bug or abuse)"

Deploy (control plane and CLI)

CodePhaseHTTPWhen it happensMessage (end user)Logged (tenant developer)What to doRefers to
RENOX_DEPLOY_REJECTEDF3422 (deploy API)An uploaded artifact failed validation: any of rules V1–V16 (bundle over 20 MB, route conflict, unknown permission key, path escape, hash mismatch, ...) or the upload rules T1–T8 (link or device entry, over 100 MiB or 10,000 entries, compression ratio over 50:1, extraction over 30 s, ...)The artifact failed validation.Deployment event with rule (V1–V13, T1–T8), path or field, reason; the deployment state becomes rejectedRun renox check locally, which applies the same rules, and fix what it reportsartifact.md §6; deploy-api.md §4; failure modes "Bundle over 20 MB, route conflict, unknown permission key", "Tarball decompression bomb / too many files"
RENOX_ACTIVATION_FAILEDF3422 (deploy API)Activation stopped on the owning node: the artifact could not be fetched, a migration file failed, module evaluation failed, the probe answered 5xx or hit a limit, or the node did not answer within 60 s. Nothing switched; the previous deployment keeps servingThe deployment could not be activated.Deployment event with stage (fetch / load / migrate / probe / timeout), module, file, sqlite_error, http_status; the deployment returns to validated with last_errorFix the failing migration file or handler and deploy again; applied migration files are not re-rundeploy-api.md §6; failure modes "Migration fails half-way during activation", "New deployment fails its first request"
RENOX_POLICY_INVALIDF5422 (deploy API); renox check exit code 1A Cedar policy file (policies/app/*.cedar, a module's policies/*.cedar) does not parse, or does not validate against the merged schema (an unknown action or entity type, a type error), or the merged schema has a duplicate entity type or action (artifact.md V24); nothing is loadedThe policies failed validation.Deployment event with rule: "V24", path (the policy file), reason (the parser's or validator's message, which names policy text the tenant wrote, never ours); the deployment state becomes rejectedRun renox check, which prints the same message with the file and line; declare the action in [authz] actions or the entity in [authz.entities] / policies/schema.cedarschemaartifact.md §3, "Policies", §6 V24; ops.md §17; docs/24-threat-model.md rule 3 (fail closed)
RENOX_CLI_WORKER_UNVERIFIEDF3— (CLI, exit code 3)The CLI refused to run a renox-worker binary whose sha256 differs from the pinned release or whose sigstore signature or provenance does not verify—CLI output only: expected_sha256, actual_sha256, the binary path on the developer's own machineRe-download the worker (renox dev fetches the pinned release) or pass a binary from an official release; never disable the checkdocs/24-threat-model.md F, "Tampered release"; #129

Control API (deploy API, F3 and F6)

Answers of control/api to tenants, nodes and operators (deploy-api.md §1, §2, §12). The body is the JSON of section 1 plus, where deploy-api.md §12 says so, a details list; it never contains a secret value, a token, a server path, or an internal address. These codes are not thrown into tenant code and are not written to the tenant log sink; mutating requests are audit-logged instead (deploy-api.md §2).

CodePhaseHTTPWhen it happensMessage (end user)Logged (tenant developer)What to doRefers to
RENOX_VALIDATIONF3400A request body, parameter or header is malformed: bad JSON, an unknown field, a value outside its pattern or range, a secret value under 8 bytes, a non-.map file in a sourcemap upload (422 there)The request is not valid.Audit row with the status; details { field, reason } in the responseFix the field named in details; the CLI validates before sendingdeploy-api.md §1, §9
RENOX_UNAUTHENTICATEDF3401, WWW-AuthenticateNo token, an unknown, expired or revoked token, or a token of the wrong kindAuthentication is required.Not logged per request; repeated failures per IP are countedrenox login again or create a new token; check the token has not expireddeploy-api.md §2; docs/24-threat-model.md E, "Leaked CLI or CI token"
RENOX_TOKEN_SCOPEF3403The token is valid but lacks the operation's scope, or a node token names another nodeThe token does not allow this action.Audit row with the token id and the actionCreate a token with the needed scope (deploy:<app>, secrets:write, ...)deploy-api.md §2
RENOX_NOT_FOUNDF3404The app, deployment, secret, domain or token does not exist in the caller's tenant (another tenant's resources are also 404)Not found.—Check the app name and hash (renox deploy --list)deploy-api.md §2; docs/24-threat-model.md E, IDOR row
RENOX_CONFLICTF3409A state transition the state machine does not allow, an app name or hostname already taken, another activation of the app in progress, a suspended tenant, an unknown node heartbeating, or a late prepare_resultThe request conflicts with the current state.Audit row; details { field, reason }Read the current state (GET the resource) and retry the right action; wait for the running activationdeploy-api.md §5, §6, §10
RENOX_RATE_LIMITEDF3429, Retry-AfterThe token's request or upload bucket is empty, or too many unauthenticated requests came from one IPToo many requests. Try again later.—Wait for Retry-After; batch work in CI instead of pollingdeploy-api.md §2; docs/24-threat-model.md E, "Deploy API abuse"
RENOX_BACKUP_UNAVAILABLEF6503, Retry-After: 30A backup action (status aside) cannot run: replication is off for the app's database, the replica or the owning node is unreachable, or no generation exists yet (deploy API 0.3, exportBackup, restoreBackup, importDatabase, moveTenant; node listener restore)No backup is available for this database right now.Audit row with the action; details { field, reason } (replication: none, node: unreachable, replica: unreachable, generation: none)Wait and retry; a database with replication off has no restore points (free tier turns it on by default from F6)deploy-api.md §14, §15; failure modes "Replica lagging / Litestream behind", "Node dies, restore fails"
RENOX_RESTORE_POINT_UNKNOWNF6422A point-in-time restore asked for a time no generation covers: in the future, before the oldest generation, outside retention, or in a gap between generationsNo backup covers the requested time.Audit row; details { field: timestamp, reason } with the oldest and newest restore pointsPick a time inside one of the generations that GET /v1/apps/{app}/backups listsdeploy-api.md §14
RENOX_IMPORT_REJECTEDF6422importDatabase refused the file: the target app is not a staging app, the file is not a SQLite database, fails PRAGMA quick_check, or has a table, view, index or trigger name the authorizer would refuse (no <module>__ prefix)The database file cannot be imported.Audit row; details { field, reason } (app / file / integrity / table)Import only into an app created with staging: true; run renox check on the schema; use <module>__ table namesdeploy-api.md §14
RENOX_TENANT_MOVINGF6503, Retry-After: 30The tenant is being moved by an operator (move to a microVM in progress): the node answers the tenant's requests with it, and the API refuses another tier change, move, export or import until the move endsThe service is being moved. Try again shortly.One line per minute while it lasts, source: platform, kind: moveNothing in the app; the move ends within minutes or is rolled backdeploy-api.md §17

4. Rules for adding a code

  • Codes are RENOX_ followed by upper-case words joined by _. A code names a cause the tenant can act on, not an implementation detail.
  • A new code needs a row here (all columns), and, if it crosses the op boundary, a row in ops.md section 13 and a type in ctx.d.ts, in the same PR. validate.ts fails otherwise.
  • A code listed with a later phase is reserved: nothing may produce it before that phase, and the row is completed by the issue that implements it.
  • The fixed messages are in English. Localised messages (EN/AR, docs/21-uae-gcc-strategy.md) are a later addition keyed by code. The code is the stable identifier, never the message.

Open

  1. Localisation of messages. The default page and the JSON message are English in F1. Arabic messages keyed by code and Accept-Language are expected with the F2 default page; the mechanism (a second fixed table) is not designed yet.
  2. Rate limit (429). Gateway rate limiting (docs/23-failure-modes.md, "Request flood from one IP") answers 429 with Retry-After and no code. The control API answers 429 with RENOX_RATE_LIMITED (F3); using the same code at the gateway would make it consistent with the others. This needs #22 to decide whether rate-limit responses carry a body at all, since a body costs bytes under a flood.
  3. RENOX_BUSY log volume. One summary line per second is a proposal for #28 to confirm.
  4. RENOX_TENANT_SUSPENDED and crawlers (0.6). A 503 with Retry-After tells search engines to come back rather than drop the pages. A suspension that lasts weeks would still lose ranking; whether a long suspension moves to 410 is the operator's decision and not in F7.

Changelog

VersionIssueChanges
0#91First catalogue (F1)
0.1#152Page (HTML) routes: the four error-template inputs, the fixed error-page render budget, the 404 page, and the fallback order (section 1). New rows: RENOX_HOST_MISMATCH (421, F2), RENOX_CLIENT_EVENTS_DROPPED (F2, dev overlay only)
0.2#50Deploy API (F3): section "Control API" (RENOX_VALIDATION, RENOX_UNAUTHENTICATED, RENOX_TOKEN_SCOPE, RENOX_NOT_FOUND, RENOX_CONFLICT, RENOX_RATE_LIMITED); RENOX_ACTIVATION_FAILED, RENOX_CLI_WORKER_UNVERIFIED; RENOX_DEPLOY_REJECTED gains T1-T8 and its message; RENOX_UNAVAILABLE covers the deploy API
0.3#251Deploy API 0.3 (F6): RENOX_BACKUP_UNAVAILABLE, RENOX_RESTORE_POINT_UNKNOWN, RENOX_IMPORT_REJECTED, RENOX_TENANT_MOVING in "Control API"
0.4#272Ops 1.5.0 (F5): RENOX_REALTIME_LIMIT and RENOX_REALTIME_BACKPRESSURE in "Request limits"; the new section "App MCP" with RENOX_TOOL_INPUT, RENOX_TOOL_SCOPE, RENOX_TOOL_CONFIRM; RENOX_POLICY_INVALID in "Deploy"; the RENOX_FORBIDDEN and RENOX_DB_SCOPE rows completed (reasons, logged fields, the uncaught-is-403 rule)
0.5#318Ops 1.7.0 (F7): the storage, image, webhook, AI and PDF codes
0.6#346Artifact 1.5 and deploy API 0.5.1 (F7): RENOX_TENANT_SUSPENDED, the neutral suspension page the gateway answers for a suspended route entry

Source on GitHub