Skip to main content

Platform Architecture

Cabina is built as a modern serverless SaaS platform using React, TypeScript, and Supabase. The architecture supports both B2C and B2B2C business models with a shared codebase.

High-Level Architecture

Tech Stack

Frontend

Core Framework

  • React 18 with TypeScript
  • Vite for build tooling
  • React Router for navigation (dashboard)

UI & Animations

  • TailwindCSS for styling
  • Framer Motion for animations
  • Lucide React for icons

State Management

  • React Hooks (useState, useEffect)
  • Custom hooks for business logic
  • Supabase Realtime for sync

Special Libraries

  • canvas-confetti for celebrations
  • qrcode.react for QR generation
  • jszip for bulk downloads

Backend (Supabase)

Database

PostgreSQL 15 with:
  • Row Level Security (RLS)
  • Atomic functions for credits
  • Realtime subscriptions

Edge Functions

Deno-based serverless functions:
  • cabina-vision - AI generation orchestration
  • mercadopago-payment - Payment processing

Authentication

Supabase Auth:
  • Email/Password
  • OAuth (Google)
  • Anonymous sessions for events

Storage

Supabase Storage:
  • Partner logos
  • Event branding assets
  • Generated images (optional)

External Integrations

  • Replicate API: AI image generation using Flux models
  • Mercado Pago: Payment processing for credit purchases
  • Cloudflare R2 (optional): CDN for generated images

Database Schema

Core Tables

-- User Profiles
CREATE TABLE profiles (
  id UUID PRIMARY KEY REFERENCES auth.users,
  email TEXT,
  credits INTEGER DEFAULT 500,
  total_generations INTEGER DEFAULT 0,
  is_master BOOLEAN DEFAULT false,
  role TEXT DEFAULT 'user', -- 'master', 'partner', 'client', 'user'
  unlocked_packs TEXT[],
  partner_id UUID REFERENCES partners(id)
);

-- Partners (Resellers)
CREATE TABLE partners (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  business_name TEXT NOT NULL,
  contact_email TEXT,
  contact_name TEXT,
  user_id UUID REFERENCES profiles(id),
  config JSONB, -- { logo_url, primary_color, enabled_styles }
  is_active BOOLEAN DEFAULT true,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Events
CREATE TABLE events (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  partner_id UUID REFERENCES partners(id),
  event_name TEXT NOT NULL,
  event_slug TEXT UNIQUE NOT NULL,
  config JSONB, -- { logo_url, primary_color, welcome_text, radius }
  selected_styles TEXT[], -- Array of style IDs
  credits_allocated INTEGER DEFAULT 0,
  credits_used INTEGER DEFAULT 0,
  start_date TIMESTAMPTZ,
  end_date TIMESTAMPTZ,
  is_active BOOLEAN DEFAULT true,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Generations (Photo Records)
CREATE TABLE generations (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  user_id UUID REFERENCES profiles(id), -- NULL for guest
  event_id UUID REFERENCES events(id), -- NULL for B2C
  style_id TEXT NOT NULL,
  image_url TEXT NOT NULL,
  aspect_ratio TEXT DEFAULT '9:16',
  created_at TIMESTAMPTZ DEFAULT NOW()
);
See the full schema in ARQUITECTURA-PLATAFORMA.md:92 in the source repository.

Atomic Credit Deduction

To prevent race conditions during high-traffic events:
CREATE OR REPLACE FUNCTION deduct_event_credit(event_uuid UUID)
RETURNS BOOLEAN AS $$
DECLARE
  remaining INTEGER;
BEGIN
  -- Atomic check and deduct
  UPDATE events
  SET credits_used = credits_used + 1
  WHERE id = event_uuid
    AND (credits_allocated - credits_used) > 0
  RETURNING (credits_allocated - credits_used - 1) INTO remaining;
  
  RETURN remaining IS NOT NULL;
END;
$$ LANGUAGE plpgsql;
This function is called from the Edge Function before starting AI generation, ensuring credits are never over-deducted even with 100+ concurrent guests.

Application Flow

B2C Generation Flow

// src/App.tsx:703
const handleSubmit = async (e: React.FormEvent) => {
  e.preventDefault();
  
  // 1. Verify credits
  if (profile.credits < 100) {
    setErrorMessage("Saldo insuficiente.");
    return;
  }
  
  // 2. Deduct credits (optimistic)
  await supabase
    .from('profiles')
    .update({ credits: profile.credits - 100 })
    .eq('id', session.user.id);
  
  // 3. Call Edge Function
  const { data, error } = await supabase.functions.invoke('cabina-vision', {
    body: {
      user_photo: capturedImage,
      model_id: selectedStyle.id,
      aspect_ratio: formData.aspectRatio,
      user_id: session.user.id
    }
  });
  
  // 4. Handle result
  if (data?.success) {
    setResultImage(data.image_url);
  } else {
    // Refund on error
    await supabase
      .from('profiles')
      .update({ credits: profile.credits })
      .eq('id', session.user.id);
  }
};

B2B2C Event Flow

// src/components/kiosk/GuestExperience.tsx:116
const handleGenerate = async () => {
  // No auth check - guests don't need accounts
  
  const { data, error } = await supabase.functions.invoke('cabina-vision', {
    body: {
      user_photo: capturedImage,
      model_id: selectedStyle.id,
      aspect_ratio: '9:16',
      event_id: eventConfig.id,
      guest_id: `guest_${Date.now()}`
    }
  });
  
  // Credits deducted atomically in Edge Function
  if (data?.success) {
    setResultImage(data.image_url);
    // Confetti celebration
    confetti({ 
      particleCount: 150,
      colors: [eventConfig.config.primary_color, '#ffffff']
    });
  }
};

Edge Function: cabina-vision

The core AI generation orchestrator:
// supabase/functions/cabina-vision/index.ts
Deno.serve(async (req) => {
  const { user_photo, model_id, event_id, user_id } = await req.json();
  
  // 1. Deduct credits atomically (for events)
  if (event_id) {
    const { data: canProceed } = await supabaseAdmin.rpc(
      'deduct_event_credit',
      { event_uuid: event_id }
    );
    
    if (!canProceed) {
      return new Response(
        JSON.stringify({ success: false, error: 'No credits' }),
        { status: 402 }
      );
    }
  }
  
  // 2. Start Replicate prediction
  const prediction = await replicate.predictions.create({
    model: FLUX_MODEL,
    input: {
      prompt: getPromptForStyle(model_id),
      image: user_photo,
      strength: 0.85
    }
  });
  
  // 3. Poll for completion (max 60s)
  let result = prediction;
  for (let i = 0; i < 30; i++) {
    await sleep(2000);
    result = await replicate.predictions.get(prediction.id);
    if (result.status === 'succeeded') break;
  }
  
  // 4. Save to database
  await supabaseAdmin.from('generations').insert({
    user_id,
    event_id,
    style_id: model_id,
    image_url: result.output[0]
  });
  
  return new Response(
    JSON.stringify({ success: true, image_url: result.output[0] }),
    { headers: { 'Content-Type': 'application/json' } }
  );
});
Timeout Handling: If the Edge Function times out (>60s), the frontend enables “background processing mode” and allows users to continue browsing.

Multi-Entry Points

Cabina uses route-based separation for different business models:
// src/index.tsx:8
const subdomain = window.location.hostname.split('.')[0];

if (subdomain === 'kiosk' || window.location.pathname.includes('dashboard')) {
  // B2B Admin Interface
  ReactDOM.createRoot(document.getElementById('root')!).render(
    <DashboardApp />
  );
} else {
  // B2C Public App
  ReactDOM.createRoot(document.getElementById('root')!).render(
    <App />
  );
}

URL Structure

  • https://app.metalabia.com → B2C Public App
  • https://app.metalabia.com?event=maria-quince → Guest Experience
  • https://kiosk.metalabia.com/dashboard.html → Partner Dashboard
  • https://app.metalabia.com/dashboard.html → Master Admin

Row Level Security (RLS)

Supabase RLS ensures data isolation:
-- Events are publicly readable (for guests)
CREATE POLICY "public_read_events" ON events
  FOR SELECT USING (true);

-- Partners can only see their own events
CREATE POLICY "partners_own_events" ON events
  FOR SELECT USING (
    partner_id IN (
      SELECT id FROM partners WHERE user_id = auth.uid()
    )
  );

-- Masters see everything
CREATE POLICY "master_all_access" ON events
  FOR ALL USING (
    EXISTS (
      SELECT 1 FROM profiles 
      WHERE id = auth.uid() AND is_master = true
    )
  );

Scalability Considerations

Database

  • Indexed queries on event_slug, user_id
  • Partitioning for generations table (future)
  • Connection pooling via Supabase

Frontend

  • Code splitting with React.lazy
  • Image lazy loading
  • Background processing for long tasks

Edge Functions

  • Stateless design for horizontal scaling
  • Built-in retry logic
  • Timeout handling with polling fallback

AI Processing

  • Replicate auto-scales based on demand
  • Queuing built into Replicate API
  • Fallback to alternative models on failure

Deployment Architecture

CI/CD: Every push to main triggers automatic deployment. Database migrations are run manually via Supabase CLI.

Next Steps

Business Models

Understand the dual B2C and B2B2C models

Multi-Tier System

Explore the Master → Partner → Client → Guest hierarchy

Credit System

Learn how atomic credits prevent double-billing

Event System

Deep dive into zero-friction events

Build docs developers (and LLMs) love