Skip to main content

Source: ocean/docs/adr/015-regional-tenant-provisioning.md | ✏️ Edit on GitHub

ADR-015: Regional Tenant Provisioning Architecture

Status

Accepted

Date

2025-08-12

Context

Our SaaS platform needs to provision isolated resources for each customer organization including:

  • Stripe customer accounts for billing
  • Neon database instances for data isolation
  • Regional deployment based on user preferences for compliance and performance

The provisioning must be:

  • Asynchronous to avoid blocking user signup
  • Resilient to failures with retry capabilities
  • Region-aware for data residency requirements
  • Secure with proper secret management

Decision

We implement a parallel asynchronous provisioning system triggered during user signup that:

  1. Captures regional preference during signup via the dataRegion field
  2. Provisions resources in parallel using Promise.allSettled() for resilience
  3. Uses Edge Functions for serverless execution with automatic scaling
  4. Stores secrets securely in Supabase secrets for Edge Function access

Architecture Components

Implementation Details

  1. Signup Flow (/supabase/functions/graphql-v2/resolvers/base.ts)

    signUp: async (args) => {
    // 1. Create user with regional preference
    const user = await supabase.auth.signUp({
    email: input.email,
    options: {
    data: {
    hosting_region: input.dataRegion, // Captured preference
    },
    },
    })

    // 2. Organization created by database trigger

    // 3. Async provisioning (fire and forget)
    provisionResources(org.id, user, supabase).catch((err) => {
    logger.error('Provisioning error:', err)
    // Log but don't fail signup
    })
    }
  2. Provisioning Service (/supabase/functions/graphql-v2/services/provisioning.ts)

    export async function provisionResources(orgId, user, supabase) {
    await Promise.allSettled([
    // Stripe Customer
    createStripeCustomer(orgId, user),

    // Neon Database with regional mapping
    createNeonProject({
    name: `org-${orgId}`,
    region: mapDataRegionToNeon(user.user_metadata.hosting_region),
    }),
    ])
    }
  3. Regional Mapping

    function mapDataRegionToNeon(region: string): string {
    const regionMap = {
    'us-east': 'aws-us-east-1',
    'us-west': 'aws-us-west-2',
    'eu-central': 'aws-eu-central-1',
    'eu-west': 'aws-eu-west-1',
    'ap-southeast': 'aws-ap-southeast-1',
    'ap-northeast': 'aws-ap-northeast-1',
    }
    return regionMap[region] || 'aws-us-east-2' // Default
    }
  4. Secret Management

    • API keys stored in Supabase secrets (not environment variables)
    • Edge Functions access via Deno.env.get()
    • Deployment workflow sets secrets automatically
    • Connection strings stored in Supabase vault

Database Schema

-- Organizations table tracks provisioning status
ALTER TABLE organizations ADD COLUMN stripe_customer_id TEXT;
ALTER TABLE organizations ADD COLUMN neon_tenant_id TEXT;
ALTER TABLE organizations ADD COLUMN neon_database_host TEXT;
ALTER TABLE organizations ADD COLUMN neon_database_ready BOOLEAN DEFAULT false;

-- Provisioning events for monitoring
CREATE TABLE provisioning_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID REFERENCES organizations(id),
event_type TEXT NOT NULL,
status TEXT NOT NULL,
metadata JSONB,
error TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
completed_at TIMESTAMPTZ
);

Error Handling & Resilience

  1. Parallel Execution: Uses Promise.allSettled() so one failure doesn't block others
  2. Validation & Retry: validateOrganizationReady() checks and retries missing resources
  3. Logging: Comprehensive logging with Sentry integration for monitoring
  4. Manual Intervention: Failed provisioning logged for manual resolution

Consequences

Positive

  • Immediate user access: Users can log in while provisioning happens in background
  • Regional compliance: Data stored in user's preferred region
  • Fault tolerance: Individual service failures don't block entire provisioning
  • Scalable: Serverless Edge Functions handle any load
  • Secure: Secrets properly managed, never exposed in code

Negative

  • Eventual consistency: Resources may not be immediately available
  • Complexity: Distributed system with multiple failure points
  • Cost: Running multiple services (Stripe, Neon) per tenant
  • Debugging: Async operations harder to troubleshoot

Trade-offs

  • Chose async provisioning over sync for better UX despite complexity
  • Parallel execution over sequential for speed despite harder error handling
  • Regional deployment over single region for compliance despite cost

Implementation Checklist

  • GraphQL signup mutation with regional preference
  • Async provisioning service
  • Stripe customer creation
  • Neon database provisioning with regions
  • Secret management in Supabase
  • Deployment workflow updates
  • Monitoring dashboard for provisioning status
  • Automated retry mechanism for failures
  • Admin tools for manual provisioning

References