demo.clarkmerrick.com

AI Gateway + OpenCode Architecture

How my Cloudflare AI Gateway is wired up so a coding agent (OpenCode) can authenticate with Cloudflare Access and pull its config remotely, with no API keys on the laptop.

The moving parts

Four pieces work together. Everything is Cloudflare native, and only the AI Gateway hostname is protected by Access.

ComponentWhat it does
aig.demo.clarkmerrick.com AI Gateway custom domain (CNAME → gateway.ai.cloudflare.com), routing to the opencode gateway. Protected by Cloudflare Access. Unified Billing supplies provider credentials, so no keys leave Cloudflare.
opencode-config.demo.clarkmerrick.com A Cloudflare Worker that serves two small JSON files that carry no credentials: /opencode (discovery) and /opencode.json (provider config). Not behind Access.
Single Redirect rule A dynamic redirect on the zone that forwards aig.demo.clarkmerrick.com/.well-known/opencode to the Worker's discovery file. Runs at the edge before Access.
Cloudflare Access app A self hosted Access application guarding aig.demo.clarkmerrick.com. Users authenticate via SSO; cloudflared mints a short lived JWT the agent sends on every request.

Topology at a glance

The laptop only ever talks to Cloudflare. Discovery/config traffic hits the Worker; model traffic hits the Access protected gateway.

                       (A) discovery + auth            (B) model inference

  ┌─────────────────────────┐
  │  Developer laptop        │
  │   • opencode CLI         │
  │   • cloudflared          │
  └───────────┬─────────────┘
              │
   (A) GET /.well-known/opencode         (B) POST /compat/chat/completions
       Host: aig.demo.clarkmerrick.com       Host: aig.demo.clarkmerrick.com
              │                                   cf-access-token: <JWT>
              ▼                                        │
  ┌───────────────────────────────────┐               │
  │  Cloudflare edge  (demo zone)      │               │
  │   1. Single Redirect  ──302──┐     │               │
  │   2. Cloudflare Access        │    │◄──────────────┘
  └───────────────┬───────────────┘    │   validate JWT (aud + policy)
                  │ 302 to Worker       │        │ allow
                  ▼                     │        ▼
  ┌───────────────────────────────┐    │   ┌──────────────────────────────┐
  │ Worker: opencode-config...     │    │   │ AI Gateway  "opencode"        │
  │  /opencode      (discovery)    │    └──►│  logging • caching • billing  │
  │  /opencode.json (provider cfg) │        │        │                      │
  └───────────────────────────────┘        │        ▼                      │
                                            │  Workers AI  @cf/* models     │
                                            └──────────────────────────────┘

Request flow, Phase A: discovery & login

This happens once when a user runs opencode auth login https://aig.demo.clarkmerrick.com (and again when the Access session expires).

1
OpenCode asks the gateway host for discovery. It requests the /.well-known/opencode path on the AI Gateway domain.
GET /.well-known/opencode HTTP/2
Host: aig.demo.clarkmerrick.com
2
The Single Redirect answers first. Dynamic redirects run at the edge before Access, so this path is never challenged. It just 302s to the Worker.
HTTP/2 302
location: https://opencode-config.demo.clarkmerrick.com/opencode
3
The Worker returns the discovery file. It names the auth command to run and where to fetch the real provider config. No secrets here.
GET /opencode HTTP/2
Host: opencode-config.demo.clarkmerrick.com

HTTP/2 200
content-type: application/json
{
  "auth": {
    "command": ["cloudflared","access","login","--no-verbose",
                "-app=https://aig.demo.clarkmerrick.com/"],
    "env": "TOKEN"
  },
  "remote_config": { "url": "https://opencode-config.demo.clarkmerrick.com/opencode.json" }
}
4
cloudflared runs the Access login. A browser SSO flow completes against the Access app, and Cloudflare issues a short lived JWT scoped to that app. OpenCode captures it as {env:TOKEN} and stores it locally.
5
OpenCode fetches the remote provider config. This tells it the base URL and headers to use. Note the credential is a header placeholder, not a key baked into the file.
GET /opencode.json HTTP/2
Host: opencode-config.demo.clarkmerrick.com

HTTP/2 200
{
  "enabled_providers": ["cf-workers-ai"],
  "provider": {
    "cf-workers-ai": {
      "npm": "@ai-sdk/openai-compatible",
      "options": {
        "baseURL": "https://aig.demo.clarkmerrick.com/compat",
        "apiKey": "",
        "headers": {
          "cf-access-token": "{env:TOKEN}",
          "X-Requested-With": "XMLHttpRequest"
        }
      },
      "models": { "workers-ai/@cf/...": { } }
    }
  }
}

Request flow, Phase B: model inference

Every prompt the agent sends now flows through the Access protected gateway.

1
OpenCode calls the gateway's OpenAI compatible endpoint, attaching the Access JWT it minted in Phase A.
POST /compat/chat/completions HTTP/2
Host: aig.demo.clarkmerrick.com
cf-access-token: <short lived Access JWT>
X-Requested-With: XMLHttpRequest
content-type: application/json

{ "model": "workers-ai/@cf/zai-org/glm-5.3",
  "messages": [ { "role": "user", "content": "refactor this function" } ] }
2
Cloudflare Access validates the JWT at the edge: signature, audience (aud) tag, and the app's policies. No token (or an invalid one) is rejected before the gateway is ever touched:
HTTP/2 401
www-authenticate: Bearer realm="OAuth", error="invalid_token",
   error_description="Missing or invalid access token"
3
A valid token is proxied to the opencode AI Gateway. The gateway applies logging, caching, rate limiting and Unified Billing, then routes to the model provider.
4
Workers AI runs the model (e.g. @cf/*) and the gateway returns a standard OpenAI shaped response back to the laptop.
HTTP/2 200
content-type: application/json

{ "choices": [ { "message": { "role": "assistant", "content": "..." } } ],
  "usage": { "prompt_tokens": 128, "completion_tokens": 512 } }

Access & permissions flow

  • Self hosted Access application is bound to the hostname aig.demo.clarkmerrick.com. It has a unique audience (aud) tag that every issued JWT is stamped with.
  • Identity + policies decide who gets in. The login flow authenticates the user against the configured identity provider (SSO), then Access evaluates the app's policies (e.g. allowed email domains or specific users) before issuing a token.
  • The token is short lived. cloudflared caches it locally; when the session expires, running opencode auth login again transparently mints a fresh one.
  • The gateway trusts Access, not the client. Because Access terminates in front of the gateway, the model endpoint only ever sees requests that are already authenticated.
  • Discovery stays public on purpose. The Single Redirect fires in the dynamic redirect phase, which is ahead of Access in the request pipeline, so /.well-known/opencode resolves without a login prompt, while everything else on the host stays locked down. The files it exposes contain configuration only, never credentials.

Want the walkthrough?

Happy to demo this live. Best point of contact is email at [email protected]