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: 2.0.0
Last Updated: 2025-01-12
Constitution Reference: Section 1.3 (Integration Patterns)
This document provides concrete examples of each integration pattern used in the Encore Health OS Platform, demonstrating correct usage and common anti-patterns to avoid.
When to Use: Cross-cutting capabilities needed by multiple cores (forms, notifications, file uploads)
Structure: Shared utilities in /src/platform/<capability>/
Problem: Multiple cores need forms functionality, but cores cannot depend on each other.
Solution: Platform Foundation provides integration layer that wraps FW core functionality.
Implementation:
// ✅ CORRECT: Platform Integration Layer
// Location: /src/platform/forms/FormEmbed.tsx
import { useFormDefinition } from './useFormDefinition';
import { useFormSubmission } from './useFormSubmission';
import { FormRenderer } from './FormRenderer';
export function FormEmbed({ formId, onSuccess, onError }) {
const { data: form, isLoading } = useFormDefinition(formId);
const { submit, isSubmitting } = useFormSubmission({ formId });
if (isLoading) return <LoadingSpinner />;
if (!form) return <ErrorMessage>Form not found</ErrorMessage>;
return (
<FormRenderer
form={form}
onSubmit={async (data) => {
const submission = await submit(data);
onSuccess?.(submission);
}}
isSubmitting={isSubmitting}
/>
);
}
Usage in RH Core:
// ✅ CORRECT: Import from platform
// Location: /src/cores/rh/pages/RHFormsPage.tsx
import { ModuleFormsPage } from '@/platform/forms/ModuleFormsPage';
export default function RHFormsPage() {
return (
<ModuleFormsPage
owningCore="rh"
moduleDisplayName="Recovery Housing"
createFormPath="/fw/forms/new?core=rh"
/>
);
}
Usage in Other Cores:
// ✅ CORRECT: HR Core using the same platform integration layer
// Location: /src/cores/hr/pages/HRFormsPage.tsx
import { ModuleFormsPage } from '@/platform/forms/ModuleFormsPage';
export default function HRFormsPage() {
return (
<ModuleFormsPage
owningCore="hr"
moduleDisplayName="Human Resources"
createFormPath="/fw/forms/new?core=hr"
/>
);
}
Anti-Pattern (What NOT to Do):
// ❌ WRONG: Direct import from FW core
import { FormEmbed } from '@/cores/fw/components/FormEmbed';
// ❌ WRONG: Shared table between cores
CREATE TABLE shared_form_submissions (...); // Violates core boundaries
// ❌ WRONG: Core-to-core dependency
// In RH core
import { FormService } from '@/cores/fw/services/FormService';
Key Benefits:
- ✅ Cores remain isolated (no direct FW imports)
- ✅ Stable API contract (platform layer doesn’t change often)
- ✅ Single source of truth (all cores use same integration)
- ✅ Easy to test (mock platform layer, not FW core)
Example: PF-15 Data Lookup Integration Layer
Problem: Form fields need dynamic dropdowns populated from database tables, but cores cannot directly query each other’s tables.
Solution: Platform Foundation provides useTableLookup hook that safely queries whitelisted tables with automatic organization scoping.
Implementation:
// ✅ CORRECT: Platform Integration Layer
// Location: /src/platform/data-lookup/useTableLookup.ts
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { getLookupTableConfig } from './lookupTables';
export function useTableLookup(options: UseTableLookupOptions): UseTableLookupReturn {
const { table, organizationId, filters = {}, searchQuery = '' } = options;
// Get table configuration from whitelist
const tableConfig = getLookupTableConfig(table);
const { data, isLoading, error } = useQuery({
queryKey: ['platform-data-lookup', table, filters, searchQuery, organizationId],
queryFn: async () => {
if (!tableConfig) {
throw new Error(`Table "${table}" is not in the lookup whitelist`);
}
const labelColumn = tableConfig.labelColumn;
const valueColumn = tableConfig.valueColumn;
// Build query with automatic organization scoping
let query = supabase
.from(table as any)
.select(`${valueColumn}, ${labelColumn}`) as any;
// Apply organization scope if required
if (tableConfig.hasOrganizationScope && organizationId) {
query = query.eq('organization_id', organizationId);
}
// Apply additional filters
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
query = query.eq(key, value);
}
});
// Apply search if provided
if (searchQuery) {
query = query.ilike(labelColumn, `%${searchQuery}%`);
}
const { data, error } = await query;
if (error) throw error;
// Map to LookupOption format
return (data || []).map((row: any) => ({
value: String(row[valueColumn]),
label: String(row[labelColumn]),
}));
},
enabled: !!tableConfig,
staleTime: 5 * 60 * 1000, // 5 minutes
});
return { options: data || [], isLoading, error, search, refetch };
}
Usage in Form Fields:
// ✅ CORRECT: Use in form field configuration
// Location: Form field with lookup type
{
field_type: 'lookup',
field_key: 'assigned_department',
label: 'Department',
settings: {
lookupTable: 'hr_departments',
valueColumn: 'id',
labelColumn: 'name',
lookupFilters: { is_active: true },
searchable: true,
},
}
Anti-Pattern (What NOT to Do):
// ❌ WRONG: Direct query from FW core to HR table
const departments = await supabase
.from('hr_departments') // Direct access to HR core table!
.select('id, name')
.eq('organization_id', orgId);
// ❌ WRONG: Hardcoded options that need to be dynamic
const departments = [
{ value: '1', label: 'Operations' },
{ value: '2', label: 'Clinical' },
]; // Becomes stale, requires code changes
Key Benefits:
- ✅ Table whitelist enforcement (security)
- ✅ Automatic organization scoping (multi-tenant safety)
- ✅ RLS policy compliance
- ✅ Reusable across all cores
Problem: Multiple cores need to select employees (RH for staff assignments, FW for form assignees, etc.), but cores cannot depend on HR core.
Solution: Platform Foundation provides EmployeeSelector component and useEmployeeLookup hook.
Implementation:
// ✅ CORRECT: Platform Integration Layer
// Location: /src/platform/workforce/EmployeeSelector.tsx
import { useEmployeeLookup } from './useEmployeeLookup';
export function EmployeeSelector({
onSelect,
filterBySite,
filterByDepartment,
excludeEmployeeIds,
includeInactive,
placeholder = 'Select employee...',
}: EmployeeSelectorProps) {
const [open, setOpen] = useState(false);
const [selectedId, setSelectedId] = useState<string | undefined>();
const { employees, isLoading, searchEmployees } = useEmployeeLookup({
siteId: filterBySite,
departmentId: filterByDepartment,
employmentStatus: includeInactive ? undefined : 'active',
});
const filteredEmployees = employees.filter(
(emp) => !excludeEmployeeIds?.includes(emp.id)
);
const selectedEmployee = filteredEmployees.find((emp) => emp.id === selectedId);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="outline" role="combobox" aria-expanded={open}>
{selectedEmployee ? (
<div className="flex items-center gap-2">
<Avatar className="h-6 w-6">
<AvatarImage src={selectedEmployee.avatar_url} />
<AvatarFallback>
{selectedEmployee.full_name.split(' ').map((n) => n[0]).join('')}
</AvatarFallback>
</Avatar>
<span>{selectedEmployee.full_name}</span>
</div>
) : (
<>
<User className="mr-2 h-4 w-4" />
{placeholder}
</>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[400px] p-0">
<Command>
<CommandInput
placeholder="Search employees..."
onValueChange={searchEmployees}
/>
<CommandEmpty>
{isLoading ? 'Loading employees...' : 'No employees found.'}
</CommandEmpty>
<CommandGroup className="max-h-[300px] overflow-auto">
{filteredEmployees.map((employee) => (
<CommandItem
key={employee.id}
value={employee.id}
onSelect={() => {
setSelectedId(employee.id);
onSelect(employee);
setOpen(false);
}}
>
<div className="flex flex-col">
<span className="font-medium">{employee.full_name}</span>
<span className="text-muted-foreground text-sm">
{employee.employee_number} • {employee.job_title}
</span>
</div>
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
);
}
Usage in RH Core (Staff Assignment):
// ✅ CORRECT: Use EmployeeSelector in RH forms
// Location: /src/cores/rh/components/forms/InvestigationActionForm.tsx
import { EmployeeSelector } from '@/platform/workforce/EmployeeSelector';
export function InvestigationActionForm({ action, investigationId, onSuccess }: Props) {
const form = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: {
assigned_to: action?.assigned_to || '',
// ... other fields
},
});
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="assigned_to"
render={({ field }) => (
<FormItem>
<FormLabel>Assigned To</FormLabel>
<FormControl>
<EmployeeSelector
onSelect={(employee) => field.onChange(employee.id)}
filterBySite={currentSiteId}
placeholder="Select staff member..."
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* ... other fields */}
</form>
</Form>
);
}
Anti-Pattern (What NOT to Do):
// ❌ WRONG: Direct import from HR core
import { EmployeeSelector } from '@/cores/hr/components/EmployeeSelector';
// ❌ WRONG: Direct query to HR tables
const employees = await supabase
.from('hr_employees') // Direct access to HR core table!
.select('*')
.eq('organization_id', orgId);
Key Benefits:
- ✅ Consistent employee selection UI across all cores
- ✅ Automatic filtering by site, department, position
- ✅ Search functionality built-in
- ✅ No direct HR core dependency
Pattern 2: Event-Based Integration
When to Use: Asynchronous workflows, loose coupling between cores
Structure: Domain events published via pg_notify, consumed via triggers or edge functions
Example: FW-03 Automation Engine
Problem: Automation engine needs to react to form submissions without tight coupling.
Solution: Form submissions publish events, automation engine subscribes.
Implementation:
-- ✅ CORRECT: Event published via database trigger
-- Location: supabase/migrations/20251125032809_7fc8dff8-de99-4948-aaa3-52eb163fd4eb.sql
CREATE OR REPLACE FUNCTION trigger_automation_on_submission()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
_form_record RECORD;
BEGIN
-- Get form details for context
SELECT id, name, organization_id, site_id INTO _form_record
FROM fw_forms WHERE id = NEW.form_id;
-- Notify automation executor via pg_notify
PERFORM pg_notify(
'automation_trigger',
json_build_object(
'trigger_type', CASE
WHEN TG_OP = 'INSERT' THEN 'form_submitted'
WHEN TG_OP = 'UPDATE' THEN 'form_updated'
END,
'submission_id', NEW.id,
'form_id', NEW.form_id,
'organization_id', _form_record.organization_id,
'site_id', _form_record.site_id,
'submitted_by', NEW.submitted_by,
'submission_data', NEW.submission_data,
'old_status', CASE WHEN TG_OP = 'UPDATE' THEN OLD.status ELSE NULL END,
'new_status', NEW.status
)::text
);
RETURN NEW;
END;
$$;
-- Create triggers for form submissions
CREATE TRIGGER after_form_submission_insert
AFTER INSERT ON fw_form_submissions
FOR EACH ROW EXECUTE FUNCTION trigger_automation_on_submission();
CREATE TRIGGER after_form_submission_update
AFTER UPDATE ON fw_form_submissions
FOR EACH ROW EXECUTE FUNCTION trigger_automation_on_submission();
Consumer (Automation Engine Edge Function):
// ✅ CORRECT: Subscribe to events via edge function
// Location: /supabase/functions/automation-executor/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';
serve(async (req) => {
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);
// Subscribe to form submission events
const channel = supabase
.channel('form-submissions')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'fw_form_submissions',
},
async (payload) => {
// Process automation rules for this submission
await processAutomationRules(payload.new);
}
)
.subscribe();
return new Response(JSON.stringify({ status: 'listening' }), {
headers: { 'Content-Type': 'application/json' },
});
});
Example: RH-01 → FA-01 Integration (Planned)
Problem: When resident is admitted, billing account must be created automatically.
Solution: RH publishes resident_admitted event, FA subscribes and creates account.
Publisher (RH Core):
-- ✅ CORRECT: Event published on resident admission
-- Location: Migration for RH-01
CREATE OR REPLACE FUNCTION rh_publish_admission_event()
RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify(
'resident_admitted',
json_build_object(
'resident_id', NEW.id,
'organization_id', NEW.organization_id,
'site_id', NEW.site_id,
'bed_id', NEW.bed_id,
'admission_date', NEW.admission_date
)::text
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_resident_admission_event
AFTER INSERT ON rh_residents
FOR EACH ROW
WHEN (NEW.status = 'admitted')
EXECUTE FUNCTION rh_publish_admission_event();
Subscriber (FA Core Edge Function):
// ✅ CORRECT: Subscribe and create billing account
// Location: /supabase/functions/create-billing-account/index.ts
serve(async (req) => {
const supabase = createClient(...);
const channel = supabase
.channel('resident-admissions')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'rh_residents',
filter: 'status=eq.admitted',
},
async (payload) => {
const resident = payload.new;
// Create billing account for resident
const { data: account, error } = await supabase
.from('fa_resident_accounts')
.insert({
resident_id: resident.id,
organization_id: resident.organization_id,
site_id: resident.site_id,
account_status: 'active',
current_balance: 0,
})
.select()
.single();
if (error) {
console.error('Failed to create billing account:', error);
// Log error, but don't fail resident admission
}
}
)
.subscribe();
return new Response(JSON.stringify({ status: 'listening' }), {
headers: { 'Content-Type': 'application/json' },
});
});
Anti-Pattern (What NOT to Do):
// ❌ WRONG: Direct function call from RH to FA
// In RH core
import { createBillingAccount } from '@/cores/fa/services/BillingService';
async function admitResident(data) {
const resident = await createResident(data);
await createBillingAccount(resident.id); // Direct dependency!
return resident;
}
// ❌ WRONG: Shared database table
CREATE TABLE shared_resident_billing (
resident_id uuid REFERENCES rh_residents(id),
balance decimal,
-- Violates core boundaries
);
// ❌ WRONG: Synchronous API call (tight coupling)
const account = await fetch('/api/fa/create-account', {
method: 'POST',
body: JSON.stringify({ resident_id: resident.id })
}); // Blocks resident admission if FA is down
Key Benefits:
- ✅ Loose coupling (cores don’t know about each other)
- ✅ Resilient (if FA is down, resident admission still succeeds)
- ✅ Scalable (events can be processed asynchronously)
- ✅ Testable (mock events, not direct calls)
Pattern 3: API Contracts
When to Use: Synchronous request-response interactions
Structure: Versioned API endpoints (edge functions) with clear request/response schemas
Example: FA-01 Billing Balance Query (Planned)
Problem: RH needs to query resident billing balance synchronously.
Solution: FA provides versioned API endpoint, RH calls it.
Provider (FA Core Edge Function):
// ✅ CORRECT: Versioned API endpoint
// Location: /supabase/functions/get-resident-balance/index.ts
interface BalanceRequest {
resident_id: string;
organization_id: string;
as_of_date?: string;
}
interface BalanceResponse {
resident_id: string;
current_balance: number;
past_due_balance: number;
last_payment_date?: string;
account_status: 'current' | 'past_due' | 'delinquent';
}
serve(async (req) => {
const { resident_id, organization_id, as_of_date } = await req.json() as BalanceRequest;
// Validate organization access (RLS enforced in query)
const supabase = createClient(...);
const { data: account, error } = await supabase
.from('fa_resident_accounts')
.select('*, transactions(*)')
.eq('resident_id', resident_id)
.eq('organization_id', organization_id)
.single();
if (error || !account) {
return new Response(
JSON.stringify({ error: 'Account not found' }),
{ status: 404, headers: { 'Content-Type': 'application/json' } }
);
}
// Calculate balance
const balance = calculateBalance(account.transactions, as_of_date);
const response: BalanceResponse = {
resident_id: account.resident_id,
current_balance: balance.current,
past_due_balance: balance.past_due,
last_payment_date: balance.last_payment?.date,
account_status: balance.status,
};
return new Response(JSON.stringify(response), {
headers: { 'Content-Type': 'application/json' },
});
});
Consumer (RH Core):
// ✅ CORRECT: Call API endpoint
// Location: /src/cores/rh/hooks/useResidentBalance.ts
import { useQuery } from '@tanstack/react-query';
export function useResidentBalance(residentId: string) {
return useQuery({
queryKey: ['resident-balance', residentId],
queryFn: async () => {
const response = await fetch('/api/v1/fa/resident-balance', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
resident_id: residentId,
organization_id: currentOrg.id,
}),
});
if (!response.ok) {
throw new Error('Failed to fetch balance');
}
return response.json() as BalanceResponse;
},
});
}
API Contract Documentation:
// ✅ CORRECT: Contract defined in spec
// Location: /docs/architecture/INTEGRATION_CONTRACTS.md
/**
* GET /api/v1/fa/resident-balance
*
* Returns current billing balance for a resident.
*
* Request:
* {
* resident_id: uuid;
* organization_id: uuid;
* as_of_date?: timestamp; // Optional, defaults to now()
* }
*
* Response:
* {
* resident_id: uuid;
* current_balance: number;
* past_due_balance: number;
* last_payment_date?: timestamp;
* account_status: 'current' | 'past_due' | 'delinquent';
* }
*
* Errors:
* - 404: Account not found
* - 403: Access denied (RLS policy)
* - 500: Server error
*/
Anti-Pattern (What NOT to Do):
// ❌ WRONG: Direct database query from RH to FA tables
// In RH core
const balance = await supabase
.from('fa_resident_accounts') // Direct access to FA table!
.select('current_balance')
.eq('resident_id', residentId)
.single();
// ❌ WRONG: Unversioned API (breaking changes break consumers)
// /api/fa/balance (no version, can't evolve)
// ❌ WRONG: Tight coupling (RH depends on FA implementation)
import { BillingService } from '@/cores/fa/services/BillingService';
const balance = await BillingService.getBalance(residentId);
Key Benefits:
- ✅ Versioned APIs (can evolve without breaking consumers)
- ✅ Clear contracts (request/response schemas documented)
- ✅ RLS enforced (organization isolation at API level)
- ✅ Testable (mock API endpoints)
Pattern Selection Guide
- ✅ Multiple cores need the same capability
- ✅ Capability is cross-cutting (forms, notifications, documents)
- ✅ Stable API surface (doesn’t change often)
- ✅ Examples: Forms, Notifications, Document Management
When to Use Pattern 2 (Event-Based)
- ✅ Asynchronous workflows
- ✅ Loose coupling desired
- ✅ Resilience important (consumer can be down)
- ✅ Examples: Resident admission → Billing, Payment → Status update
When to Use Pattern 3 (API Contracts)
- ✅ Synchronous request-response needed
- ✅ Real-time data required
- ✅ Versioning important for evolution
- ✅ Examples: Balance queries, Census lookups
Common Anti-Patterns Summary
❌ Direct Core Imports
// ❌ WRONG
import { BillingService } from '@/cores/fa/services/BillingService';
Why Wrong: Violates Constitution Section 1.2 (cores cannot depend on each other)
Correct Approach: Use Platform Integration Layer, Events, or API Contracts
❌ Shared Database Tables
-- ❌ WRONG
CREATE TABLE shared_resident_billing (
resident_id uuid REFERENCES rh_residents(id),
balance decimal
);
Why Wrong: Table owned by multiple cores violates boundaries
Correct Approach: Each core owns its tables, integrate via events/APIs
❌ Hidden Dependencies
// ❌ WRONG: Implicit dependency on FA
function getResidentBalance(id: uuid) {
return supabase.from('fa_resident_accounts').select(...);
}
Why Wrong: Hidden dependency makes testing and evolution difficult
Correct Approach: Explicit integration contract (API, event, or Platform Layer)
Testing Integration Patterns
Pattern 1 Testing
// Mock platform layer, not FW core
jest.mock('@/platform/forms', () => ({
FormEmbed: jest.fn(() => <div>Mock Form</div>),
}));
Pattern 2 Testing
// Test event publishing and consumption
test('resident admission publishes event', async () => {
const resident = await admitResident({ ... });
await waitForEvent('resident_admitted', 5000);
// Verify event payload
});
Pattern 3 Testing
// Mock API endpoint
nock('https://api.example.com')
.post('/api/v1/fa/resident-balance')
.reply(200, { current_balance: 1000 });
Last Updated: 2025-01-12
Next Review: Quarterly (Q2 2025)