> ## 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.

# API Development Guide

> Version: 1.0.1 Last Updated: 2026-04-09 Status: Stable Target Audience: Developers and AI agents (GitHub Copilot, Cursor, general AI assistants)

**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 Type                | Location                    | Use Case                 |
| ----------------------- | --------------------------- | ------------------------ |
| Supabase Auto-generated | `/rest/v1/`                 | Standard CRUD operations |
| Edge Functions          | `supabase/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

```typescript theme={null}
// 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:**

```typescript theme={null}
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](../architecture/integrations/API_CONTRACTS.md)

***

## Creating API Endpoints

### Edge Function Creation

**1. Create Function:**

```bash theme={null}
supabase functions new {function-name}
```

**2. Implement Function:**

```typescript theme={null}
// 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:**

```bash theme={null}
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:**

```typescript theme={null}
// 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:**

```typescript theme={null}
// 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

```typescript theme={null}
// 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](../../specs/pf/specs/PF-97-multi-tenant-organization-api-access.md)**. 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](../architecture/integrations/API_CONTRACTS.md)** (section *Machine / organization API access*) and the **[PF-97 integration doc](../architecture/integrations/multi-tenant-organization-api-access-integration.md)**.

***

## API Error Handling

### Standard Error Response Format

```typescript theme={null}
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

| Status | Use Case     | Error Code Example                          |
| ------ | ------------ | ------------------------------------------- |
| 400    | Bad Request  | `INVALID_DATE`, `INVALID_EPISODE_ID`        |
| 401    | Unauthorized | `UNAUTHORIZED`                              |
| 403    | Forbidden    | `ACCESS_DENIED`, `INSUFFICIENT_PERMISSIONS` |
| 404    | Not Found    | `EPISODE_NOT_FOUND`, `RESOURCE_NOT_FOUND`   |
| 429    | Rate Limited | `RATE_LIMIT_EXCEEDED`                       |
| 500    | Server Error | `SERVER_ERROR`                              |

### Error Handling Pattern

```typescript theme={null}
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

```typescript theme={null}
// Standard success response
{
  data: {
    // Response data
  }
}

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

### Error Response

```typescript theme={null}
{
  error: {
    code: string,
    message: string,
    details?: {
      [key: string]: any;
    };
  }
}
```

### Pagination Response

```typescript theme={null}
{
  data: T[],
  pagination: {
    page: number,
    pageSize: number,
    total: number,
    totalPages: number,
  }
}
```

***

## API Testing

### Unit Testing

**Test business logic:**

```typescript theme={null}
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:**

```typescript theme={null}
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:**

```typescript theme={null}
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:**

```typescript theme={null}
// 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:**

```typescript theme={null}
headers: {
  'X-RateLimit-Limit': '100',
  'X-RateLimit-Remaining': String(100 - currentCount),
  'X-RateLimit-Reset': String(resetTime),
}
```

***

## CORS Configuration

### Standard CORS Headers

```typescript theme={null}
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:**

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

***

## Related Documentation

### Architecture

* [API Contracts](../architecture/integrations/API_CONTRACTS.md) - API contract standards and examples
* Integration Patterns - Integration pattern overview

### Development Guides

* [Database Development Guide](./DATABASE_DEVELOPMENT_GUIDE.md) - Database queries and RLS
* [Testing Setup and Run Guide](../testing/TESTING_SETUP_AND_RUN.md) - API testing patterns
* [Troubleshooting Guide](./TROUBLESHOOTING_GUIDE.md) - API debugging

### Standards

* [Constitution](../../constitution.md) §1.3 - Integration patterns (Pattern 3: API Contracts)

***

**Maintained By:** Platform Foundation Team
