DAY 19 WEEK 4: INTEGRATION

BFF & API Patterns

Master the Backend-for-Frontend architecture: proxy routing, session management, CSRF protection, error handling, and LLM proxy capabilities.

The BFF (Backend-for-Frontend) is the gateway between your plugins and backend services. It handles authentication, routes requests, manages sessions, and provides a unified API surface. Understanding the BFF is essential for building secure, performant plugins.

📋Learning Objectives

  • Understand BFF architecture and responsibilities
  • Configure proxy routes to backend services
  • Master session management with secure cookies
  • Implement CSRF protection in plugins
  • Handle API errors consistently
  • Use the LLM proxy for AI features

🏗️BFF Architecture

The BFF sits between the Experience Platform and backend services, providing security, routing, and aggregation.

flowchart TB subgraph CLIENT["Browser"] EXP["Experience Platform"] PLUGIN["Plugins"] end subgraph BFF["BFF (bff.self.empowernow.ai)"] PROXY["Proxy Router"] SESSION["Session Manager"] CSRF["CSRF Validator"] AUTH["Auth Interceptor"] LLM["LLM Proxy"] end subgraph BACKEND["Backend Services"] IDP["IdP :8002"] PDP["PDP :8001"] MEMB["Membership :8003"] CRUD["CRUD Service"] end subgraph EXTERNAL["External"] OPENAI["OpenAI"] ANTHRO["Anthropic"] end EXP --> BFF PLUGIN --> BFF PROXY --> IDP PROXY --> PDP PROXY --> MEMB PROXY --> CRUD LLM --> OPENAI LLM --> ANTHRO style BFF fill:#0ea5e922,stroke:#0ea5e9 style CLIENT fill:#a855f722,stroke:#a855f7 style BACKEND fill:#10b98122,stroke:#10b981

BFF Responsibilities

🔀

Proxy Routing

Routes requests to appropriate backend services

🔐

Authentication

Handles OIDC flows, stores tokens securely

🍪

Session Management

Manages secure httpOnly cookies + Redis

🛡️

CSRF Protection

Validates tokens for state-changing requests

🤖

LLM Proxy

Multi-provider AI routing with cost tracking

📊

Aggregation

Combine multiple backend calls into one response

🔀Proxy Routing

The BFF routes requests based on URL path prefixes. Each route maps to a backend service.

Route Configuration

# BFF proxy configuration (bff/config/routes.yaml)
routes:
  - path: /api/membership/**
    target: http://membership:8003
    strip_prefix: true
    auth_required: true
    rate_limit:
      requests: 100
      window: 60s
    
  - path: /api/pdp/**
    target: http://pdp:8001
    strip_prefix: true
    auth_required: true
    
  - path: /api/idp/**
    target: http://idp:8002
    strip_prefix: true
    auth_required: false     # IdP handles its own auth
    
  - path: /api/crud/**
    target: http://crudservice:8004
    strip_prefix: true
    auth_required: true
    inject_headers:
      X-Request-ID: "{{ request_id }}"
      X-User-ID: "{{ user.sub }}"

How Routing Works

Plugin Request BFF Routes To
/api/membership/users/me/roles http://membership:8003/users/me/roles
/api/pdp/access/v1/evaluation http://pdp:8001/access/v1/evaluation
/api/idp/userinfo http://idp:8002/userinfo
/api/crud/workflows/run http://crudservice:8004/workflows/run

Plugin API Allowlist

Plugins can only call endpoints explicitly allowed in their plugins.yaml:

# plugins.yaml - API permissions
permissions:
  api:
    - method: GET
      path: /api/membership/users/{id}/roles
    - method: GET
      path: /api/membership/roles/{id}
    - method: POST
      path: /access/v1/evaluation
    - method: POST
      path: /access/v1/evaluations
  sse:
    - /api/events/membership/roles
⚠️ 403 If Not Allowed

If a plugin calls an endpoint not in its permissions.api list, the BFF returns 403 Forbidden. Always add new endpoints to plugins.yaml before using them.

🍪Session Management

The BFF manages user sessions with secure, httpOnly cookies backed by Redis.

sequenceDiagram participant User participant Browser participant BFF participant IdP participant Redis User->>Browser: Login Browser->>BFF: /auth/login BFF->>IdP: Authorization Request IdP-->>Browser: Login Page User->>Browser: Enter Credentials Browser->>IdP: Submit IdP-->>BFF: Auth Code BFF->>IdP: Exchange for Tokens IdP-->>BFF: Access + Refresh + ID Tokens BFF->>Redis: Store tokens (session:abc123) BFF-->>Browser: Set-Cookie: session=abc123 (httpOnly, secure) Note over Browser,BFF: Subsequent requests Browser->>BFF: /api/membership/... (Cookie: session=abc123) BFF->>Redis: Get tokens for session:abc123 Redis-->>BFF: Tokens BFF->>BFF: Attach Authorization header

Session Configuration

# Session settings
session:
  cookie_name: empowernow_session
  cookie_secure: true           # HTTPS only
  cookie_httponly: true         # No JavaScript access
  cookie_samesite: lax          # CSRF protection
  max_age: 86400                # 24 hours
  
  store:
    type: redis
    url: redis://redis:6379/1
    prefix: session:
    ttl: 86400                  # Match cookie max_age

Token Refresh

The BFF automatically refreshes tokens before they expire:

// BFF token refresh logic (simplified)
async function getTokensForSession(sessionId: string) {
  const session = await redis.get(`session:${sessionId}`);
  
  // Check if access token expires within 60 seconds
  if (session.access_token_expires_at - Date.now() < 60000) {
    // Refresh tokens
    const newTokens = await idpClient.refreshTokens(session.refresh_token);
    
    // Update session
    await redis.set(`session:${sessionId}`, {
      ...session,
      access_token: newTokens.access_token,
      access_token_expires_at: Date.now() + newTokens.expires_in * 1000
    });
    
    return newTokens.access_token;
  }
  
  return session.access_token;
}
💡 Plugins Don't See Tokens

Tokens are stored in httpOnly cookies—JavaScript can't access them. This protects against XSS attacks. The SDK's sdk.api.fetch() automatically includes cookies with every request.

🛡️CSRF Protection

Cross-Site Request Forgery attacks trick users into making unwanted requests. The BFF requires CSRF tokens for state-changing operations.

How CSRF Protection Works

  1. BFF generates a CSRF token and stores it in the session
  2. Token is returned in a response header or cookie
  3. Plugin includes token in X-CSRF-Token header for POST/PUT/DELETE
  4. BFF validates token matches session

Automatic CSRF with sdk.api.fetch()

// The SDK handles CSRF automatically!
const sdk = (globalThis as any).pluginSdk;

// POST request - CSRF token automatically included
const response = await sdk.api.fetch('/api/crud/workflows/run', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    workflow: 'access-request',
    input: { resource_id: 'res-123' }
  })
});

// GET requests don't need CSRF tokens
const data = await sdk.api.fetch('/api/membership/users/me/roles');

Manual CSRF (rare cases)

// If you need to make a raw fetch (not recommended)
const getCsrfToken = async () => {
  const response = await fetch('/api/csrf-token', {
    credentials: 'include'
  });
  const data = await response.json();
  return data.csrf_token;
};

const manualPost = async (url: string, body: any) => {
  const csrfToken = await getCsrfToken();
  
  return fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': csrfToken      // Must include for POST
    },
    body: JSON.stringify(body),
    credentials: 'include'           // Include session cookie
  });
};
⚠️ 403 on Missing CSRF

If you make a POST/PUT/DELETE without a valid CSRF token, the BFF returns 403. Always use sdk.api.fetch() which handles this automatically.

⚠️Error Handling Patterns

Consistent error handling makes your plugin robust and user-friendly.

Standard Error Response

// BFF returns standardized error format
{
  "error": {
    "code": "PERMISSION_DENIED",
    "message": "You don't have permission to access this resource",
    "details": {
      "required_permission": "documents:read",
      "resource": "/api/documents/doc-123"
    },
    "request_id": "req-abc-123"    // For debugging
  }
}

Handling Errors in Plugins

// Comprehensive error handling
const sdk = (globalThis as any).pluginSdk;

async function fetchWithErrorHandling(url: string) {
  try {
    const response = await sdk.api.fetch(url);
    
    if (!response.ok) {
      const errorData = await response.json().catch(() => ({}));
      
      switch (response.status) {
        case 401:
          // Session expired - redirect to login
          window.location.href = '/auth/login?returnTo=' + encodeURIComponent(window.location.pathname);
          throw new Error('Session expired');
          
        case 403:
          // Permission denied
          const msg = errorData.error?.message || 'Access denied';
          throw new Error(msg);
          
        case 404:
          throw new Error('Resource not found');
          
        case 429:
          // Rate limited - show retry message
          const retryAfter = response.headers.get('Retry-After') || '60';
          throw new Error(`Rate limited. Retry after ${retryAfter} seconds`);
          
        case 500:
        case 502:
        case 503:
          throw new Error('Service temporarily unavailable');
          
        default:
          throw new Error(errorData.error?.message || `HTTP ${response.status}`);
      }
    }
    
    return await response.json();
  } catch (err: any) {
    if (err.name === 'TypeError' && err.message.includes('fetch')) {
      // Network error
      throw new Error('Network error. Check your connection.');
    }
    throw err;
  }
}

HTTP Status Code Reference

Status Meaning Plugin Action
401 Unauthorized (session invalid) Redirect to login
403 Forbidden (no permission) Show "access denied" message
404 Not found Show "not found" state
422 Validation error Show field-level errors
429 Rate limited Show retry message, implement backoff
5xx Server error Show generic error, retry option

🤖LLM Proxy

The BFF includes an LLM proxy for AI-powered features with multi-provider support and cost tracking.

LLM Proxy Features

  • Multi-provider: OpenAI, Anthropic, Azure OpenAI
  • Dynamic routing: Automatic fallback if primary provider fails
  • Budget enforcement: Switch to cheaper models when budget exceeded
  • Cost tracking: Track spending per user/tenant with receipts
  • Streaming: SSE support for real-time responses

Using the LLM Proxy

// Call LLM via BFF proxy
const chatCompletion = async (messages: any[]) => {
  const response = await sdk.api.fetch('/api/llm/chat/completions', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      model: 'gpt-4o',           // Or 'claude-3-sonnet', etc.
      messages: messages,
      temperature: 0.7,
      max_tokens: 1000
    })
  });
  
  const data = await response.json();
  return data.choices[0].message.content;
};

// Streaming responses
const streamChat = async (messages: any[], onChunk: (text: string) => void) => {
  const response = await sdk.api.fetch('/api/llm/chat/completions', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      model: 'gpt-4o',
      messages: messages,
      stream: true
    })
  });
  
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    
    const chunk = decoder.decode(value);
    // Parse SSE format
    const lines = chunk.split('\n').filter(line => line.startsWith('data: '));
    for (const line of lines) {
      const json = JSON.parse(line.slice(6));
      if (json.choices?.[0]?.delta?.content) {
        onChunk(json.choices[0].delta.content);
      }
    }
  }
};
🤖 Week 5: AI Deep Dive

Learn more about LLM proxy configuration, AI agent authorization (ARIA), budget management, and cost tracking in Week 5, Days 22-23.

🔧Troubleshooting

Issue: 403 on API calls

# Most common causes:
# 1. Endpoint not in plugins.yaml permissions.api
# 2. Missing CSRF token for POST/PUT/DELETE
# 3. Session expired

# Check plugins.yaml has the endpoint:
permissions:
  api:
    - method: POST
      path: /api/crud/workflows/run   # Add this

# Verify using sdk.api.fetch (handles CSRF):
await sdk.api.fetch('/api/crud/workflows/run', {
  method: 'POST',
  body: JSON.stringify(data)
});

Issue: Session cookie not sent

# Symptom: 401 on every request
# Cause: credentials: 'include' missing in fetch

# Wrong:
fetch('/api/membership/users/me')

# Correct (use SDK):
sdk.api.fetch('/api/membership/users/me')

# Or with raw fetch:
fetch('/api/membership/users/me', { credentials: 'include' })

Issue: CORS errors

# Symptom: "No 'Access-Control-Allow-Origin' header"
# Cause: Calling backend directly instead of through BFF

# Wrong (direct to backend):
fetch('http://membership:8003/users/me')

# Correct (through BFF):
sdk.api.fetch('/api/membership/users/me')

Issue: Rate limited (429)

# Implement exponential backoff
const fetchWithRetry = async (url: string, maxRetries = 3) => {
  for (let i = 0; i < maxRetries; i++) {
    const response = await sdk.api.fetch(url);
    
    if (response.status !== 429) {
      return response;
    }
    
    const retryAfter = parseInt(response.headers.get('Retry-After') || '1');
    await new Promise(r => setTimeout(r, retryAfter * 1000 * Math.pow(2, i)));
  }
  
  throw new Error('Rate limit exceeded after retries');
};

Knowledge Check

1 Why can't plugins access tokens directly?

Tokens are stored in httpOnly cookies, which JavaScript cannot access. This protects against XSS attacks—even if malicious script runs in the browser, it can't steal the token. The BFF retrieves tokens from Redis using the session ID from the cookie.

2 When is a CSRF token required?

CSRF tokens are required for state-changing requests: POST, PUT, PATCH, DELETE. GET requests don't need them because they should be safe (no side effects). The SDK's sdk.api.fetch() handles CSRF automatically.

3 What happens if a plugin calls an endpoint not in permissions.api?

The BFF returns 403 Forbidden. The plugin allowlist in plugins.yaml acts as a security boundary—plugins can only call explicitly permitted endpoints. This prevents plugins from accessing sensitive APIs they shouldn't.

4 How does the BFF route /api/membership/users/me?

Based on route config, the BFF:

  1. Matches path /api/membership/**
  2. Strips prefix /api/membership
  3. Forwards to http://membership:8003/users/me
  4. Adds Authorization header with user's access token
5 What's the benefit of the LLM proxy over direct API calls?
  • Security: API keys never exposed to browser
  • Multi-provider: Switch between OpenAI/Anthropic transparently
  • Budget control: Enforce spending limits per user/tenant
  • Cost tracking: Detailed receipts and analytics
  • Fallback: Automatic failover to backup providers

📝Day 19 Checkpoint

  • Understand BFF architecture and responsibilities
  • Know how proxy routing works
  • Understand session management with cookies + Redis
  • Know when CSRF tokens are required
  • Can handle API errors consistently
  • Know how to use the LLM proxy
🎉 What You Learned

The BFF is the security gateway for all plugin API calls. It handles authentication, session management, CSRF protection, and provides a unified API surface. Understanding these patterns is essential for building secure, production-ready plugins.