> ## Documentation Index
> Fetch the complete documentation index at: https://docs.grantex.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Grantex Gateway

> Zero-code reverse-proxy that enforces grant tokens in front of any API.

## Overview

`@grantex/gateway` is a standalone reverse-proxy that enforces Grantex grant tokens in front of any upstream API. Define routes and required scopes in a YAML config — no code required.

```bash theme={null}
npm install @grantex/gateway@0.3.0 @grantex/sdk@0.8.2
```

Node.js 22.12+ is required. Audience and trusted-amount configuration are
breaking changes; see [migration](/migration-enforcement). Gateway JWT checks
alone do not prove current grant revocation; wire current-state enforcement
at the protected service before side effects.

## Quick Start

**1. Create `gateway.yaml`:**

```yaml theme={null}
upstream: https://api.internal.example.com
jwksUri: https://your-auth-server/.well-known/jwks.json
port: 8080
upstreamHeaders:
  X-Internal-Auth: "secret-key"
routes:
  - path: /calendar/**
    methods: [GET]
    requiredScopes: [calendar:read]
  - path: /calendar/**
    methods: [POST, PUT, PATCH]
    requiredScopes: [calendar:write]
  - path: /payments/**
    methods: [POST]
    requiredScopes: [payments:initiate]
```

**2. Start the gateway:**

```bash theme={null}
npx @grantex/gateway --config gateway.yaml
```

**3. Make requests with grant tokens:**

```bash theme={null}
curl -H "Authorization: Bearer <grant-token>" \
  http://localhost:8080/calendar/events
```

## How It Works

```
Client → Gateway (verify token + check scopes) → Upstream API
```

1. **Route matching** — finds the first route matching the request method + path
2. **Token verification** — extracts the Bearer token, resolves keys from the configured JWKS endpoint, and verifies signature and claims locally
3. **Scope checking** — ensures the grant includes all required scopes, and (from version 0.2.0) that the token's `aud` claim is for this gateway; see [Grant token audience](#grant-token-audience)
4. **Proxy** — strips Authorization header, adds upstream headers + `X-Grantex-*` context, forwards to upstream
5. **Response** — returns the upstream response to the client

## YAML Config Reference

| Field | Type | Required | Default | Description |
| - | - | - | - | - |
| `upstream` | string | Yes | — | Base URL of the upstream API |
| `jwksUri` | string | Yes | — | JWKS endpoint used to retrieve keys for local signature verification |
| `port` | number | No | 8080 | Listen port |
| `upstreamHeaders` | object | No | — | Headers added to every upstream request |
| `grantexApiKey` | string | No | — | The gateway's own API key; needed for `currentAuthorityCheck` (or set `GRANTEX_API_KEY`) |
| `grantexBaseUrl` | string | No | `https://api.grantex.dev` | The issuer the current-authority check asks; set it for a self-hosted issuer |
| `currentAuthorityCheck` | boolean | No | `false` | `true` asks the issuer on every request whether the grant is still active, so a revoked or stopped grant is refused on the next request rather than at token expiry. The documented default for stop-sensitive deployments; needs an audience on every route and `audienceCheck: on`. See [Stop-sensitive deployments](#stop-sensitive-deployments) |
| `audience` | string | No | — | Grant token audience the gateway expects. From version 0.2.0 |
| `audienceCheck` | `on` \| `off` | No | `on` | `off` ignores the token's `aud`, as earlier releases did; cannot be combined with `audience`. From version 0.2.0 |
| `dataRegion` | string | No | none | The data region the upstream processes data in (for example `in`). With `dataRegionCheck: on`, a grant whose tools entries name another region is refused with `REGION_MISMATCH`, and a region-bound grant is refused with `REGION_UNCONFIGURED` when no region is set. A route's `dataRegion` overrides it |
| `dataRegionCheck` | `on` \| `off` | No | `off` | `on` checks a grant's `data_region`; `off` ignores it, as earlier releases did. Cannot be `off` with `dataRegion` set |
| `credentialReference` | `on` \| `off` | No | `off` | `on` redeems a `Grantex-Credential-Ref` request header (a reference from the [vault exchange](/api-reference/vault/exchange) with `delivery: reference`) with `grantexApiKey` against `grantexBaseUrl` and injects the credential upstream as `Authorization: Bearer`; the agent never holds the secret. `off` leaves the header alone, as earlier releases did |
| `routes` | array | Yes | — | Route definitions |

### Route Definition

| Field | Type | Description |
| - | - | - |
| `path` | string | URL pattern. `*` matches one segment, `**` matches any depth |
| `methods` | string\[] | HTTP methods (GET, POST, PUT, PATCH, DELETE) |
| `requiredScopes` | string\[] | Scopes that must be present in the grant |
| `audience` | string | Grant token audience for this route; overrides the top-level `audience`. From version 0.2.0 |

### Path Matching

| Pattern | Matches | Does Not Match |
| - | - | - |
| `/users/*` | `/users/123` | `/users/123/profile` |
| `/calendar/**` | `/calendar/events`, `/calendar/events/123/attendees` | `/api/calendar` |
| `/health` | `/health` | `/health/check` |

## Context Headers

The gateway adds these headers to every upstream request:

| Header | Value |
| - | - |
| `X-Grantex-Principal` | Principal ID (end-user) |
| `X-Grantex-Agent` | Agent DID |
| `X-Grantex-GrantId` | Grant ID |

Your upstream API can use these to apply fine-grained business logic without re-verifying the token.

## Credentials by reference

With `credentialReference: on`, an agent that exchanged its grant token for a credential
reference ([exchange](/api-reference/vault/exchange) with `delivery: "reference"`) presents it as
`Grantex-Credential-Ref: vcr_...`. The gateway redeems the reference for the request's grant with
its own API key ([resolve](/api-reference/vault/resolve)) and forwards the request with
`Authorization: Bearer <credential>`; the agent never holds the secret. The auth service refuses
a reference that belongs to another grant, has expired, or whose grant is revoked or stopped, and
the gateway then denies the request rather than forwarding it without the credential. A request
that presents no reference is proxied as before. With the check on, the header is not forwarded
upstream; off, the gateway treats it as any other request header, as earlier releases did.

```yaml theme={null}
credentialReference: on
grantexApiKey: gx_key_...
grantexBaseUrl: https://api.grantex.dev
```

## Error Responses

All errors return JSON with `error` and `message` fields:

| Status | Error Code | When |
| - | - | - |
| 404 | `ROUTE_NOT_FOUND` | No route matches the request |
| 401 | `TOKEN_MISSING` | No Bearer token in Authorization header |
| 401 | `TOKEN_INVALID` | Token signature verification failed |
| 401 | `TOKEN_EXPIRED` | Token has expired |
| 401 | `AUDIENCE_UNCONFIGURED` | Token carries `aud` and no `audience` is configured (0.2.0) |
| 401 | `AUDIENCE_MISMATCH` | Token's `aud` does not contain the route's or gateway's `audience`, or it has no `aud` (0.2.0) |
| 403 | `SCOPE_INSUFFICIENT` | Grant doesn't include required scopes |
| 502 | `UPSTREAM_ERROR` | Upstream API is unreachable |
| 400 | `CREDENTIAL_REF_INVALID` | `Grantex-Credential-Ref` is not a credential reference |
| 403 | `CREDENTIAL_REF_INVALID` | The auth service refused the reference: another grant's, expired, the grant no longer active, or unknown |
| 502 | `CREDENTIAL_RESOLVE_FAILED` | The auth service could not be reached, refused the gateway's key or answered without a credential |

## Grant token audience

<Warning>
  From version 0.2.0 (not in `@grantex/gateway` 0.1.5). This is a breaking
  change: a grant token that carries an `aud` claim is refused unless the
  gateway is configured with that audience. Tokens issued without an audience
  are unaffected.
</Warning>

The gateway checks the token's `aud` claim
([RFC 7519 section 4.1.3](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.3))
with the same semantics as `enforce()` in the SDKs, right after the signature
and before anything is forwarded. As in `enforce()`, the audience comes before
the scopes: a token for another relying party that also lacks the route's
scopes is refused with the audience code below, not `SCOPE_INSUFFICIENT`.

| Token `aud` | Expected audience | Result |
| - | - | - |
| none | none | forwarded |
| a string or an array of strings | none | 401 `AUDIENCE_UNCONFIGURED` |
| contains the expected audience | set | forwarded |
| does not contain it, or no `aud` | set | 401 `AUDIENCE_MISMATCH` |

The expected audience is the route's `audience`, or else the top-level
`audience`. `aud` matches when the expected audience is one of its values,
compared as exact strings. A token whose payload cannot be read is refused
with `TOKEN_INVALID`. Every one of these denials is logged with the method,
path and grant ID.

```yaml theme={null}
audience: https://api.merchant.example
routes:
  - path: /tools/**
    methods: [POST]
    requiredScopes: [tools:invoke]
    audience: https://tools.merchant.example
```

To keep the earlier behaviour while you find the audience, set
`audienceCheck: off`, and remove it once `audience` is set. An `audienceCheck`
other than `on` or `off`, an empty `audience`, or an `audience` together with
`audienceCheck: off` stops the gateway from starting.

## CLI Usage

```bash theme={null}
# Start with config file
npx @grantex/gateway --config gateway.yaml

# Short flag
npx @grantex/gateway -c gateway.yaml

# Default: looks for gateway.yaml in current directory
npx @grantex/gateway
```

## Library API

Use the gateway programmatically in your own server:

```typescript theme={null}
import { createGatewayServer, loadConfig } from '@grantex/gateway';

const config = loadConfig('./gateway.yaml');
const server = createGatewayServer(config);

await server.listen({ port: config.port, host: '0.0.0.0' });
```

## Docker Deployment

```dockerfile theme={null}
FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --ignore-scripts
COPY tsconfig*.json ./
COPY src/ src/
RUN npm run build

FROM node:20-slim
WORKDIR /app
COPY --from=builder /app/package*.json ./
RUN npm ci --omit=dev --ignore-scripts
COPY --from=builder /app/dist/ dist/
EXPOSE 8080
ENTRYPOINT ["node", "dist/cli.js"]
CMD ["--config", "/etc/grantex/gateway.yaml"]
```

```bash theme={null}
docker build -t grantex-gateway .
docker run -p 8080:8080 -v ./gateway.yaml:/etc/grantex/gateway.yaml grantex-gateway
```

## Example: Protecting a Calendar API

```yaml theme={null}
upstream: https://calendar-api.internal.svc
jwksUri: https://api.grantex.dev/.well-known/jwks.json
port: 8080
upstreamHeaders:
  X-Service-Key: "calendar-internal-key"
routes:
  - path: /calendars/*/events
    methods: [GET]
    requiredScopes: [calendar:read]
  - path: /calendars/*/events
    methods: [POST]
    requiredScopes: [calendar:write]
  - path: /calendars/*/events/*
    methods: [PUT, PATCH]
    requiredScopes: [calendar:write]
  - path: /calendars/*/events/*
    methods: [DELETE]
    requiredScopes: [calendar:delete]
```

## Ownership

Grantex is owned by Orchestrum Technologies LLP. Inventor and owner: Sanjeev Kumar. Ownership contact: [sanjeev@orchestrum.in](mailto:sanjeev@orchestrum.in) or [mishra.sanjeev@gmail.com](mailto:mishra.sanjeev@gmail.com).

## Stop-sensitive deployments

A gateway verifies a grant token's signature, claims and scopes locally. That
accepts a token until it expires: a grant revoked, or ended by an emergency stop,
a minute after the token was issued is still accepted for the rest of the token's
lifetime. With `currentAuthorityCheck: true` the gateway also asks the issuer, on
every request, whether the grant is still active, so a stopped or revoked grant is
refused on the next request. This is the documented default for stop-sensitive
deployments: anywhere an emergency stop must take effect before token expiry, turn
it on and treat a gateway without it as accepting stale authority for up to a
token lifetime. Set `GRANTEX_API_KEY` (or `grantexApiKey`), give every route an
audience, and keep `audienceCheck` on; `grantexBaseUrl` selects the issuer for a
self-hosted deployment. The issuer being unreachable denies the request: the
gateway never falls back to the signature alone.

```yaml theme={null}
audience: calendar-service
currentAuthorityCheck: true
grantexBaseUrl: https://api.grantex.dev
```

## Current Authority and Human Identity

Use the exact integration version and minimum primary SDK in [Release Status](/release-status).
Per-invocation issuer authority and trusted principal/agent binding are opt-in.
Configure the callback with the trusted issuer and derive identity from the host's
authenticated session; signature and scope checks alone do not prove current
revocation or human consent. See [SDK execution authority](/guides/sdk-execution-authority)
for configuration, denial behavior and manifest/decision/caps boundaries.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.