Architecture

How Gate One sits between the browser, the identity provider, the applications and the plugins.

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:

  1. match the route by host and path;

  2. reserve durable event space (when a durable type is subscribed);

  3. authenticate and check the identity policy;

  4. verify the anti-robot token on a protected form POST (Turnstile);

  5. forward the request, or answer 401/403/502/503 itself;

  6. 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

ModeUseExternal dependency

dev

Onboarding, tests, local demos. A dedicated login.<domain> host, real alice/bob form, loopback only.

none

http-bridge

Any service implementing the Gate One HTTP bridge contract; auth-bridge is the reference. The legacy http alias is accepted.

auth-bridge or a custom bridge

oidc

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

jwt

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. authProviders
defaultAuthProvider + 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-Subject header 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, hash or nonce mode.

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 shared ssoGroups session 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: SIGHUP re-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 → targeted 503 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 dev provider 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.