Schema Overview

Supabase Database
β”œβ”€β”€ public (legacy customer-facing - being phased out)
β”‚   β”œβ”€β”€ customer_profile      # User accounts + subscription
β”‚   β”œβ”€β”€ items                 # Inventory management
β”‚   β”œβ”€β”€ actions               # Service requests (pickup/delivery)
β”‚   β”œβ”€β”€ inventory_events      # Item movement audit trail
β”‚   β”œβ”€β”€ booking_events        # Booking lifecycle audit trail
β”‚   β”œβ”€β”€ claims                # Insurance claims
β”‚   └── service_areas         # Serviceable ZIP codes
β”‚
β”œβ”€β”€ sv (canonical domain schema - primary)
β”‚   β”œβ”€β”€ customer_profile      # Canonical customer data
β”‚   β”œβ”€β”€ items                 # Canonical inventory
β”‚   β”œβ”€β”€ actions               # Service requests
β”‚   β”œβ”€β”€ inventory_events      # Item movement audit
β”‚   β”œβ”€β”€ ops_tasks             # Operations tasks
β”‚   β”œβ”€β”€ item_embeddings       # Vector embeddings for search
β”‚   β”œβ”€β”€ staff                 # Staff access control
β”‚   β”œβ”€β”€ container_catalog     # Container types
β”‚   β”œβ”€β”€ referrals             # Referral program
β”‚   β”œβ”€β”€ account_ledger        # Financial transactions
β”‚   β”œβ”€β”€ pre_customers         # Pre-signup leads
β”‚   β”œβ”€β”€ signup_anomalies      # Error tracking
β”‚   └── webhook_events        # Webhook idempotency
β”‚
β”œβ”€β”€ billing (Stripe integration)
β”‚   └── webhook_events        # Webhook idempotency log
β”‚
β”œβ”€β”€ auth (Supabase-managed)
β”‚   └── users                 # Authentication (magic links)
β”‚
└── storage (Supabase-managed)
    └── buckets               # File storage (item-photos)

Current Status (Feb 2026)

SchemaStatusNotes
publicACTIVE PRODUCTION1 customer profile (total, as of Feb 19, 2026)
svPLANNED CANONICAL0 records - not yet migrated to
billingACTIVEStripe webhook idempotency log (webhook_events)

The sv schema represents the target architecture. Migration from public to sv is planned post-launch.

Entity Relationship Diagram

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚     auth.users       β”‚ (Supabase-managed)
β”‚  ─────────────────   β”‚
β”‚  id (PK)             β”‚
β”‚  email               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚ 1:1
           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         public.customer_profile                  β”‚
β”‚  ─────────────────────────────────────────────  β”‚
β”‚  user_id (PK, FK β†’ auth.users)                  β”‚
β”‚  email, stripe_customer_id, subscription_id     β”‚
β”‚  subscription_status, last_payment_at           β”‚
β”‚  full_name, phone, delivery_address (jsonb)     β”‚
β”‚  out_of_service_area, needs_manual_refund       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
            β”‚ 1:N
            β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚          public.items                      β”‚
β”‚  ───────────────────────────────────────  β”‚
β”‚  id (PK), user_id (FK)                     β”‚
β”‚  label, description, category              β”‚
β”‚  status (home|in_transit|stored|scheduled) β”‚
β”‚  photo_paths[], qr_code (UNIQUE)           β”‚
β”‚  weight_lbs, dimensions, cubic_feet        β”‚
β”‚  physical_locked_at, tags[]                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚ 1:N              β”‚ M:N (via arrays)
         β–Ό                  β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ inventory_events   β”‚   β”‚    public.actions        β”‚
β”‚  ───────────────   β”‚   β”‚  ─────────────────────  β”‚
β”‚  id, item_id (FK)  β”‚   β”‚  id, user_id (FK)        β”‚
β”‚  event_type        β”‚   β”‚  service_type, status    β”‚
β”‚  event_data        β”‚   β”‚  pickup_item_ids[]       β”‚
β”‚  created_at        β”‚   β”‚  delivery_item_ids[]     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚  calendly_event_uri      β”‚
                         β”‚  scheduled_start/end     β”‚
                         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                   β”‚ 1:N
                                   β–Ό
                         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                         β”‚   booking_events     β”‚
                         β”‚  ─────────────────── β”‚
                         β”‚  id, action_id (FK)  β”‚
                         β”‚  event_type          β”‚
                         β”‚  metadata, created_atβ”‚
                         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Core Tables

customer_profile

Purpose: User account and subscription data (1:1 with auth.users)

ColumnTypeDescription
user_idUUID (PK, FK)Links to auth.users
emailTEXT (UNIQUE)Customer email
stripe_customer_idTEXT (UNIQUE)Stripe customer ID
subscription_idTEXTActive subscription ID
subscription_statusENUMinactive, active, past_due, canceled, trialing, etc.
last_payment_atTIMESTAMPTZLatest successful payment
full_nameTEXTCustomer name
phoneTEXTContact phone
delivery_addressJSONB{street, unit, city, state, zip}
out_of_service_areaBOOLEANSoft gate for out-of-area

Protected Columns

Users CANNOT update subscription_status, stripe_customer_id, subscription_id directly.

items

Purpose: Customer inventory with multi-photo support

ColumnTypeDescription
idUUID (PK)Item identifier
user_idUUID (FK)Owner
labelTEXT (NOT NULL)Item name
descriptionTEXT (NOT NULL)Item description
categoryTEXTOptional category
statusENUMhome, in_transit, stored, scheduled
photo_pathsTEXT[]Multi-photo array (1-5)
qr_codeTEXT (UNIQUE)Format: SV-YYYY-NNNNNN
cubic_feetNUMERIC (GENERATED)Auto-calculated
tagsTEXT[]Searchable keywords

actions

Purpose: Service requests (pickup, redelivery, container delivery)

ColumnTypeDescription
idUUID (PK)Action identifier
user_idUUID (FK)Customer
service_typeENUMpickup, redelivery, container_delivery
statusENUMpending_items, pending_confirmation, confirmed, in_progress, completed, canceled
pickup_item_idsUUID[]Items to pick up
delivery_item_idsUUID[]Items to deliver
calendly_event_uriTEXT (UNIQUE)Calendly event identifier
scheduled_startTIMESTAMPTZCalendly booking start

service_areas

Purpose: Serviceable ZIP codes

ColumnTypeDescription
zipTEXT (PK)ZIP code
cityTEXTCity name
stateTEXTState code

Billing Tables

billing.webhook_events

Purpose: Stripe webhook idempotency log

ColumnTypeDescription
idBIGSERIAL (PK)Auto-increment ID
event_idTEXT (UNIQUE)Stripe event.id (idempotency key)
event_typeTEXTe.g., checkout.session.completed
payloadJSONBFull Stripe event object
processed_atTIMESTAMPTZProcessing completion

Database Functions

update_subscription_status()

Purpose: Update billing-protected columns from webhooks
Type: SECURITY DEFINER (bypasses RLS)

SELECT update_subscription_status(
  p_user_id := 'user-uuid',
  p_status := 'active',
  p_subscription_id := 'sub_123',
  p_last_payment_at := now()
);

log_booking_event()

Purpose: Insert booking events from edge functions
Type: SECURITY DEFINER

SELECT log_booking_event(
  p_action_id := 'action-uuid',
  p_event_type := 'calendly_webhook',
  p_metadata := '{"event_uri": "..."}'::jsonb
);

fn_revert_orphaned_scheduled()

Purpose: Detect and revert items stuck in scheduled state with no active booking. Called on dashboard load as a safety guardrail.
Type: SECURITY DEFINER

  • Finds items where status='scheduled' with no active action referencing them
  • Reverts pickup orphans to home, delivery orphans to stored
  • Logs each reversion to inventory_events with type orphaned_scheduled_reverted
  • Returns JSON summary for frontend toast notification
  • Items with no action history are flagged as unknown (not mutated)

get_ops_actions()

Purpose: Staff-only RPC for the Ops Dashboard. Returns actions joined with customer profile data.
Type: SECURITY DEFINER, gated by sv.is_staff()

  • Wraps sv.v_ops_actions view (cross-schema join of public.actions + sv.customer_profile)
  • Only accessible to authenticated staff members

RLS Policies

customer_profile

  • βœ… Owner can SELECT own profile
  • βœ… Owner can UPDATE editable fields (name, phone, address)
  • ❌ Owner CANNOT update billing fields

items

  • βœ… Owner can SELECT/INSERT/UPDATE/DELETE own items

actions

  • βœ… Owner can SELECT own actions (or staff can view all)
  • βœ… Owner can INSERT actions
  • βœ… Owner can UPDATE/DELETE own actions in pending, pending_items, or pending_confirmation status only
  • βœ… Staff can UPDATE/DELETE any action regardless of status
  • ❌ Owner CANNOT update to confirmed/completed (ops only)

service_areas

  • βœ… RLS enabled β€” authenticated users can SELECT (lookup table)
  • ❌ No INSERT/UPDATE/DELETE for users

sv.staff

  • βœ… RLS enabled β€” service_role only (no direct user access)

inventory_events

  • βœ… Owner can SELECT events for their items
  • ❌ Owner CANNOT insert (system-only via service_role)

booking_events

  • βœ… Owner can SELECT events for their own bookings (via action ownership JOIN)
  • βœ… Staff can SELECT all booking events
  • ❌ Orphan events (action_id IS NULL) are only visible to staff
  • ❌ Owner CANNOT insert (system-only via service_role)

Common Queries

Get user items for dashboard

SELECT * FROM items
WHERE user_id = auth.uid()
ORDER BY created_at DESC;

Search items

SELECT * FROM items
WHERE user_id = auth.uid()
  AND status = 'stored'
  AND (
    label ILIKE '%keyword%'
    OR description ILIKE '%keyword%'
    OR 'keyword' = ANY(tags)
  );

Get pending bookings

SELECT * FROM actions
WHERE user_id = auth.uid()
  AND status IN ('pending_items', 'pending_confirmation')
ORDER BY scheduled_start ASC;

Migration History

Total: 44 migrations applied (verified Mar 13, 2026)

Key migrations include:

  • 0001_init β€” Core schema
  • 0004_phase1_inventory_enhancements β€” Multi-photo, status, batch ops
  • 0005_create_sv_schema β€” Create sv domain schema
  • 20251112000005_billing_status_tracking β€” Payment timestamps
  • 20251201062222_create_service_areas_table β€” Service areas
  • 20260107000001 β€” Billing v2 trial columns
  • 20260129000001 β€” Enable RLS on service_areas
  • 20260202000001 β€” fn_revert_orphaned_scheduled guardrail
  • 20260202000002 β€” Deprecate billing.customers
  • 20260204000001–05 β€” Ops dashboard: sv.v_ops_actions view, get_ops_actions RPC, staff RLS hardening
  • 20260211000001 β€” Track A: Harden actions UPDATE/DELETE RLS (restore status constraint), fix booking_events cross-tenant leak
  • 20260211000002 β€” Track A: Capture 3 missing RPCs in migration history (check_stripe_webhook_event, insert_stripe_webhook_event, get_user_id_by_email)
  • 20260211000003 β€” Track A: is_valid_zip_code() STABLE table-backed function
  • 20260211000004 β€” Track A: Drop stale booking_events_read_policy (P0 leak closure) and duplicate p_actions_owner_insert
  • 20260215000001 β€” Drop duplicate update_subscription_status(subscription_status_enum) overload (P0 stripe-webhook fix)
  • 20260227000001 β€” Drop deprecated upsert_billing_customer function (legacy helper for billing.customers, no longer referenced)
  • 20260304000001 β€” Drop deprecated billing.customers table (redundant with customer_profile.stripe_customer_id)

Pending Migrations

No pending migrations.

Full migration list: ~/code/sv-db/supabase/migrations/