Usage events and plugins

Durable, subscribed, pseudonymized events delivered to signed out-of-process plugins.

Useful if you want to…

  • meter or audit usage without putting code on the request path;

  • keep operational metrics (auth failures, errors, request counts) in your own system;

  • integrate a commercial or closed-source extension without granting it access to tokens or identities;

  • survive a plugin outage without losing the events you declared durable.

Events are produced only for subscribed types. If no plugin subscribes to request, Gate One produces no request event at all. Free/self-hosted Gate One is operational-only: it ships no commercial metering.

Event types

TypeProduced whenDefault durability

request

after a response, if subscribed

best effort

auth

login, logout, callback, back-channel logout, revocation

zero loss

error

401/403/502/5xx, provider unavailable

window

billing

a billable unit (units/unitType, optional attrs)

zero loss

audit

reload, plugin load/revoke

best effort

audit records control-plane events: action is reload, reload_failed or plugin_load, with detail carrying the plugin name or the reload error. It is best effort and never blocks the operation it records: if the type is unsubscribed, no plugin matches, or the WAL is full, the event is dropped and logged — a reload is never refused for lack of audit space. Admin 403`s surface as `error events.

request and error cannot use fsync: "always": the proxy emitter cannot fsync before a streaming response. Use window_ms for them.

Important

Subscribing to auth makes sign-in depend on the event bus. On a login, auth is fail-closed: if the event cannot be written (WAL full, onMaxLag: block, plugin behind, disk error), Gate One returns 503 before creating the session — the user cannot log in. Do not subscribe to auth in production unless you explicitly accept that a full or unavailable WAL blocks login. For a demo, an audit trail, or any non-critical use, subscribe to request and error instead: they are window/best-effort and never block a request.

A route can opt out of the bus entirely with "usage": false (default true): it is served normally but produces no event — no WAL reservation and no admission 503 on that route. Use it for control or monitoring routes (a status page, an events viewer) that must not pollute, or depend on, the event bus.

Event shape

An auth event:

{
  "schemaVersion": 1,
  "type": "auth",
  "ts": "2026-11-02T12:00:00Z",
  "instanceId": "gw-1",
  "epoch": "2026-11-02T11:00:00Z",
  "tenant": "company",
  "subjectId": "v1....",
  "host": "app.company.example",
  "action": "login",
  "sessionId": "...",
  "status": 200,
  "authResult": "ok"
}

A request event additionally carries the route, host and method:

{
  "schemaVersion": 1,
  "type": "request",
  "ts": "2026-11-02T12:00:01Z",
  "instanceId": "gw-1",
  "epoch": "2026-11-02T11:00:00Z",
  "tenant": "company",
  "subjectId": "v1....",
  "host": "app.company.example",
  "route": "/api/orders",
  "method": "GET",
  "status": 200,
  "durationMs": 12,
  "bytes": 1024,
  "authResult": "ok"
}

Rules:

  • no token, cookie, raw query string, email or username;

  • route is the normalized pattern, never the raw URL;

  • host is the request host, without port. Routes are keyed by (host, path): host + route identify the route, so the same path served on two hosts is not ambiguous;

  • the user is an opaque subjectId: a per-deployment HMAC of the (issuer, subject) pair, so it is stable and not reversible. The secret is shared by every instance and held only by the producer; a plugin never receives it. The exact construction is in the developer documentation.

The application may receive the same identifier as a backend header:

X-Gate-One-Subject: v1.<base64url>

It is added after authentication, never accepted from a client, and is not an authentication token.

Billing and capture

A billing event carries a metered amount: units + unitType (request, token, byte, mcp_call), plus optional bounded attrs.

The cheapest and most accurate source is the backend, which knows what it actually consumed. It returns internal response headers:

X-Gate-One-Units: 1240
X-Gate-One-Unit: token
X-Gate-One-Billable: {"units":1240,"unitType":"token","attrs":{"model":"gpt-4"}}

X-Gate-One-Billable is an optional JSON shortcut (≤ 512 bytes). The gateway copies the values into the event and strips the headers from the client response; a client-supplied copy is stripped from the request. Prefer this path: no request-body parsing, the smallest record, the most accurate count.

When the backend cannot report, opt in to gateway capture for specific endpoints:

"usage": {
  "capture": [{
    "hosts": ["app.company.example"],
    "routes": ["/api/process_stream"],
    "methods": ["POST"],
    "types": ["billing"],
    "query": ["coupes", "lang"],
    "body": { "paths": ["$.coupes", "$.model"], "maxBytes": 2048 },
    "maxAttrsBytes": 1024
  }]
}

Captured values appear as attrs: q.<name> for a query parameter and b.<path> for a body field. Everything is bounded: at most 8 keys, keys ≤ 32 bytes, values ≤ 256 bytes, total ≤ maxAttrsBytes (default 1024, max 2048). When a value is dropped or cut the event sets attrsTruncated: true. Sensitive names (token, password, secret, authorization, cookie, credential, session, …) are always skipped, and a body is parsed only when it is no larger than body.maxBytes.

Capture applies only to the event types listed in types (default billing), so high-volume request events stay small. Keep the WAL lean: prefer the backend header, allow-list a few query names rather than whole bodies, and use a content-addressed spool (not the WAL) for large payloads.

WAL and backpressure

  • one WAL per (node, event type);

  • one cursor per (type, subscriber);

  • a node budget (50 MiB by default) split into per-type shares; the sum of shares must not exceed the budget;

  • admission reserves space before authentication and before the backend call;

  • a full durable share answers the affected route with a targeted 503, Retry-After and a usage_unavailable body;

  • healthz, readyz, the auth probe and the usage endpoints are exempt;

  • fsync: "always" (zero loss) appends and fsyncs before the response; a failed fsync is a 503;

  • fsync: "window_ms" groups commits; a crash may lose the current window;

  • a subscriber beyond maxLagBytes either blocks (503 until it catches up) or drops (cursor advances to the lag limit, events become evictable, the plugin is marked degraded);

  • a new subscriber starts at the tail; there is no historical replay.

Note

usage.dir is local to each replica: it is that node’s own WAL. With several replicas behind a load balancer, give every node its own directory (and its own nodeId) — two writers on one directory would corrupt it. Each node delivers its events independently; there is no cross-node aggregation.

On a login, auth is fail-closed: if the event cannot be written, Gate One returns 503 before creating the session. Logout and back-channel logout are best effort: the revocation must succeed even if auditing fails.

Signed plugins

A plugin is an out-of-process HTTP/JSON service. It is never loaded into the Gate One process and never decides authentication.

{
  "usage": {
    "dir": "./usage-wal",
    "nodeId": "gw-1",
    "subjectSecretEnv": "GATE_ONE_SUBJECT_SECRET",
    "pluginSecretEnv": "GATE_ONE_PLUGIN_SECRET",
    "trustStore": "trust-store.json",
    "types": {
      "auth":  { "shareBytes": 10485760, "fsync": "always" },
      "error": { "shareBytes": 10485760, "fsync": "window_ms", "windowMs": 250 }
    },
    "plugins": [{
      "name": "com.cladone.audit",
      "url": "http://127.0.0.1:9100",
      "manifest": "audit.manifest.json",
      "artifact": "audit-echo.placeholder"
    }]
  }
}

The manifest is signed (Ed25519) and declares the artefact digest, the dependencies, the host API and the event subscription; Gate One verifies all of it at startup against usage.trustStore. A manifest is authoritative for the subscription (events.types, filters, maxLagBytes, onMaxLag) and the licence feature. Filters can narrow by hosts, routes, tenants, methods and statusClasses. routes matches the path only, not host+path, so add hosts when the same path exists on several hosts. The complete example is routes.json, audit.manifest.json, trust-store.json.

The plugin-side contract — the signed assertion, the /health and /events endpoints, the reference cmd/plugin-audit-echo, and how to sign, install and revoke — is in Plugins and Publish and trust a plugin.

Licensing

Non-auth, non-IAM extensions may be closed-source or commercial; a commercial plugin may declare licenseFeature and the host refuses to load it without that feature. The full terms are under Source and licence.

Important
  • There is no public compile-time plugin API. Go plugin (.so) is not supported.

  • A plugin cannot add a pre-auth hook, register its own route, mutate the identity or change auth, securityHeaders, license, rateLimit or quota.

  • Plugins are internal-network services: no Traefik exposure, signed assertion, separate UID, no secrets in the environment, egress allow-list.

  • Long-term retention is the plugin’s policy, not the core’s.