Database Schema
PostgreSQL 17.6.1 (Supabase-managed) β Tables, relationships, and migrations
Last updated: Mar 13, 2026
On This Page
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)
| Schema | Status | Notes |
|---|---|---|
public | ACTIVE PRODUCTION | 1 customer profile (total, as of Feb 19, 2026) |
sv | PLANNED CANONICAL | 0 records - not yet migrated to |
billing | ACTIVE | Stripe 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)
| Column | Type | Description |
|---|---|---|
user_id | UUID (PK, FK) | Links to auth.users |
email | TEXT (UNIQUE) | Customer email |
stripe_customer_id | TEXT (UNIQUE) | Stripe customer ID |
subscription_id | TEXT | Active subscription ID |
subscription_status | ENUM | inactive, active, past_due, canceled, trialing, etc. |
last_payment_at | TIMESTAMPTZ | Latest successful payment |
full_name | TEXT | Customer name |
phone | TEXT | Contact phone |
delivery_address | JSONB | {street, unit, city, state, zip} |
out_of_service_area | BOOLEAN | Soft 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
| Column | Type | Description |
|---|---|---|
id | UUID (PK) | Item identifier |
user_id | UUID (FK) | Owner |
label | TEXT (NOT NULL) | Item name |
description | TEXT (NOT NULL) | Item description |
category | TEXT | Optional category |
status | ENUM | home, in_transit, stored, scheduled |
photo_paths | TEXT[] | Multi-photo array (1-5) |
qr_code | TEXT (UNIQUE) | Format: SV-YYYY-NNNNNN |
cubic_feet | NUMERIC (GENERATED) | Auto-calculated |
tags | TEXT[] | Searchable keywords |
actions
Purpose: Service requests (pickup, redelivery, container delivery)
| Column | Type | Description |
|---|---|---|
id | UUID (PK) | Action identifier |
user_id | UUID (FK) | Customer |
service_type | ENUM | pickup, redelivery, container_delivery |
status | ENUM | pending_items, pending_confirmation, confirmed, in_progress, completed, canceled |
pickup_item_ids | UUID[] | Items to pick up |
delivery_item_ids | UUID[] | Items to deliver |
calendly_event_uri | TEXT (UNIQUE) | Calendly event identifier |
scheduled_start | TIMESTAMPTZ | Calendly booking start |
service_areas
Purpose: Serviceable ZIP codes
| Column | Type | Description |
|---|---|---|
zip | TEXT (PK) | ZIP code |
city | TEXT | City name |
state | TEXT | State code |
Billing Tables
billing.webhook_events
Purpose: Stripe webhook idempotency log
| Column | Type | Description |
|---|---|---|
id | BIGSERIAL (PK) | Auto-increment ID |
event_id | TEXT (UNIQUE) | Stripe event.id (idempotency key) |
event_type | TEXT | e.g., checkout.session.completed |
payload | JSONB | Full Stripe event object |
processed_at | TIMESTAMPTZ | Processing 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 tostored - Logs each reversion to
inventory_eventswith typeorphaned_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_actionsview (cross-schema join ofpublic.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, orpending_confirmationstatus 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_roleonly (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 schema0004_phase1_inventory_enhancementsβ Multi-photo, status, batch ops0005_create_sv_schemaβ Create sv domain schema20251112000005_billing_status_trackingβ Payment timestamps20251201062222_create_service_areas_tableβ Service areas20260107000001β Billing v2 trial columns20260129000001β Enable RLS on service_areas20260202000001βfn_revert_orphaned_scheduledguardrail20260202000002β Deprecatebilling.customers20260204000001β05β Ops dashboard:sv.v_ops_actionsview,get_ops_actionsRPC, staff RLS hardening20260211000001β Track A: Harden actions UPDATE/DELETE RLS (restore status constraint), fix booking_events cross-tenant leak20260211000002β 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 function20260211000004β Track A: Drop stalebooking_events_read_policy(P0 leak closure) and duplicatep_actions_owner_insert20260215000001β Drop duplicateupdate_subscription_status(subscription_status_enum)overload (P0 stripe-webhook fix)20260227000001β Drop deprecatedupsert_billing_customerfunction (legacy helper forbilling.customers, no longer referenced)20260304000001β Drop deprecatedbilling.customerstable (redundant withcustomer_profile.stripe_customer_id)
Pending Migrations
No pending migrations.
Full migration list: ~/code/sv-db/supabase/migrations/