API Gateway
The outbound half of the gateway is the Hub's capability resolver; this is the inbound half. Every request passes ten stages — edge limit, match, authenticate, resolve tenant, authorise, rate limit, validate, invoke, normalise, observe — before it reaches a module, and nothing throws: every refusal is a receipt carrying a status, the stage that refused and a sentence. The edge limit is the only ceiling a refused request meets; every other class is reached after authorisation has already passed. The service key scheme is real; Supabase JWT verification is a declared port with nothing wired behind it.
This is not the Hub’s gateway. lib/integration/gateway.tsresolves a capability to a connector — it is the estate’s outbound router and knows nothing about HTTP. This module is the inbound HTTP door, and it reaches external platforms only by asking that resolver. Neither holds a provider client.
HTTP API Gateway34
ReadingReading live figures from /api/v1/health…
Rate limits are counted per server process. A multi-instance deployment multiplies every published ceiling by the instance count. Satisfy the RateLimiter port with a shared store before relying on these numbers across instances.
Published Routes34
| Method | Path | Permission | Rate class | Returns |
|---|---|---|---|---|
| GET | /api/v1Version metadata and the route manifest. | Public | public | GatewayIndex |
| GET | /api/v1/healthGateway, route, authentication and integration health. Aggregate figures only. | Public | public | GatewayHealth |
| GET | /api/v1/openapiThe OpenAPI 3.1 document, generated from this route table. | Public | public | OpenApiDocument |
| GET | /api/v1/configThe GoTrue project URL and publishable key a browser needs to sign in. Public by design; carries no server credential. | Public | public | GatewayConfig |
| GET | /api/v1/companiesCompanies in the caller's tenant, with derived health and counts. | company:read | read | CompanySummary[] |
| GET | /api/v1/companies/{id}One company by id or slug. | company:read | read | CompanyDetail |
| POST | /api/v1/companiesCreate a company workspace. The blueprint — executives and departments — is generated server-side by the same function the UI uses; projects, agents, knowledge and reports start empty. | company:write | write | Company |
| PATCH | /api/v1/companies/{id}Replace a company's editable fields — name, icon, industry, business model, mission, goals and status. The slug is not editable. Requires the company's current updatedAt and refuses with 409 if it has moved. | company:write | write | Company |
| DELETE | /api/v1/companies/{id}Remove a company by id or slug. A hard delete: everything the schema owns from it goes with it by cascade — departments, executive seats, workflows and their approval gates, company-scoped activity, and agent execution history. Memory records survive with their company cleared. | company:write | write | RemovedCompany |
| GET | /api/v1/departmentsThe department roster and its derived state. | company:read | read | DepartmentSummary[] |
| GET | /api/v1/agentsThe agent fleet roster. | agent:read | read | AgentSummary[] |
| GET | /api/v1/workflowsWorkflows with their derived lifecycle state. | workflow:read | read | WorkflowSummary[] |
| GET | /api/v1/workflows/{id}One workflow, with stages, tasks and gates. | workflow:read | read | WorkflowDetail |
| POST | /api/v1/workflowsOpen a workflow from a template. Structure only — no history, no assignees, no progress. | workflow:write | write | WorkflowDetail |
| POST | /api/v1/workflows/{id}/gates/{gateId}Approve, reject or waive an approval gate. Records the decision and an activity line. | workflow:write | write | WorkflowDetail |
| POST | /api/v1/workflows/{id}/releaseRelease a draft workflow into running. A no-op on anything but a draft. | workflow:write | write | WorkflowDetail |
| GET | /api/v1/memoryMemory records. Supports a substring query across title and body. | memory:read | read | MemoryRecord[] |
| GET | /api/v1/knowledge-graphKnowledge Graph rollup. Read model only — the graph has no write API by design. | company:read | read | GraphRollup |
| GET | /api/v1/decisionsDecision Engine output: recommendations, alerts and the risk register. | analytics:read | read | DecisionDigest |
| GET | /api/v1/analyticsEnterprise Intelligence read models: company health, enterprise metrics, agent and workflow performance. | analytics:read | read | IntelligenceDigest |
| GET | /api/v1/integrationsThe connector estate as the Integration Hub reports it. | integration:read | read | IntegrationDigest |
| GET | /api/v1/integrations/healthConnector health and the Hub's own gateway summary. | integration:read | read | IntegrationHealth |
| GET | /api/v1/integrations/installsThis tenant's own connector overrides, undecorated. Empty where no durable estate exists yet. | integration:read | read | ConnectorOverride[] |
| POST | /api/v1/integrations/{key}/lifecycleRun one lifecycle operation — install, enable, disable, configure, reconnect, remove, fork or drop a connection, update, rollback — against one connector, durably. | integration:write | dispatch | LifecycleOutcome |
| POST | /api/v1/integrations/{key}/{capability}Route a capability call through the Integration Hub. The gateway never names a provider — the Hub resolves which connector serves it. | integration:write | dispatch | DispatchOutcome |
| GET | /api/v1/marketplace/pluginsThe plugin catalogue: every manifest the Plugin Registry holds, plus the declared integration classes that have none. | integration:read | read | MarketplaceCatalogue |
| GET | /api/v1/marketplace/plugins/{key}One plugin in full, with its compatibility, dependency and permission reports and a dry-run install pre-flight. | integration:read | read | MarketplacePlugin |
| GET | /api/v1/marketplace/installedPlugins installed in this tenant's estate. | integration:read | read | MarketplaceCatalogue |
| POST | /api/v1/marketplace/installRun the install pre-flight — manifest, compatibility, dependencies, permissions, visibility — and the lifecycle transition. Returns the outcome; does not persist. | integration:write | dispatch | MarketplaceOutcome |
| POST | /api/v1/marketplace/uninstallRemove a plugin from the estate. Its manifest stays in the registry. Returns the outcome; does not persist. | integration:write | dispatch | MarketplaceOutcome |
| POST | /api/v1/marketplace/updateTake a plugin's published version. Never automatic — this route is the explicit operator action. Returns the outcome; does not persist. | integration:write | dispatch | MarketplaceOutcome |
| POST | /api/v1/runtime/executionsExecute a registered agent through the AI Runtime. Returns the structured execution result — every admission gate, the lifecycle state reached, and the Hub's own dispatch outcome. | agent:execute | dispatch | ExecutionResult |
| POST | /api/v1/orchestrator/runsCreate a Task Orchestrator run. The run is queued as `pending` with the due time you supply; it is not claimed, leased or executed by this call. Attribution is always the calling principal — delegation is not supported. | workflow:write | write | CreatedRun |
| POST | /api/v1/webhooks/{tenant}/{company}/{ref}Inbound delivery. Authenticated by provider signature inside the Webhook Manager's pipeline, not by a gateway credential. | Public | inbound | WebhookReceipt |
5 routes are public. Every other route requires both a role that grants its permission and a credential whose scopes do not exclude it — scopes intersect with the role and can only narrow it, so a key can never grant more than its holder has.
Permissions by Role11
| Permission | viewer | operator | admin | owner |
|---|---|---|---|---|
company:readCompanies, departments, executives and the knowledge graph projection. | ● | ● | ● | ● |
company:writeHeld by `companies.create` and `companies.remove` as of §33 P2. No permission was minted for them: this one already existed and is granted to ADMIN only, so creating a company is an administrator's act. | · | · | ● | ● |
agent:readThe agent roster and its derived state. | ● | ● | ● | ● |
agent:executeRun a registered agent through the AI Runtime. Required by runtime.execute — the reservation this permission was written for, now taken up. | · | ● | ● | ● |
workflow:readWorkflows, stages, tasks and approval gates. | ● | ● | ● | ● |
workflow:writeReserved. Start, pause, cancel and retry land here when the engine has a server store. | · | ● | ● | ● |
memory:readMemory records across every scope the caller may see. | ● | ● | ● | ● |
memory:writeReserved. | · | · | ● | ● |
integration:readThe connector estate, its health and its event routing. | ● | ● | ● | ● |
integration:writeRoute a capability call through the Integration Hub to a connector. | · | ● | ● | ● |
analytics:readEnterprise Intelligence read models and the Decision Engine's outputs. | ● | ● | ● | ● |
Rate Classes6
edgeEdge (pre-authentication)1,200 per 60sApplied per client address before the route is matched or the credential is read. The only ceiling that covers a request the gateway REFUSES — every other class is reached after match, authentication, tenancy and authorisation have all passed, so without this one a caller could send unlimited 401s and 404s. Deliberately well above any per-route ceiling: it bounds abuse, it does not shape traffic.
publicPublic120 per 60sUnauthenticated routes. Keyed by client address, because there is no principal to key on.
readRead600 per 60sRead models are computed in-process from static rosters; the ceiling exists to bound a runaway caller, not to protect a database.
writeWrite60 per 60sReserved for the write routes v1 does not yet publish.
dispatchIntegration dispatch30 per 60sLowest ceiling in the table. A dispatch becomes a real external call the moment the Hub's seam is wired, and every connector has its own rate ceiling behind this one.
inboundInbound webhook1,200 per 60sHigh by design. A platform retry storm that gets rate-limited turns into a longer retry storm; the Webhook Manager's own dedupe ledger is the real defence.
Rate limits are counted per server process. A multi-instance deployment multiplies every published ceiling by the instance count. Satisfy the RateLimiter port with a shared store before relying on these numbers across instances.
Error Catalogue13
| Code | Status | Retryable | Meaning |
|---|---|---|---|
unsupported_version | 404 | final | That API version does not exist. |
not_found | 404 | final | No route matches that path. |
method_not_allowed | 405 | final | That method is not allowed on this route. |
unauthenticated | 401 | final | Authentication is required. |
forbidden | 403 | final | This credential does not hold the permission this route requires. |
tenant_mismatch | 403 | final | That resource belongs to another tenant. |
unsupported_media_type | 415 | final | The request body must be application/json. |
payload_too_large | 413 | final | The request body exceeds the maximum accepted size. |
invalid_request | 400 | final | The request failed validation. |
conflict | 409 | final | The resource changed since it was read. |
rate_limited | 429 | retry | Too many requests. Retry after the interval given. |
upstream_unavailable | 503 | retry | A module this route depends on could not answer. |
internal | 500 | retry | The gateway could not complete the request. |
The status is a property of the code rather than a choice made at the refusal site, so whether a client retries is decided once in a table a reviewer can read. Nothing in the gateway throws — every refusal is a receipt carrying one of these codes.
Versions1
v1v1current34 routesThe first published surface. Read routes over the platform read models, plus capability dispatch through the Integration Hub.
A version is a field on a route, not a directory tree. Introducing v2 is a second row here plus routes carrying version: "v2" — existing v1 rows are untouched, so v1 cannot break as a side effect of adding v2.