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.

This directory contains security documentation for the Encore Health OS Platform.

Overview

Encore Health OS implements defense-in-depth security with multiple layers:
  1. Authentication & Authorization (PF-02 RBAC)
  2. Multi-Tenant Data Isolation (Row-Level Security)
  3. Input Validation & Sanitization
  4. Audit Logging (PF-04)
  5. Edge Function Security
  6. Database Security

Security Documents

Reporting Security

Guide to the Reporting Engine’s security measures:
  • Parameterized query execution (SQL injection prevention)
  • Query complexity validation
  • Enhanced audit logging
  • Database function security (SECURITY DEFINER)
  • Incident response procedures
Key Controls:
  • Max 5 JOINs, 3 subqueries per query
  • 10,000 row limit, 30-second timeout
  • Blocks dangerous keywords (DROP, ALTER, GRANT, etc.)
  • All executions logged with full SQL and parameters

Automation Security

Security architecture for the Forms & Workflow Automation Engine:
  • Trigger isolation (organization/site scoped)
  • RLS enforcement on all actions
  • Action execution controls (email, webhook, record update)
  • Safe condition evaluation (injection prevention)
  • Incident response playbook
Key Controls:
  • Email sender validation
  • HTTPS-only webhooks
  • Dynamic value resolution without code execution
  • Rate limiting recommendations
  • Comprehensive execution logging

Security Principles

1. Least Privilege

  • Users only access data within their organization
  • Role-based permissions (platform_admin → readonly)
  • SECURITY DEFINER functions minimize privilege escalation

2. Defense in Depth

  • Multiple security layers (RLS + application + edge function)
  • Input validation at every boundary
  • Audit logging for accountability

3. Fail Secure

  • Errors don’t leak sensitive data
  • Default deny (RLS policies must explicitly grant access)
  • Graceful degradation (failed emails don’t break workflows)

4. Zero Trust

  • Every request validated (JWT auth + RLS)
  • No implicit trust between modules
  • Service role used only in edge functions

Row-Level Security (RLS)

Multi-Tenant Isolation Pattern

Every business table follows this pattern:
CREATE POLICY "org_isolation"
ON table_name
FOR SELECT
TO authenticated
USING (
  organization_id IN (
    SELECT organization_id 
    FROM pf_user_roles 
    WHERE user_id = auth.uid()
  )
);
Key Points:
  • All tables have organization_id column
  • RLS enabled on all business tables
  • Site-scoped data also includes site_id
  • SECURITY DEFINER functions prevent recursion

Critical Anti-Pattern: Avoid RLS Recursion

❌ WRONG: Recursive RLS (causes infinite loop)
CREATE POLICY "document_org_access" ON pf_documents
FOR SELECT USING (
  organization_id IN (
    SELECT organization_id FROM pf_user_roles 
    WHERE user_id = auth.uid()  -- pf_user_roles has RLS!
  )
);
✅ CORRECT: Use SECURITY DEFINER function
CREATE FUNCTION has_org_access(org_id uuid, user_id uuid)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
  RETURN EXISTS (
    SELECT 1 FROM pf_user_roles
    WHERE pf_user_roles.organization_id = org_id
      AND pf_user_roles.user_id = user_id
  );
END;
$$;

CREATE POLICY "document_org_access" ON pf_documents
FOR SELECT USING (has_org_access(organization_id, auth.uid()));

Edge Function Security

Authentication

  • JWT verification enabled by default (verify_jwt = true)
  • System services disable JWT (verify_jwt = false)
  • Service role used for cron jobs

Secrets Management

  • Never hardcode secrets in code
  • Use Supabase environment variables
  • Secrets accessed via Deno.env.get('SECRET_NAME')

Example: Secure Edge Function

import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';

Deno.serve(async (req) => {
  // 1. Verify authentication
  const authHeader = req.headers.get('Authorization');
  if (!authHeader) {
    return new Response('Unauthorized', { status: 401 });
  }

  // 2. Create client with user's JWT
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_ANON_KEY')!,
    { global: { headers: { Authorization: authHeader } } }
  );

  // 3. RLS automatically enforces access control
  const { data, error } = await supabase
    .from('fw_forms')
    .select('*');

  // 4. User only sees forms in their organization
  return new Response(JSON.stringify(data), {
    headers: { 'Content-Type': 'application/json' }
  });
});

Input Validation

Client-Side (React Hook Form + Zod)

const formSchema = z.object({
  email: z.string().email(),
  age: z.number().min(18).max(120),
  role: z.enum(['staff', 'admin', 'readonly'])
});

// TypeScript enforces types
const { register, handleSubmit } = useForm<FormData>({
  resolver: zodResolver(formSchema)
});

Server-Side (Edge Functions)

// Validate parameters
const body = await req.json();
if (!body.report_id || typeof body.report_id !== 'string') {
  return new Response('Invalid report_id', { status: 400 });
}

// Sanitize SQL parameters (use parameterized queries)
const { data } = await supabase.rpc('execute_report_query', {
  sql: 'SELECT * FROM residents WHERE name = $1',
  params: [body.user_input] // Safe: treated as literal value
});

Database (CHECK Constraints & Validation Triggers)

-- Use validation triggers (not CHECK constraints) for time-based validations
CREATE TRIGGER validate_expiration
BEFORE INSERT OR UPDATE ON hr_employee_credentials
FOR EACH ROW
EXECUTE FUNCTION validate_expiration_date();

Audit Logging (PF-04)

Automatic Logging (Triggers)

All critical tables have audit triggers:
CREATE TRIGGER audit_fw_forms
AFTER INSERT OR UPDATE OR DELETE ON fw_forms
FOR EACH ROW
EXECUTE FUNCTION log_audit_event();
What Gets Logged:
  • User ID (auth.uid())
  • Organization ID
  • Action (insert, update, delete)
  • Table name and record ID
  • OLD and NEW values (full JSONB)
  • Timestamp

Manual Logging (Application Code)

await supabase.rpc('log_audit_event', {
  action: 'resident_admitted',
  module: 'rh',
  table_name: 'rh_residents',
  record_id: residentId,
  new_values: { phase: 'intake', bed_id: bedId }
});

Incident Response

Security Incident Classification

Critical (P0):
  • Data breach or unauthorized access
  • SQL injection exploit
  • Authentication bypass
  • Cross-organization data leak
High (P1):
  • RLS policy misconfiguration
  • Exposed secrets
  • Privilege escalation
  • Malicious automation rule
Medium (P2):
  • Failed login attempts (brute force)
  • Excessive automation failures
  • Suspicious audit log patterns
Low (P3):
  • Non-critical validation errors
  • Quota limit exceeded

Incident Response Procedures

1. Detection

  • Monitor audit logs for suspicious activity
  • Alert on failed authentication (> 10 attempts/hour)
  • Track automation execution failures
  • Review report execution logs for SQL injection attempts

2. Containment

  • Disable affected user accounts
  • Pause automation rules
  • Block malicious IP addresses
  • Rotate compromised secrets

3. Investigation

  • Query pf_audit_logs for full timeline
  • Review edge function logs
  • Check database RLS policies
  • Analyze network traffic

4. Recovery

  • Restore from backup if data corrupted
  • Re-enable services after patching
  • Notify affected users (if required by law)

5. Post-Incident

  • Document root cause
  • Update security policies
  • Implement additional controls
  • Train team on lessons learned

Security Checklist for New Features

Before deploying any new feature, verify:

Database

  • All tables have organization_id
  • RLS policies enabled
  • SECURITY DEFINER functions used (avoid recursion)
  • Validation triggers for time-based checks
  • Audit triggers attached
  • Indexes on organization_id for performance

Edge Functions

  • JWT verification enabled (unless system service)
  • Input validation on all parameters
  • Parameterized queries (no string interpolation)
  • Error messages don’t leak sensitive data
  • Secrets loaded from environment variables
  • CORS configured correctly

Frontend

  • Forms use Zod validation
  • TypeScript types enforce data structure
  • No hardcoded secrets
  • Error boundaries catch exceptions
  • User feedback doesn’t expose internals

Integration

  • Cross-core communication via Platform Layer
  • Event payloads include organization_id
  • Webhooks use HTTPS only
  • External API calls use secure credentials

Testing

  • RLS tests verify tenant isolation
  • SQL injection tests confirm prevention
  • Error handling tests cover edge cases
  • Performance tests meet SLAs

Threat Model

Threats Mitigated

SQL Injection
  • Parameterized queries in reporting engine
  • No dynamic SQL construction
  • Database function restrictions
Cross-Tenant Data Access
  • RLS policies enforce isolation
  • All queries scoped to organization_id
  • Service role only in trusted edge functions
Privilege Escalation
  • Role-based access control (RBAC)
  • SECURITY DEFINER functions minimize scope
  • Manager access limited to direct reports
Code Injection
  • Dynamic value resolution uses safe patterns
  • No eval() or Function() constructors
  • HTTPS-only webhooks
Sensitive Data Exposure
  • PHI/PII not logged in free text
  • Error messages sanitized
  • Audit logs use structured data

Threats Not Yet Mitigated

⚠️ Rate Limiting
  • No global rate limits on API calls
  • No per-user/org limits on reports
  • Mitigation: Monitor usage, add limits in production
⚠️ Advanced Persistent Threats (APT)
  • No intrusion detection system (IDS)
  • No anomaly detection
  • Mitigation: Use Supabase security advisory alerts
⚠️ DDoS Protection
  • Relies on Supabase infrastructure
  • No custom DDoS mitigation
  • Mitigation: Leverage Supabase’s CDN and rate limiting

Security Resources

Internal

External


Contact

For security concerns or to report vulnerabilities: Do not disclose vulnerabilities publicly until patched.
Last Updated: 2025-11-25
Next Review: 2026-02-25 (Quarterly)