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 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
// 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
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
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 }
);
}
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;
};
}
}
{
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),
}
}
);
}
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