Architecture
Gate One is a reverse proxy at the edge. It authenticates the request, applies route policy, optionally injects UX, then forwards the request to the existing application. The application keeps its own URL, stack and deployment; it does not need an SDK.
Request path
flowchart TB
C["Browser / API client"] --> G
subgraph G["Gate One"]
direction TB
M["1. match route
host + path"] --> D["2. reserve durable
event space"]
D --> A["3. authenticate
+ identity policy"]
A --> T["4. verify anti-robot
token (Turnstile)"]
T --> F["5. proxy, or
401 / 403 / 502 / 503"]
F --> E["6. emit subscribed
event after the response"]
end
G -->|"OIDC / JWT"| I["Identity provider
or IAM"]
G -->|"proxied request"| AP["Existing application"]
G -.->|"HTTP/JSON event"| P["Out-of-process plugin"]
The gateway does six things in order:
match the route by host and path;
reserve durable event space (when a durable type is subscribed);
authenticate and check the identity policy;
verify the anti-robot token on a protected form POST (Turnstile);
forward the request, or answer 401/403/502/503 itself;
emit the subscribed event after the response.
A failure at step 2 is a 503 before authentication, so an event the operator declared durable is not lost. A failure at step 3 never reaches the backend. A failed anti-robot check (step 4) is a 403 after the rate limit and before the backend; a Siteverify outage is a 503 — never a "you are a bot" answer.
Bot protection
Cloudflare Turnstile is configured once, at the top level, and applies to
every matching host/path. In forms mode Gate One injects the widget
into matching forms and verifies the POST with Siteverify before the
backend. In gate mode the first request to a protected path gets a
challenge page at its own URL; solving it sets a signed 30-minute
clearance cookie (HMAC, checked locally), and the following requests pass
without another Siteverify call. both combines the two. It only covers
traffic that passes through this instance: a second ingress or a
published port bypasses it, so restrict the application’s network access
to Gate One. See Turnstile.
Why run behind a reverse proxy
Gate One protects applications as a reverse proxy, but it should itself
sit behind a front reverse proxy or ingress controller in most
deployments. The gateway listens on plain HTTP, so the ingress terminates
TLS, publishes one stable public URL, load-balances and health-checks the
replicas, and keeps WAF, rate limits and access logs in front. Gate One
trusts X-Forwarded-* from that immediate hop and must not be reachable
directly by clients.
See Reverse proxy for the forwarded headers and probes.
Provider types
| Mode | Use | External dependency |
|---|---|---|
| Onboarding, tests, local demos. A dedicated | none |
| Any service implementing the Gate One HTTP bridge
contract; auth-bridge is the reference. The legacy | auth-bridge or a custom bridge |
| Native OpenID Connect relying party: discovery, code + PKCE, state/nonce, JWKS, signed session, RP-initiated and back-channel logout, MCP bearer validation. | an OIDC Identity Provider (IdP) or IAM |
| Stateless JWKS bearer validation for APIs and MCP. No login page, no session cookie. | a JWT issuer with JWKS |
authProvider is the single-provider form. authProvidersdefaultAuthProvider + auth.provider bind one provider per route for
multi-tenant hosting. Only oidc and jwt are allowed in the plural
form; http-bridge and dev remain single-provider types.
For unrelated domains (for example app-a.com and app-b.fr), use
ssoGroups: the shared OIDC/client/session settings are written once,
providers[] declares one domain-specific provider, and
routes[].hosts[] expands one route body across those domains. Each
domain keeps its own Gate One cookie; the IdP session provides the
zero-form login. This is not a shared cross-TLD cookie. See
Cross-domain SSO.
An authentication provider is not a plugin. authProvider.type:
"plugin" is rejected. Authentication adapters live in the embedded IAM
and remain under the Gate One licence.
With or without the embedded IAM
The embedded IAM is optional.
Without the embedded IAM, Gate One talks directly to one OIDC or JWT issuer per provider. This is the simplest production setup.
With the embedded IAM (
cladone-auth), Gate One trusts one local issuer that federates Google, Microsoft, Auth0, Keycloak or local accounts, applies tenant policy and re-emits Ed25519-signed tokens. Gate One never trusts the upstream token.
The embedded IAM adds a second service and a second session (browser SSO on the IAM host), but it keeps the trust boundary local and makes multi-provider and multi-tenant setups uniform. It also publishes the revocation keyspace Gate One reads for a global logout.
Identity and injectable UX
After authentication, Gate One knows an identity: user, roles, scopes, claims and an opaque subject id. It can:
enforce route conditions (
auth.when,auth.identity);add the opaque
X-Gate-One-Subjectheader for the backend;inject a user badge, a login guard, gated controls, a forbidden banner, a PWA shell and custom HTML/JS/CSS;
emit a strict CSP in
merge,hashornoncemode.
The injected guard is class-based and runs after the gateway has decided authentication. It is a UX layer, not an authorization boundary: hiding a control does not grant or revoke rights.
The authorization check itself is a first-class route setting. Configure
auth.identity with the roles, scopes, claims or user that your backend
expects, and Gate One returns 403 before forwarding a request that lacks
them; MCP services get the equivalent scope check. This covers
route-level rights validation. The backend still enforces its own
resource-level permissions, because only it knows what a given record or
action means.
Usage bus and plugins
Usage events are opt-in per type. A plugin declares a subscription in a signed manifest; only then are those events produced.
one WAL per (node, type);
one cursor per (type, subscriber);
a node budget split into per-type shares;
fsync: always= zero loss before the response;window_ms= group commit;none= best effort;onMaxLag: block= targeted 503 until the plugin catches up;onMaxLag: drop= cursor advances to the lag limit and the plugin is marked degraded.
Events contain no token, cookie, email or username. The user is an
opaque subjectId = v1. + HMAC(issuer, sub). The HMAC secret is held by
the producer only.
Extensions are out-of-process HTTP/JSON services, verified by an Ed25519 manifest and a local trust store. There is no compile-time plugin API. See Plugins and the manifest reference.
Deployment and high availability
A production deployment looks like this:
flowchart TB LB["Load balancer / ingress
TLS termination"] --> R1["Gate One
replica 1"] LB --> R2["Gate One
replica 2"] R1 --> RS[("Redis Sentinel
sessions + revocation")] R2 --> RS R1 --> AP["Existing application"] R2 --> AP
Two replicas behind a load balancer; no published container port.
Shared state in Redis (Sentinel + TLS + ACL) with a versioned key prefix. A local bbolt file is valid for a single instance only. See Redis.
Same session key on every replica (
sessionKeyEnv), and one sharedssoGroupssession store for cross-TLD providers.Health probes:
/healthz(liveness) and/readyz(readiness, including providers and usage).Manual rolling restart: one replica at a time, 30-second stop grace, in-flight drain. There is no blue/green deployment yet.
WAL per node: a plugin consumes each node’s WAL.
Configuration reload:
SIGHUPre-reads and validates the file, then swaps the gateway atomically. An invalid file is refused and the previous configuration stays active.Fail closed: Redis unavailable on an authenticated route →
502 authentication_unavailable; a full durable share → targeted503 usage_unavailable; a failed login audit → no session.
The Horizontal scaling feature and the High Availability example cover when to scale and the Redis-backed configuration.
What Gate One is not
It is not an identity provider (the
devprovider is for local development only).It is not a token issuer; the embedded IAM is a separate optional service.
It is not a general-purpose authorization server: policy is per route (technical conditions, roles, scopes, claims), not per resource.
It is not a plugin runtime inside the request process: plugins never decide authentication and never sit on the hot path.
It does not provide commercial metering in the free/self-hosted edition; usage events are operational.