Eloquent

Documentation

API Gateway

This guide covers how to build a production-ready API Gateway for the Eloquent platform. The API Gateway serves as the single HTTP entry point for all frontend-to-backend communication.

What is an API Gateway?

An API Gateway is a thin HTTP reverse proxy that serves as the single entry point for all frontend requests. It handles:

  • Authentication: JWT validation before requests reach backend
  • Authorization: Scope-based access control per route
  • Routing: Maps HTTP paths to backend services
  • Cross-cutting concerns: CORS, rate limiting, logging, caching

HTTP Proxy Pattern (Required)

IMPORTANT: The API Gateway MUST use the HTTP Proxy pattern. Each backend service exposes REST endpoints, and the gateway proxies requests directly to them.

Browser → API Gateway → HTTP → Backend Service (REST API)

Architecture

Frontend (Next.js App)
         │
    HTTP REST API
         │
         ▼
┌─────────────────────┐
│    API Gateway      │
│                     │
│  ┌───────────────┐  │
│  │  Middleware   │  │
│  │  1. Recovery  │  │
│  │  2. RequestID │  │
│  │  3. CORS      │  │
│  │  4. Logging   │  │
│  │  5. JWT Auth  │  │
│  │  6. Scopes    │  │
│  └───────────────┘  │
│         │           │
│   HTTP Reverse Proxy│
└─────────────────────┘
         │
    HTTP Internal
         │
         ▼
┌─────────────────────┐
│  Backend Services   │
│  (Go Microservices) │
└─────────────────────┘

Route Configuration

Routes are defined in routes.yaml:

version: "1.0"

public_paths:
  - /health
  - /ready
  - /api/v1/docs

routes:
  # READ operation (GET) - no scope required, just auth
  - path: /api/v1/agents
    method: GET
    proxy_upstream: $AGENTS_SERVICE_URL
    auth_required: true
    cache_ttl: 60s

  # WRITE operation (POST) - requires scope
  - path: /api/v1/agents
    method: POST
    proxy_upstream: $AGENTS_SERVICE_URL
    auth_required: true
    scopes:
      - write:agents

  # WRITE operation (PUT/DELETE) - requires scope
  - path: /api/v1/agents/{id}
    method: PUT
    proxy_upstream: $AGENTS_SERVICE_URL
    auth_required: true
    scopes:
      - write:agents

Route Configuration Options

OptionDescriptionExample
pathURL path (supports {param} placeholders)/api/v1/widgets/{id}
methodHTTP methodGET, POST, PUT, DELETE
proxy_upstreamBackend service URL (env var)$WIDGET_SERVICE_URL
auth_requiredRequire JWT authenticationtrue / false
scopesRequired JWT scopes["write:widgets"]
cache_ttlResponse cache duration60s, 5m
timeoutRequest timeout300s
descriptionDocumentation"Create widget"

JWT Authentication

Token Structure

{
  org_id: "org-uuid",
  user_id: "user-uuid",
  email: "user@example.com",
  role: "user",
  scopes: ["read", "write", "write:agents"],
  product: "eloquent"
}

Frontend Token Generation

The frontend generates JWT tokens via a Next.js API route:

// app/api/gateway-token/route.ts
import * as jose from "jose";

export async function GET() {
  const session = await auth();
  if (!session?.user?.email) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const secret = new TextEncoder().encode(process.env.JWT_SECRET);

  const scopes = [
    "read",
    "write",
    "write:agents",
    "write:workflows",
    "write:entities",
  ];

  const token = await new jose.SignJWT({
    org_id: orgId,
    user_id: user.id,
    email: user.email,
    role: "user",
    scopes,
    product: "eloquent",
  })
    .setProtectedHeader({ alg: "HS256" })
    .setIssuedAt()
    .setIssuer("eloquent-gateway")
    .setAudience("eloquent-api")
    .setExpirationTime("1h")
    .sign(secret);

  return NextResponse.json({ token, expiresIn: 3600 });
}

Header Forwarding

The gateway extracts JWT claims and forwards them as headers to backend services:

JWT ClaimForwarded Header
org_idX-Org-ID
user_idX-User-ID
emailX-User-Email
roleX-User-Role

Backend services use these headers for authorization:

func (h *Handler) CreateWidget(w http.ResponseWriter, r *http.Request) {
    orgID := r.Header.Get("X-Org-ID")
    userID := r.Header.Get("X-User-ID")

    if orgID == "" {
        writeError(w, http.StatusBadRequest, "MISSING_ORG_ID", "X-Org-ID required")
        return
    }
    // ... handle request
}

Error Responses

The gateway returns standardized error responses:

{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Invalid or expired token",
    "request_id": "req-abc123"
  },
  "metadata": {
    "success": false,
    "timestamp": "2024-03-01T10:00:00Z"
  }
}

Common Error Codes

StatusCodeDescription
401UNAUTHORIZEDMissing or invalid token
401EXPIRED_TOKENToken has expired
403FORBIDDENAccess denied
403INSUFFICIENT_SCOPEToken lacks required scope
429RATE_LIMITEDToo many requests
503SERVICE_UNAVAILABLEBackend service down

Adding a New Route

  1. Add route to routes.yaml:
- path: /api/v1/widgets
  method: POST
  proxy_upstream: $WIDGET_SERVICE_URL
  auth_required: true
  scopes:
    - write:widgets
  1. Add service URL to environment:
WIDGET_SERVICE_URL=http://widget-service:80
  1. Add scope to frontend token (if new):
const scopes = [
  // ... existing scopes
  "write:widgets",
];
  1. Rebuild gateway:
make docker-rebuild-api-gateway

Next Steps