Skip to main content

Schema Overview

Cabina uses PostgreSQL via Supabase with the following core tables:
  • profiles - User accounts and credits
  • partners - White-label resellers (B2B)
  • clients - Partner’s customers
  • events - Photo booth events
  • generations - AI-generated photos
  • api_key_pool - Load-balanced AI API keys
  • payment_notifications - Mercado Pago webhooks
  • styles_metadata - Available photo styles
  • identity_prompts - AI prompts for styles

Core Tables

profiles

User accounts for the B2C app and authenticated users.
CREATE TABLE profiles (
  id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
  email TEXT,
  full_name TEXT,
  credits INTEGER DEFAULT 0,
  total_generations INTEGER DEFAULT 0,
  is_master BOOLEAN DEFAULT FALSE,
  role TEXT DEFAULT 'user',
  referral_code TEXT UNIQUE,
  referred_by UUID REFERENCES profiles(id),
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);
id
uuid
required
Primary key, references auth.users(id)
email
text
User’s email address
credits
integer
default:"0"
Available credits for B2C users (Modelo A)
total_generations
integer
default:"0"
Total number of photos generated by this user
is_master
boolean
default:"false"
Admin flag (Leo = true)
role
text
default:"'user'"
User role: 'user', 'partner', 'client', or 'master'
referral_code
text
Unique referral code for this user
referred_by
uuid
References the user who referred this account

partners

White-label resellers who create events for their clients.
CREATE TABLE partners (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  business_name TEXT,
  contact_name TEXT,
  contact_email TEXT,
  user_id UUID REFERENCES profiles(id),
  credits_total INTEGER DEFAULT 0,
  credits_used INTEGER DEFAULT 0,
  is_active BOOLEAN DEFAULT TRUE,
  config JSONB DEFAULT '{}'::jsonb,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);
id
uuid
required
Primary key
name
text
required
Partner display name
user_id
uuid
Links partner to a login account in profiles
credits_total
integer
default:"0"
Total credits purchased from Master
credits_used
integer
default:"0"
Credits consumed across all partner events
config
jsonb
White-label configuration:
{
  "primary_color": "#FF6B35",
  "logo_url": "https://...",
  "radius": "0.5rem",
  "style_presets": ["pb_a", "suit_a"]
}

clients

Partner’s end customers who manage specific events.
CREATE TABLE clients (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  partner_id UUID NOT NULL REFERENCES partners(id) ON DELETE CASCADE,
  name TEXT NOT NULL,
  email TEXT,
  contact_person TEXT,
  phone TEXT,
  credits_total INTEGER DEFAULT 0,
  credits_used INTEGER DEFAULT 0,
  contracted_styles TEXT[] DEFAULT '{}',
  config JSONB DEFAULT '{}'::jsonb,
  created_at TIMESTAMPTZ DEFAULT NOW()
);
partner_id
uuid
required
Foreign key to partners(id)
contracted_styles
text[]
Array of style IDs this client can use

events

Photo booth events with allocated credits and configuration.
CREATE TABLE events (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  partner_id UUID REFERENCES partners(id) ON DELETE SET NULL,
  client_id UUID REFERENCES clients(id) ON DELETE SET NULL,
  event_name TEXT NOT NULL,
  event_slug TEXT UNIQUE NOT NULL,
  client_name TEXT,
  client_email TEXT,
  client_access_pin TEXT,
  credits_allocated INTEGER DEFAULT 0,
  credits_used INTEGER DEFAULT 0,
  selected_styles TEXT[] DEFAULT '{}',
  config JSONB DEFAULT '{}'::jsonb,
  start_date TIMESTAMPTZ,
  end_date TIMESTAMPTZ,
  is_active BOOLEAN DEFAULT TRUE,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);
event_slug
text
required
Unique URL slug for the event (e.g., quince-sofia-2024)Used in URLs: /?event=quince-sofia-2024
credits_allocated
integer
default:"0"
Total credits assigned to this event
credits_used
integer
default:"0"
Credits consumed by guest generationsUpdated atomically via increment_event_credit RPC
selected_styles
text[]
Array of style IDs enabled for this eventExample: ['pb_a', 'suit_b', 'jhonw_c']
config
jsonb
Event branding and settings:
{
  "logo_url": "https://...",
  "primary_color": "#FF6B35",
  "welcome_text": "Bienvenidos a la fiesta!"
}
client_access_pin
text
Simple PIN for client dashboard access

generations

AI-generated photos from both B2C users and event guests.
CREATE TABLE generations (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES profiles(id) ON DELETE SET NULL,
  event_id UUID REFERENCES events(id) ON DELETE SET NULL,
  model_id TEXT,
  style_id TEXT,
  image_url TEXT NOT NULL,
  aspect_ratio TEXT DEFAULT '9:16',
  created_at TIMESTAMPTZ DEFAULT NOW()
);
user_id
uuid
User who generated this (B2C model)Nullable - event guests don’t have accounts
event_id
uuid
Event this generation belongs to (B2B model)Nullable - B2C generations have no event
model_id
text
AI model/style identifier (e.g., pb_a, suit_b)
image_url
text
required
URL to the generated image in Supabase Storage or external CDN
Credit Model Logic:
  • If user_id is set: deduct from profiles.credits
  • If event_id is set: deduct from events.credits_allocated (atomic RPC)

api_key_pool

Load balancer for AI API keys (Kie.ai).
CREATE TABLE api_key_pool (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  api_key TEXT NOT NULL UNIQUE,
  account_name TEXT,
  is_active BOOLEAN DEFAULT TRUE,
  last_used_at TIMESTAMPTZ DEFAULT NOW(),
  usage_count INTEGER DEFAULT 0,
  created_at TIMESTAMPTZ DEFAULT NOW()
);
api_key
text
required
Kie.ai API key
is_active
boolean
default:"true"
Whether this key should be used in rotation
last_used_at
timestamptz
Timestamp of last usage (for least-recently-used selection)
usage_count
integer
default:"0"
Total number of times this key has been used
The cabina-vision edge function selects the least recently used active key for each generation request.

payment_notifications

Webhook events from Mercado Pago.
CREATE TABLE payment_notifications (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  mercadopago_id TEXT UNIQUE NOT NULL,
  user_id UUID REFERENCES profiles(id),
  status TEXT,
  credits_added INTEGER,
  amount NUMERIC,
  data JSONB,
  created_at TIMESTAMPTZ DEFAULT NOW()
);
mercadopago_id
text
required
Unique payment ID from Mercado Pago
user_id
uuid
User who made the purchase
credits_added
integer
Number of credits granted for this payment
data
jsonb
Full webhook payload from Mercado Pago

styles_metadata

Available photo styles and their metadata.
CREATE TABLE styles_metadata (
  id TEXT PRIMARY KEY,
  label TEXT,
  category TEXT,
  subcategory TEXT,
  image_url TEXT,
  tags TEXT[] DEFAULT '{}',
  is_premium BOOLEAN DEFAULT FALSE,
  usage_count INTEGER DEFAULT 0,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);
id
text
required
Style identifier (e.g., pb_a, suit_b)
subcategory
text
Style pack name (e.g., “Peaky Blinders”, “La Ley de los Audaces”)
tags
text[]
Searchable tags: ['gangster', 'vintage', 'cap', 'whiskey']
is_premium
boolean
default:"false"
Whether this style requires premium access

identity_prompts

AI prompts for each style.
CREATE TABLE identity_prompts (
  id TEXT PRIMARY KEY,
  master_prompt TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW()
);
id
text
required
Matches styles_metadata.id
master_prompt
text
Full AI prompt for image generationExample:
"Professional portrait in the style of Peaky Blinders, 
1920s Birmingham, wearing flat cap, vintage suit, 
cinematic lighting, moody atmosphere"

Database Functions

increment_event_credit

Atomically increments event credit usage.
CREATE OR REPLACE FUNCTION increment_event_credit(p_event_id UUID)
RETURNS BOOLEAN AS $$
DECLARE
  v_allocated INTEGER;
  v_used INTEGER;
BEGIN
  SELECT credits_allocated, credits_used
  INTO v_allocated, v_used
  FROM events
  WHERE id = p_event_id
  FOR UPDATE;

  IF v_used >= v_allocated THEN
    RETURN FALSE; -- No credits remaining
  END IF;

  UPDATE events
  SET credits_used = credits_used + 1
  WHERE id = p_event_id;

  RETURN TRUE;
END;
$$ LANGUAGE plpgsql;
Usage in Edge Function:
const { data: creditOk, error } = await supabase.rpc(
  'increment_event_credit',
  { p_event_id: event_id }
);

if (!creditOk) {
  throw new Error('Event credits exhausted');
}

Indexes

Key indexes for performance:
-- Generations lookup by user
CREATE INDEX idx_generations_user_id ON generations(user_id);

-- Generations lookup by event
CREATE INDEX idx_generations_event_id ON generations(event_id);

-- Event lookup by slug (for QR codes)
CREATE INDEX idx_events_slug ON events(event_slug);

-- Partner's events
CREATE INDEX idx_events_partner_id ON events(partner_id);

-- Client's events
CREATE INDEX idx_events_client_id ON events(client_id);

-- API key pool selection
CREATE INDEX idx_api_key_pool_active_lru 
  ON api_key_pool(is_active, last_used_at);

Relationships

Storage Buckets

user_photos

  • Purpose: Temporary storage for uploaded user photos
  • Public: No
  • RLS: Users can only access their own uploads

generations

  • Purpose: Final AI-generated images
  • Public: Yes
  • Path: results/{user_id}_{timestamp}.png

event_assets

  • Purpose: Event logos and branding
  • Public: Yes
  • Path: events/{event_id}/logo.png

Next Steps

Migrations

Learn how to manage database migrations

RLS Policies

Understand Row Level Security

Build docs developers (and LLMs) love