Integration Patterns

How you integrate FLXBL depends on your application architecture. This guide covers the most common patterns: Backend-for-Frontend (BFF), direct mobile/SPA access, and serverless functions.

Pattern 1: Backend-for-Frontend (BFF)

Recommended for: Web applications with server-side rendering (Nuxt, Next.js, SvelteKit, Remix)

Your web server acts as a secure proxy between the browser and FLXBL:

  • Stores tokens in HTTP-only cookies (not accessible to JavaScript)
  • Implements your application's authorization logic
  • Can use admin API key for privileged operations
100% Drag to pan
flowchart LR
    subgraph browser [Browser]
        UI[Your Web App]
    end
    
    subgraph bff [Your Server]
        Cookies["HTTP-Only Cookies"]
        Auth[Auth Middleware]
        RBAC[Your RBAC Logic]
    end
    
    subgraph flxbl [FLXBL]
        Identity[Identity API]
        Dynamic[Dynamic API]
    end
    
    UI -->|"Requests"| Auth
    Auth <-->|"Tokens"| Cookies
    Auth -->|"Login/register"| Identity
    RBAC -->|"End-user token"| Dynamic
    RBAC -->|"Admin key"| Dynamic
BFF pattern: Server handles tokens, browser never sees them

Why BFF?

Benefit Description
Token security HTTP-only cookies can't be stolen by XSS attacks
Centralized auth One place for all authentication and authorization logic
API key protection Admin API keys stay on your server, never exposed to browser
Token refresh Server handles refresh automatically without client complexity

Implementation: Login Endpoint

// server/api/auth/login.post.ts (Nuxt example)
export default defineEventHandler(async (event) => {
  const { email, password } = await readBody(event);
  
  // Call FLXBL Identity API
  const response = await $fetch(`${FLXBL_URL}/api/v1/identity/${tenantId}/login`, {
    method: 'POST',
    body: { identifier: email, password }
  });
  
  // Store tokens in HTTP-only cookies (not accessible to JavaScript)
  setCookie(event, 'access_token', response.accessToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: response.expiresIn
  });
  
  setCookie(event, 'refresh_token', response.refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 60 * 60 * 24 * 7 // 7 days
  });
  
  // Return user info (without tokens)
  return { user: response.user };
});

Implementation: Auth Middleware

// server/middleware/auth.ts
export default defineEventHandler(async (event) => {
  // Skip public routes
  if (isPublicRoute(event.path)) return;
  
  const accessToken = getCookie(event, 'access_token');
  
  if (!accessToken) {
    throw createError({ statusCode: 401, message: 'Not authenticated' });
  }
  
  // Fetch user profile from FLXBL
  const user = await $fetch(`${FLXBL_URL}/api/v1/identity/${tenantId}/me`, {
    headers: { Authorization: `Bearer ${accessToken}` }
  });
  
  // YOUR authorization logic - FLXBL stores roles, you enforce them
  const requiredRole = getRequiredRole(event.path);
  if (requiredRole && !hasMinimumRole(user.role, requiredRole)) {
    throw createError({ statusCode: 403, message: 'Insufficient permissions' });
  }
  
  // Attach user to request context
  event.context.user = user;
});

// Your role hierarchy
const ROLE_LEVELS = { viewer: 1, editor: 2, admin: 3 };

function hasMinimumRole(userRole, requiredRole) {
  return (ROLE_LEVELS[userRole] || 0) >= (ROLE_LEVELS[requiredRole] || 0);
}

Implementation: Data Proxy

Proxy data requests to FLXBL, adding the user's token from the cookie:

// server/api/data/[...path].ts
// Proxy data requests to FLXBL with user's token
export default defineEventHandler(async (event) => {
  const accessToken = getCookie(event, 'access_token');
  const path = event.context.params.path;
  
  // Forward request to FLXBL Dynamic API
  const response = await $fetch(`${FLXBL_URL}/api/v1/dynamic/${path}`, {
    method: event.method,
    headers: { Authorization: `Bearer ${accessToken}` },
    body: event.method !== 'GET' ? await readBody(event) : undefined
  });
  
  return response;
});

Token Refresh Strategy

Handle token expiration gracefully by refreshing when needed:

// Token refresh logic (BFF example)
async function refreshTokens(event) {
  const refreshToken = getCookie(event, 'refresh_token');
  
  if (!refreshToken) {
    throw createError({ statusCode: 401, message: 'No refresh token' });
  }
  
  try {
    const response = await $fetch(`${FLXBL_URL}/api/v1/identity/${tenantId}/refresh`, {
      method: 'POST',
      body: { refreshToken }
    });
    
    // Update access token cookie
    setCookie(event, 'access_token', response.accessToken, {
      httpOnly: true,
      secure: true,
      sameSite: 'strict',
      maxAge: response.expiresIn
    });
    
    return response.accessToken;
  } catch (error) {
    // Refresh token expired - user must log in again
    deleteCookie(event, 'access_token');
    deleteCookie(event, 'refresh_token');
    throw createError({ statusCode: 401, message: 'Session expired' });
  }
}

Pattern 2: Direct API Access (Mobile/SPA)

Recommended for: Mobile apps, trusted single-page applications

Your app communicates directly with FLXBL:

  • Stores tokens in secure storage (Keychain, SecureStore)
  • Uses end-user JWT for all operations
  • Never exposes admin API keys to client code
100% Drag to pan
flowchart LR
    subgraph mobile [Mobile App]
        App[Your App]
        Storage["Secure Storage"]
    end
    
    subgraph flxbl [FLXBL]
        Identity[Identity API]
        Dynamic[Dynamic API]
    end
    
    App -->|"Login"| Identity
    Identity -->|"Tokens"| Storage
    App -->|"End-user JWT"| Dynamic
Direct access: App stores tokens securely and calls FLXBL directly

Security Considerations

Do Don't
Use SecureStore/Keychain for tokens Store tokens in AsyncStorage/localStorage
Implement token refresh logic Assume tokens never expire
Use HTTPS for all requests Hardcode API keys in client code
Clear tokens on logout Leave tokens after user logs out

Implementation: React Native / Expo

// React Native / Expo example
import * as SecureStore from 'expo-secure-store';

async function login(email: string, password: string) {
  const response = await fetch(`${FLXBL_URL}/api/v1/identity/${tenantId}/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ identifier: email, password })
  });
  
  const { accessToken, refreshToken, user } = await response.json();
  
  // Store tokens in secure storage (Keychain on iOS, Keystore on Android)
  await SecureStore.setItemAsync('access_token', accessToken);
  await SecureStore.setItemAsync('refresh_token', refreshToken);
  
  return user;
}

async function fetchData(endpoint: string) {
  const accessToken = await SecureStore.getItemAsync('access_token');
  
  const response = await fetch(`${FLXBL_URL}/api/v1/dynamic/${endpoint}`, {
    headers: { Authorization: `Bearer ${accessToken}` }
  });
  
  // Handle 401 by refreshing token
  if (response.status === 401) {
    await refreshAccessToken();
    return fetchData(endpoint); // Retry
  }
  
  return response.json();
}

Authorization in Mobile Apps

Since you can't hide logic in a mobile app, authorization should be:

  1. Enforced on the server — FLXBL returns only data the user can access
  2. UI-level only on client — Hide buttons/screens based on role, but don't rely on it for security
// Check role for UI purposes (not security)
function canEditProduct(user) {
  return ['editor', 'admin'].includes(user.role);
}

// The real protection is on your backend or in FLXBL scopes
<Button 
  disabled={!canEditProduct(user)}
  onPress={handleEdit}
>
  Edit Product
</Button>

Pattern 3: Serverless / Edge Functions

Recommended for: Cloudflare Workers, Vercel Edge, AWS Lambda, backend services

Your functions use API keys for all operations:

  • No end-user authentication (API key represents your service)
  • API keys scoped to specific entities and operations
  • Service accounts for audit trails
100% Drag to pan
flowchart LR
    subgraph edge [Edge/Serverless]
        Function["Your Function"]
        EnvVars["Environment Variables"]
    end
    
    subgraph flxbl [FLXBL]
        Dynamic[Dynamic API]
    end
    
    EnvVars -->|"API Key"| Function
    Function -->|"Scoped requests"| Dynamic
Serverless pattern: Functions authenticate with scoped API keys

Implementation

// Cloudflare Worker / Vercel Edge Function
export default async function handler(request: Request) {
  // API key from environment variables
  const apiKey = process.env.FLXBL_API_KEY;
  
  // Make request to FLXBL with scoped API key
  const response = await fetch(`${FLXBL_URL}/api/v1/dynamic/Product`, {
    method: 'GET',
    headers: {
      'Authorization': `Bearer ${apiKey}`,
      'Content-Type': 'application/json'
    }
  });
  
  return new Response(await response.text(), {
    headers: { 'Content-Type': 'application/json' }
  });
}

Best Practices for Serverless

  • Use scoped API keys — One key per function with minimal permissions
  • Store keys in environment variables — Never hardcode in source
  • Consider service accounts — For better audit trails
  • Set key expiration — Rotate keys periodically

Choosing the Right Pattern

Application Type Pattern Why
Next.js / Nuxt / SvelteKit BFF Best security with SSR, HTTP-only cookies
React Native / Flutter Direct Secure storage available, no server needed
SPA (React, Vue) BFF or Direct BFF preferred; Direct if no backend available
Cloudflare Workers Serverless No user auth, just API key access
Cron jobs / Webhooks Serverless Service account for audit, scoped key
Admin dashboard BFF with admin key Use API key for privileged operations

Common Implementation Details

Environment Variables

# .env (never commit this file)
FLXBL_URL=https://api.flxbl.dev
FLXBL_TENANT_ID=your_tenant_id
FLXBL_API_KEY=flxbl_your_api_key  # For server-side only

Error Handling

// Handle common FLXBL errors
async function handleFlxblRequest(request) {
  const response = await fetch(request);
  
  switch (response.status) {
    case 401:
      // Token expired or invalid
      throw new AuthError('Not authenticated');
    case 403:
      // Valid token but insufficient permissions
      throw new ForbiddenError('Insufficient permissions');
    case 404:
      // Entity or record not found
      throw new NotFoundError('Resource not found');
    case 429:
      // Rate limited
      throw new RateLimitError('Too many requests');
    default:
      return response.json();
  }
}

Next Steps