Documentation that lists three supported gateways is how customers pick the weakest one. Intertrace production is Fly agentgateway, pinned at v1.3.1 in deploy/agentgateway/, with all planes live: LLM, MCP, Inference, Service, and A2A. Clients set base_url to https://intertrace.fly.dev/v1 and present Bearer itr_rt_*. Deploy is npm run deploy:fly-agentgateway-full. Smoke is npm run smoke:fly-agentgateway-full. If a design discussion begins with which Railway service we should extend, the discussion is already on the wrong path.
We shipped Node and Railway prototypes because they were fast to iterate. They taught us event shapes, classifier latency, and how customers actually point an SDK at a base_url. They also created a gravitational well: every new feature wanted to land on the thing that was already receiving bst_* traffic. Product discipline meant starving those codepaths of new detection and new planes so the pin stayed honest. This post is the record of that decision, not a migration guide for the prototypes.
Preloop is a control-plane foundation for discovery and governance authoring, wrapped behind lib/platform/preloop/. It is not the customer UI. Fly agentgateway plus the Intertrace Go enforcer is the runtime. Customers see one product spine.
What production actually is
Customer apps and agents send HTTPS to the Fly edge at intertrace.fly.dev. Agentgateway, running the pinned --file YAML we compile in-repo, muxes by plane. The Intertrace Go enforcer sits on ExtAuthz, promptGuard, ExtMCP, and authorize-plus-permits. Those hooks are fail-closed: if the enforcer cannot answer, the request does not proceed to the upstream model, MCP server, or HTTP service. Tenancy is key-derived. ExtAuthz calls POST /api/runtime/verify-key. The key maps to an organization. Caller-supplied org headers never override it.
After a decision, the runtime emits a SecurityEventEnvelope toward dashboard ingest. Emit is asynchronous relative to the user-visible path. The dashboard writes gateway_events under the derived org, may create findings, and never needs the client to name the tenant correctly. Investigation happens in the Next.js app on platform.intertrace.ai, reading Supabase under RLS. That is the whole loop. Anything that is not on this loop is compatibility or local development.
- LLM: OpenAI-compatible /v1/* — chat completions and related surfaces, promptGuard on the inspect path.
- MCP: /mcp — list and call inspect, ExtMCP, tool-return bodies treated as untrusted context.
- Inference: self-hosted / pool routing with the same security hooks.
- Service: /service/* plus a per-tenant registry for HTTP/gRPC-style tool surfaces.
- A2A: /a2a with deep inspect of message/send, not a blind TCP forward.
The pin is the product
deploy/agentgateway/ pins v1.3.1. lib/agentgateway/ compiles the --file YAML we actually run. That compiler is Intertrace-owned. We do not hand customers an upstream admin UI and call it configuration. We do not float latest on a Tuesday. When we bump the pin, we deploy with npm run deploy:fly-agentgateway-full and we prove the edge with npm run smoke:fly-agentgateway-full. A pin without a smoke is a comment. A smoke against localhost is not that smoke.
Agentgateway is the transport and plane mux. The Go enforcer is the security kernel. Splitting them is how we compose an open-source gateway without outsourcing authorize, key verification, or evidence. If upstream grows a console, customers still do not go there. The product spine—Connect, Estate, Traffic, Work, Graph, Policies, Labs—is the only operating picture. Preloop’s console is in the same bucket: mature, separate, not shipped.
Keys
New runtime onboarding issues itr_rt_* keys. verify-key looks up runtime_api_keys by prefix, checks hash, revocation, expiry, and optional project/agent binding, and returns the org. Invalid, revoked, or expired keys fail closed. bst_* keys exist for older clients on the legacy proxy. Do not issue them for new work. Do not teach a new SDK to send X-Org-Id. The tenant is a consequence of the key.
What still exists and why we ignore it for product work
Compatibility surfaces are real. They have customers or had customers. They are also how a roadmap dissolves. The freeze is not emotional. It is a list.
- Railway gateway-v2 (the Go binary in provider/proxy mode) may still serve older bst_* clients. New detection, new planes, and new authorize behavior do not land there first. Security and correctness fixes for an active legacy client are allowed. Cutover planning against it is not.
- Node bastion-gateway-deploy and gateway.js are deployable Node proxies from an earlier era. They are not a second production. Do not add planes to them.
- gateway-connected.js is a local process that loads .env.gateway and mirrors deploy behavior for development. It is not a fourth production, and a successful curl against it is not a Fly smoke.
- gateway.intertrace.ai is a dashboard/edge hostname in the Vercel project. It is not the planning target for runtime features. Runtime features go to intertrace.fly.dev.
- services/mcp-proxy and similar standalone MCP shims: existing clients only. New MCP transport belongs on agentgateway /mcp.
The temptation is always the same: a customer is still pointed at Railway, a bug is easier to patch in Node, a demo is already using bst_*. Each of those is a support ticket, not a design input. If we keep implementing A2A inspect or Service registry on the legacy proxy, we will have two incomplete data planes and no pin we can defend. Starving the prototype is how the product path stays a path.
gateway.intertrace.ai deserves a specific warning because it looks like a runtime. It is a hostname in the same Vercel project as marketing and platform. Middleware and docs sometimes mention it as a gateway edge. Runtime features, ExtAuthz, plane routes, and itr_rt_* verification are planned against intertrace.fly.dev. If you put a new MCP inspect flag behind a Vercel route on gateway.intertrace.ai, you have not shipped it to production traffic. You have shipped a comment with a TLS certificate.
Why Fly won
We needed a data plane we could pin, compile --file YAML for, attach ExtAuthz to, scrape /metrics from, and describe to an SDK user as a base_url. Fly gave us a host that matches how OpenAI-compatible clients already think: set baseURL, set apiKey, send traffic. The enforcer can colocate. TLS is the edge’s problem. We can talk about p50 and p95 of gateway-added time from production metrics rather than from a laptop.
Railway was a good place to learn. Node was a good place to learn. Learning is not the same as production. The Node proxy made it too easy to add a header, a regex, and a fail-open for the demo. gateway-v2 accumulated runtime modes that still matter as history (ADR 0002) and do not matter as a place to put the fifth plane. Fly plus a compiled YAML plus a fail-closed Go process is slower to iterate and harder to lie about.
The challenge was not YAML. It was people—including us—who had already integrated the prototype. Saying internally, repeatedly, that new work does not go there was harder than writing the compiler. A feature that exists only on Railway is a feature we cannot smoke on Fly, cannot pin, and cannot sell as the product. We would rather delay the feature than mint a second source of truth for inspect.
Control plane versus data plane
ADR 0005 is explicit. Preloop is the control-plane foundation: discovery and onboarding patterns, agent lifecycle, governance authoring. Agentgateway is the runtime. Intertrace owns composition, tenancy, authorize, telemetry, and the customer UX. Preloop is not the customer UI, not the identity provider, and not the authorize SSOT. A live remote Preloop process exists only when INTERTRACE_PRELOOP_BASE_URL is explicitly configured. Adapter-only is the default. deploy/preloop/ is a pin, not a promise that we operate their console.
Operators define policies in the Intertrace UI. Authoring inputs compile into bundles and into the agentgateway YAML. Agentgateway enforces connectivity and plane routing. Intertrace enforces security decisions. If that sentence flips—if agentgateway becomes the policy brain, or Preloop becomes the place customers click—we have forked the product. Customers should never see separate Preloop or agentgateway consoles. Hosts exist so we can separate marketing, platform, and mission. They are not an invitation to fork the UX.
What we refuse to outsource to the gateway process
Key verification, org isolation, ExtAuthz decisions, promptGuard, ExtMCP, authorize and execution permits, SecurityEventEnvelope ingest, and RLS-backed investigation remain Intertrace. The gateway may terminate TLS and route. It may not become the findings store. It may not mint org membership. It may not fail open because a dashboard URL was slow. Event emit is async so that Postgres latency is not TTFT.
Fail closed is part of the path
A security kernel that forwards to the model because ExtAuthz timed out is not a kernel. The Go enforcer fails closed on ExtAuthz, promptGuard, ExtMCP, and authorize-plus-permits. Stream buffer caps that would flush uninspected tokens fail closed. High-impact missing agent context fails closed. That makes the authz path a production dependency: it must be fast, keyed, and observable, or we will be blamed for outages we correctly caused.
verify-key is on that path. It is an internal POST, authenticated with INTERTRACE_INTERNAL_SECRET, not a public login. If the control plane is down, new keys cannot be verified unless we have a cache we are willing to defend. We do not paper over that with a debug allow-all. A local gateway that still has a hardcoded org is a development hazard, not a production fallback. Never point agentgateway at a verify-key URL that boots into allow all for debugging.
A request, end to end
An OpenAI-compatible client sets baseURL to https://intertrace.fly.dev/v1 and apiKey to an itr_rt_* secret. Fly terminates TLS. Agentgateway matches the LLM route from the compiled YAML. ExtAuthz runs the Go enforcer. The enforcer calls verify-key, binds org_id, loads agent context, runs promptGuard, and either forwards to the provider or returns 403. On the way, it may mint or check an execution permit for a consequential tool. When the response exists, outbound inspect can still block. A SecurityEventEnvelope leaves asynchronously. The human who later opens Traffic is a Clerk session on platform.intertrace.ai, reading the same org through RLS. Preloop did not sit on that path. Railway did not sit on that path. gateway.intertrace.ai did not sit on that path.
MCP and A2A are the same story with different routes. /mcp list and call are inspect points, including tool-return bodies. /a2a message/send is inspected in depth, not forwarded as opaque JSON-RPC. /service/* is the HTTP tool surface plus the per-tenant registry. Inference is the self-hosted pool path with the same hooks. All planes live means all of those routes exist on the pin, not that we have a slide that names them while only /v1 is configured.
What the compiler is for
lib/agentgateway/compile-config.ts and the production overlays under deploy/agentgateway/ are how ExtAuthz cluster names, plane routes, and fail-closed hooks stay in git. Hand-editing a live Fly config until it works is how you get a data plane nobody can reproduce. The smoke after npm run deploy:fly-agentgateway-full is how you prove the compiled file is the file that is running. If smoke only hits /v1 chat, it is not a full-plane smoke. The script name says full for a reason.
How to integrate today
- Mint a runtime key (itr_rt_*) for the org and environment. Do not send an org UUID as a header.
- Set the SDK base URL to https://intertrace.fly.dev/v1 (or the plane path: /mcp, /a2a, /service).
- Send Bearer itr_rt_*. Expect 403s when the enforcer denies. Do not retry those as if they were upstream 429s from the model vendor.
- Watch Traffic in the dashboard for SecurityEventEnvelope-backed gateway_events. If events are missing, the problem is ingest or emit, not a second gateway you should switch to.
- Treat npm run smoke:fly-agentgateway-full as the definition of done for edge changes, not a curl against gateway-connected.js.
If you are holding a bst_* key, the compatibility path may still route. That is not an invitation to build on it. Migration is: new key, new base_url, confirm events, revoke the old key. We will not publish a dual-write protocol that keeps Node and Fly equally smart. Equal smartness is how the pin dies.
Smoke is not a chat completion that returned 200. npm run smoke:fly-agentgateway-full should exercise the planes you claim are live: LLM deny and allow, MCP list or call inspect, a Service route if configured, A2A message/send if configured, and a verify-key failure that does not reach upstream. A smoke that only greets gpt-4o through Fly tells you TLS works. It does not tell you ExtAuthz is fail-closed.
Deploy ordering
Never roll an enforcer that cannot answer. Never roll agentgateway YAML that points ExtAuthz at the wrong place. Never roll a verify-key change that breaks prefix lookup without a freeze on key minting. The smoke is not optional because Fly deploys are fast. Fast deploys of a fail-closed kernel are how you get a fast, correct outage. Metrics on https://intertrace.fly.dev/metrics (or the configured scrape URL) are how you see gateway-added time after the smoke, not before.
Dashboard deploys (Vercel) and data-plane deploys (Fly) are coupled at the contract, not at the release button. Envelope shape changes require the gateway payload builder, app/api/gateway/event/route.ts, and typed mappers together. A dashboard-only change cannot invent a field the enforcer does not emit. An enforcer-only change cannot assume ingest will store a field the schema rejects. The product path includes that coordination. The Node proxy is not a shortcut around it.
What we will not do
- We will not list Railway, Node, and Fly as three production choices in customer-facing setup.
- We will not plan MCP, A2A, or Service features against gateway.intertrace.ai.
- We will not issue bst_* for new runtime onboarding.
- We will not send customers to Preloop or upstream agentgateway UIs.
- We will not treat a green local curl as evidence that v1.3.1 on Fly is healthy.
If you are integrating today, do not ask which Railway service to use. Ask for a runtime key and point at Fly. If you are implementing today, do not open gateway-v2 to add a detector. Open the enforcer that Fly already calls, the YAML compiler, and the smoke. The prototypes taught us what to pin. They are not what we pin.