Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt

Use this file to discover all available pages before exploring further.

Version: 1.0.1
Last Updated: 2026-04-09
Status: Stable
Target Audience: Developers and AI agents (GitHub Copilot, Cursor, general AI assistants)
Comprehensive guide for API development in the Encore Health OS Platform, covering API patterns, endpoint creation, authentication, authorization, error handling, response formatting, and API testing.

AI Agent Context

Key Patterns for AI:
  • Always use Supabase auto-generated APIs for standard CRUD (RLS enforced automatically)
  • Always use Edge Functions for complex business logic or cross-core operations
  • Always include organization_id in API requests for multi-tenant isolation
  • Always return consistent error formats with proper HTTP status codes
  • Always document API contracts in docs/architecture/integrations/API_CONTRACTS.md
Common Mistakes to Avoid:
  • Bypassing RLS by using service role keys in application code
  • Creating custom API endpoints when Supabase auto-generated APIs suffice
  • Not including organization_id in API requests (breaks multi-tenancy)
  • Returning inconsistent error formats
  • Not documenting API contracts before implementation

Quick Reference

API TypeLocationUse Case
Supabase Auto-generated/rest/v1/Standard CRUD operations
Edge Functionssupabase/functions/Complex business logic
API Contracts/api/v1/{core}/{resource}Cross-core integration

API Development Patterns

Pattern 1: Supabase Auto-generated APIs

Use for: Standard CRUD operations Access: Via Supabase client with RLS enforcement
// Standard CRUD - RLS automatically enforced
const { data } = await supabase
  .from('hr_employees')
  .select('*')
  .eq('organization_id', orgId);
Benefits:
  • Automatic RLS enforcement
  • No custom code needed
  • Type-safe with generated types

Pattern 2: Edge Functions

Use for: Complex business logic, third-party integrations, elevated privileges Location: supabase/functions/{function-name}/index.ts Example Structure:
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';

const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};

serve(async (req) => {
  // Handle CORS preflight
  if (req.method === 'OPTIONS') {
    return new Response(null, { headers: corsHeaders });
  }

  try {
    // Authenticate user
    const supabaseClient = createClient(
      Deno.env.get('SUPABASE_URL') ?? '',
      Deno.env.get('SUPABASE_ANON_KEY') ?? '',
      {
        global: {
          headers: { Authorization: req.headers.get('Authorization') ?? '' },
        },
      }
    );

    const { data: { user }, error: authError } = await supabaseClient.auth.getUser();
    if (authError || !user) {
      return new Response(
        JSON.stringify({ error: 'Unauthorized' }),
        { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
      );
    }

    // Process request
    const body = await req.json();
    // ... business logic ...

    return new Response(
      JSON.stringify({ success: true, data: result }),
      { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    );
  } catch (error) {
    return new Response(
      JSON.stringify({ error: error.message }),
      { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    );
  }
});

Pattern 3: API Contracts

Use for: Cross-core synchronous integration Standard: /api/v1/{core}/{resource} Documentation: See API Contracts

Creating API Endpoints

Edge Function Creation

1. Create Function:
supabase functions new {function-name}
2. Implement Function:
// supabase/functions/{function-name}/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';

const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};

serve(async (req) => {
  if (req.method === 'OPTIONS') {
    return new Response(null, { headers: corsHeaders });
  }

  // Authentication
  const supabaseClient = createClient(
    Deno.env.get('SUPABASE_URL') ?? '',
    Deno.env.get('SUPABASE_ANON_KEY') ?? '',
    {
      global: {
        headers: { Authorization: req.headers.get('Authorization') ?? '' },
      },
    }
  );

  const { data: { user }, error: authError } = await supabaseClient.auth.getUser();
  if (authError || !user) {
    return new Response(
      JSON.stringify({ error: { code: 'UNAUTHORIZED', message: 'Authentication required' } }),
      { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    );
  }

  // Process request
  try {
    const body = await req.json();
    
    // Validate organization context
    const orgId = body.organization_id || user.user_metadata?.organization_id;
    if (!orgId) {
      return new Response(
        JSON.stringify({ error: { code: 'MISSING_ORG', message: 'Organization context required' } }),
        { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
      );
    }

    // Business logic here
    const result = await processRequest(body, orgId, supabaseClient);

    return new Response(
      JSON.stringify({ data: result }),
      { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    );
  } catch (error) {
    return new Response(
      JSON.stringify({ 
        error: { 
          code: 'SERVER_ERROR', 
          message: error instanceof Error ? error.message : 'Unknown error' 
        } 
      }),
      { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    );
  }
});
3. Deploy Function:
supabase functions deploy {function-name}

API Contract Endpoint

For cross-core APIs, follow API Contract standards: Endpoint Pattern: /api/v1/{core}/{resource} Example: /api/v1/fa/episode-balance Documentation: Document in docs/architecture/integrations/API_CONTRACTS.md

API Authentication and Authorization

Authentication

All APIs MUST verify authentication:
// Get user from JWT token
const { data: { user }, error: authError } = await supabaseClient.auth.getUser();
if (authError || !user) {
  return new Response(
    JSON.stringify({ error: { code: 'UNAUTHORIZED', message: 'Authentication required' } }),
    { status: 401, headers: corsHeaders }
  );
}

Authorization

Verify organization access:
// Verify user has access to organization
const { data: userRole } = await supabaseClient
  .from('pf_user_roles')
  .select('organization_id, role')
  .eq('user_id', user.id)
  .eq('organization_id', orgId)
  .single();

if (!userRole) {
  return new Response(
    JSON.stringify({ error: { code: 'ACCESS_DENIED', message: 'Access denied' } }),
    { status: 403, headers: corsHeaders }
  );
}

Role-Based Authorization

// Check specific role
if (userRole.role !== 'org_admin' && userRole.role !== 'platform_admin') {
  return new Response(
    JSON.stringify({ error: { code: 'INSUFFICIENT_PERMISSIONS', message: 'Admin access required' } }),
    { status: 403, headers: corsHeaders }
  );
}

Organization and machine API access (PF-97)

Interactive APIs use the patterns above: Supabase user JWT + verified membership in the target organization_id. Machine / integration APIs (organization API keys, service accounts, OAuth client credentials) are not the same as passing a user session. They require explicit credential validation, org binding, and scope checks per PF-97. Do not use the service role key in browser or client apps; server-side exchange flows must mint short-lived tokens or use validated headers. See API Contracts (section Machine / organization API access) and the PF-97 integration doc.

API Error Handling

Standard Error Response Format

interface ErrorResponse {
  error: {
    code: string;        // Error code (e.g., 'EPISODE_NOT_FOUND')
    message: string;     // Human-readable message
    details?: {          // Optional additional context
      [key: string]: any;
    };
  };
}

HTTP Status Codes

StatusUse CaseError Code Example
400Bad RequestINVALID_DATE, INVALID_EPISODE_ID
401UnauthorizedUNAUTHORIZED
403ForbiddenACCESS_DENIED, INSUFFICIENT_PERMISSIONS
404Not FoundEPISODE_NOT_FOUND, RESOURCE_NOT_FOUND
429Rate LimitedRATE_LIMIT_EXCEEDED
500Server ErrorSERVER_ERROR

Error Handling Pattern

try {
  // Business logic
  const result = await processRequest(body);
  return new Response(
    JSON.stringify({ data: result }),
    { headers: corsHeaders }
  );
} catch (error) {
  // Handle specific errors
  if (error.code === 'NOT_FOUND') {
    return new Response(
      JSON.stringify({ 
        error: { 
          code: 'EPISODE_NOT_FOUND', 
          message: 'Episode not found' 
        } 
      }),
      { status: 404, headers: corsHeaders }
    );
  }

  // Generic error
  return new Response(
    JSON.stringify({ 
      error: { 
        code: 'SERVER_ERROR', 
        message: error instanceof Error ? error.message : 'Unknown error' 
      } 
    }),
    { status: 500, headers: corsHeaders }
  );
}

API Response Formatting

Success Response

// Standard success response
{
  data: {
    // Response data
  }
}

// With metadata
{
  data: {
    // Response data
  },
  meta: {
    count: number,
    page?: number,
    total?: number,
  }
}

Error Response

{
  error: {
    code: string,
    message: string,
    details?: {
      [key: string]: any;
    };
  }
}

Pagination Response

{
  data: T[],
  pagination: {
    page: number,
    pageSize: number,
    total: number,
    totalPages: number,
  }
}

API Testing

Unit Testing

Test business logic:
import { describe, it, expect } from 'vitest';

describe('API: processRequest', () => {
  it('processes valid request', async () => {
    const result = await processRequest(validRequest);
    expect(result).toBeDefined();
  });

  it('handles invalid input', async () => {
    await expect(processRequest(invalidRequest)).rejects.toThrow();
  });
});

Integration Testing

Test with real Supabase:
import { createAuthenticatedClient } from '../utils/supabase-test-client';

describe('API: Edge Function', () => {
  it('returns data for authenticated user', async () => {
    const client = createAuthenticatedClient(accessToken);
    const response = await fetch('/functions/v1/{function-name}', {
      headers: {
        Authorization: `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ organization_id: orgId }),
    });

    expect(response.status).toBe(200);
    const data = await response.json();
    expect(data.data).toBeDefined();
  });
});

E2E Testing

Test complete flow:
import { test, expect } from '@playwright/test';

test('API endpoint works end-to-end', async ({ page }) => {
  // Login
  await page.goto('/login');
  // ... login steps ...

  // Call API
  const response = await page.request.get('/api/v1/fa/episode-balance?episode_id=123');
  expect(response.status()).toBe(200);
  
  const data = await response.json();
  expect(data.data).toBeDefined();
});

Rate Limiting

Implementation

For API contracts, implement rate limiting:
// Rate limit: 100 requests/minute per organization
const rateLimitKey = `rate_limit:${orgId}`;
const currentCount = await redis.incr(rateLimitKey);
if (currentCount === 1) {
  await redis.expire(rateLimitKey, 60); // 1 minute window
}

if (currentCount > 100) {
  return new Response(
    JSON.stringify({ 
      error: { 
        code: 'RATE_LIMIT_EXCEEDED', 
        message: 'Rate limit exceeded' 
      } 
    }),
    { 
      status: 429,
      headers: {
        ...corsHeaders,
        'X-RateLimit-Limit': '100',
        'X-RateLimit-Remaining': '0',
        'X-RateLimit-Reset': String(Date.now() + 60000),
      }
    }
  );
}

Rate Limit Headers

Include in all responses:
headers: {
  'X-RateLimit-Limit': '100',
  'X-RateLimit-Remaining': String(100 - currentCount),
  'X-RateLimit-Reset': String(resetTime),
}

CORS Configuration

Standard CORS Headers

const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
};

CORS Preflight

Handle OPTIONS requests:
if (req.method === 'OPTIONS') {
  return new Response(null, { headers: corsHeaders });
}

Architecture

  • API Contracts - API contract standards and examples
  • Integration Patterns - Integration pattern overview

Development Guides

Standards

  • Constitution §1.3 - Integration patterns (Pattern 3: API Contracts)

Maintained By: Platform Foundation Team