{
"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"
}Usage events and 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
| Type | Produced when | Default durability |
|---|---|---|
| after a response, if subscribed | best effort |
| login, logout, callback, back-channel logout, revocation | zero loss |
| 401/403/502/5xx, provider unavailable | window |
| a billable unit ( | zero loss |
| 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 |
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:
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;
routeis the normalized pattern, never the raw URL;hostis the request host, without port. Routes are keyed by (host, path):host+routeidentify 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-Afterand ausage_unavailablebody;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
maxLagByteseither 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 |
|
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 |
|