Skip to main content

Event System

Cabina’s event system enables zero-friction AI photo booth experiences at weddings, quinceañeras, corporate events, and parties. Guests scan a QR code and generate photos instantly without registration or payment.

What is an Event?

An event is a time-bound, branded photo booth experience created by a Partner for their client.
interface Event {
  id: string;
  partner_id: string;            // Who created it
  event_name: string;            // "María's Quinceañera"
  event_slug: string;            // URL: ?event=maria-quince
  
  // Credits
  credits_allocated: number;     // Total budget (e.g., 5000 = 50 photos)
  credits_used: number;          // Consumed so far
  
  // Dates
  start_date: string;            // Event begins
  end_date: string;              // Event ends
  
  // Branding
  config: {
    logo_url?: string;           // Client logo
    primary_color: string;       // Hex color (#ff69b4)
    welcome_text: string;        // Greeting message
    show_welcome_screen: boolean;// Pre-event landing page
  };
  
  // AI Styles
  selected_styles: string[];     // ['pixar_a', 'disney_a', 'barbie_a']
  
  is_active: boolean;            // Can be disabled by partner
}

Event Lifecycle

Phase 1: Creation

1

Partner Initiates

Partner opens the “Crear Evento” modal:
// src/components/dashboards/partner/modals/CreateEventModal.tsx
const [formData, setFormData] = useState({
  event_name: '',
  event_slug: '',
  credits_allocated: 5000,
  start_date: '',
  end_date: '',
  selected_styles: [],
  config: {
    logo_url: partner.config?.logo_url || '',
    primary_color: partner.config?.primary_color || '#7f13ec',
    welcome_text: 'Bienvenidos a nuestro evento'
  }
});
2

Fill Event Details

Required fields:
  • Event Name: Display name (e.g., “Boda de Juan & María”)
  • Event Slug: URL identifier (e.g., boda-juan-maria)
    • Auto-generated from name
    • Must be unique globally
    • Lowercase, alphanumeric + hyphens only
// Auto-generate slug from name
const generateSlug = (name: string) => {
  return name
    .toLowerCase()
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '') // Remove accents
    .replace(/[^a-z0-9]+/g, '-')     // Replace spaces with hyphens
    .replace(/^-+|-+$/g, '');        // Trim hyphens
};

// "María's Quinceañera" → "marias-quinceanera"
3

Set Date Range

start_date: '2026-03-15T18:00:00Z',  // 6 PM local time
end_date: '2026-03-16T03:00:00Z'     // 3 AM next day
If no dates are set, event is accessible immediately and indefinitely (not recommended).
4

Allocate Credits

Partner assigns credits from their wallet:
credits_allocated: 10_000  // 100 photos max
Credits are deducted from partner’s available balance immediately. They CANNOT be returned to the wallet once allocated.
5

Select AI Styles

Choose which styles guests can use:
selected_styles: [
  'pixar_a',      // Pixar animation
  'disney_a',     // Disney style
  'barbie_a',     // Barbie aesthetic
  'magazine_a'    // Magazine cover
]
Limit to 4-8 styles for focused events. Too many choices overwhelm guests.
6

Customize Branding

Upload logo and choose colors:
// src/hooks/useBranding.ts:85
const handleLogoUpload = async (file: File) => {
  // Upload to Supabase Storage
  const { data, error } = await supabase.storage
    .from('event-logos')
    .upload(`${partner.id}/${Date.now()}-${file.name}`, file);
  
  // Get public URL
  const { data: publicUrl } = supabase.storage
    .from('event-logos')
    .getPublicUrl(data.path);
  
  setBrandingConfig(prev => ({
    ...prev,
    logo_url: publicUrl.publicUrl
  }));
};
7

Create Event

// src/hooks/usePartnerDashboard.ts:67
const { data: event, error } = await supabase
  .from('events')
  .insert({
    partner_id: partner.id,
    event_name: formData.event_name,
    event_slug: formData.event_slug,
    credits_allocated: formData.credits_allocated,
    credits_used: 0,
    start_date: formData.start_date,
    end_date: formData.end_date,
    selected_styles: formData.selected_styles,
    config: formData.config,
    is_active: true
  })
  .select()
  .single();

Phase 2: Pre-Event (Optional)

If config.show_welcome_screen = true and start_date is in the future:
// src/App.tsx:1096
if (isPreEvent) {
  return (
    <div className="pre-event-screen">
      {eventConfig.config?.logo_url && (
        <img src={eventConfig.config.logo_url} alt="Logo" />
      )}
      <h1>{eventConfig.event_name}</h1>
      <p>{eventConfig.config?.welcome_text}</p>
      <p>Comienza el {new Date(eventConfig.start_date).toLocaleDateString()}</p>
    </div>
  );
}
Guests who scan the QR early see a countdown screen instead of the full experience.

Phase 3: Live Event

1

Guest Scans QR

https://app.metalabia.com?event=maria-quince
2

Platform Validates Event

// src/App.tsx:283
const fetchEvent = async (eventSlug: string) => {
  const { data: event, error } = await supabase
    .from('events')
    .select('*')
    .eq('event_slug', eventSlug)
    .maybeSingle();
  
  if (!event) {
    setEventError('❌ Evento no encontrado');
    return;
  }
  
  // Validate dates
  const now = new Date();
  if (event.start_date && new Date(event.start_date) > now) {
    setEventError(`📅 Evento aún no comenzó`);
    return;
  }
  if (event.end_date && new Date(event.end_date) < now) {
    setEventError(`🎬 Evento finalizó`);
    return;
  }
  
  // Validate credits
  const remaining = event.credits_allocated - event.credits_used;
  if (remaining <= 0) {
    setEventError(`🎫 Créditos agotados`);
    return;
  }
  
  setEventConfig(event);
};
3

Apply Branding

Event’s custom branding is applied via CSS variables:
// src/App.tsx:346
const primary = eventConfig.config.primary_color;
const glow = hexToRgba(primary, 0.4);

document.documentElement.style.setProperty('--accent-color', primary);
document.documentElement.style.setProperty('--accent-glow', glow);
All buttons, highlights, and accents now use the event’s color.
4

Load Guest Experience

// src/App.tsx:1137
if (eventConfig && !isStaff) {
  return <GuestExperience eventConfig={eventConfig} supabase={supabase} />;
}
5

Guest Generates Photos

Phase 4: Post-Event

After end_date or when credits run out:
// Event becomes read-only
// Guests see: "Este evento ya finalizó. ¡Gracias por participar!"
// Partner can still:
// - View analytics
// - Download all photos
// - Export generation report

Zero-Friction Flow

The magic of Cabina’s event system is no friction for guests.

No Registration

Guests never create accounts or enter personal info

No Login

No username, password, or OAuth flow

No Payment

Credits come from event pool, not guest wallets

3-Step Flow

Select style → Take photo → Download. That’s it.

Guest Journey

// src/components/kiosk/GuestExperience.tsx:23
export const GuestExperience: React.FC<GuestExperienceProps> = ({
  eventConfig, supabase
}) => {
  const [step, setStep] = useState<Step>('WELCOME');
  
  // Step 1: Welcome screen with event branding
  // Step 2: Select AI style (from event.selected_styles)
  // Step 3: Capture photo with camera
  // Step 4: Processing (10-15 seconds)
  // Step 5: Result with download/share options
};
1

Welcome Screen

<motion.div>
  {eventConfig.config?.logo_url && (
    <img src={eventConfig.config.logo_url} alt="Logo" />
  )}
  <h1>{eventConfig.event_name}</h1>
  <h2>{eventConfig.config?.welcome_text || 'Bienvenidos'}</h2>
  <button onClick={() => setStep('STYLE_SELECTION')}>
    Iniciar Experiencia
  </button>
</motion.div>
2

Style Selection

Only show styles selected by partner:
const availableStyles = IDENTITIES.filter(style => 
  eventConfig.selected_styles.includes(style.id)
);

// Display as grid of cards
availableStyles.map(style => (
  <UploadCard
    key={style.id}
    title={style.title}
    sampleImageUrl={style.url}
    onSelect={() => {
      setSelectedStyle(style);
      setStep('CAMERA');
    }}
  />
));
3

Camera Capture

Simplified camera with auto-mirror:
const takePhoto = () => {
  const canvas = canvasRef.current;
  const video = videoRef.current;
  const ctx = canvas.getContext('2d');
  
  // Mirror the image for selfie naturalness
  ctx.translate(canvas.width, 0);
  ctx.scale(-1, 1);
  ctx.drawImage(video, 0, 0);
  
  setCapturedImage(canvas.toDataURL('image/jpeg', 0.8));
};
4

AI Processing

Call Edge Function with event context:
const { data, error } = await supabase.functions.invoke('cabina-vision', {
  body: {
    user_photo: capturedImage,
    model_id: selectedStyle.id,
    aspect_ratio: '9:16',  // Fixed for mobile
    event_id: eventConfig.id,
    guest_id: `guest_${Date.now()}`
  }
});
No user_id is sent. Guests are anonymous. Tracking is via guest_id (timestamp-based).
5

Celebration & Download

if (data?.success) {
  setResultImage(data.image_url);
  setStep('RESULT');
  
  // Confetti with event colors
  confetti({
    particleCount: 150,
    colors: [eventConfig.config.primary_color, '#ffffff']
  });
}
Guest can:
  • Download image
  • Share via WhatsApp (mobile)
  • Generate QR code for later access
  • Start over (“Hacer otra foto”)

QR Code Generation

Every event gets a unique QR code:
// src/components/EventQRGenerator.tsx:14
const EventQRGenerator = ({ eventSlug }: { eventSlug: string }) => {
  const eventUrl = `https://app.metalabia.com?event=${eventSlug}`;
  
  return (
    <div>
      <QRCodeSVG
        value={eventUrl}
        size={512}
        level="H"          // High error correction (30%)
        includeMargin={true}
        imageSettings={{
          src: '/logo.png',
          height: 64,
          width: 64,
          excavate: true   // Remove background behind logo
        }}
      />
      
      <button onClick={downloadQR}>Descargar PNG</button>
      <button onClick={downloadSVG}>Descargar SVG</button>
    </div>
  );
};
Print Recommendations:
  • Table Tents: 4x6” cards with QR code + instructions
  • Posters: 11x17” at entrance with large QR
  • Screens: Display QR on loop at photo booth station

White-Label Branding

Events can be fully customized to match the client’s brand.

Logo Upload

// src/hooks/useBranding.ts:100
const handleLogoUpload = async (file: File) => {
  // Validate file
  if (file.size > 5 * 1024 * 1024) {
    throw new Error('Logo debe ser menor a 5MB');
  }
  
  if (!file.type.startsWith('image/')) {
    throw new Error('Solo se aceptan imágenes');
  }
  
  setIsUploading(true);
  
  // Upload to Supabase Storage
  const filePath = `${partner.id}/${Date.now()}-${file.name}`;
  const { data, error } = await supabase.storage
    .from('event-logos')
    .upload(filePath, file, {
      cacheControl: '3600',
      upsert: false
    });
  
  if (error) throw error;
  
  // Get public URL
  const { data: publicUrl } = supabase.storage
    .from('event-logos')
    .getPublicUrl(data.path);
  
  // Update config
  setBrandingConfig(prev => ({
    ...prev,
    logo_url: publicUrl.publicUrl
  }));
  
  setIsUploading(false);
};

Dynamic Color Application

// Applied at runtime when event loads
const applyBranding = (primaryColor: string) => {
  const root = document.documentElement;
  
  // Main accent color
  root.style.setProperty('--accent-color', primaryColor);
  
  // Glow/shadow variant (40% opacity)
  const rgba = hexToRgba(primaryColor, 0.4);
  root.style.setProperty('--accent-glow', rgba);
  
  // All CSS using these variables updates instantly:
  // - Buttons background
  // - Text highlights
  // - Loading spinners
  // - Confetti colors
};

// Example usage:
applyBranding('#ff69b4');  // Hot pink for a quinceañera
CSS Variables Used:
  • --accent-color: Primary brand color
  • --accent-glow: Semi-transparent glow effect

Welcome Message

Custom greeting text:
config: {
  welcome_text: '¡Bienvenidos a la boda de Juan & María! 💍'
}
Displayed prominently on the welcome screen.

Event Analytics

Partner View

// src/components/dashboards/partner/EventsSection.tsx:25
const eventStats = {
  total_photos: 87,
  credits_used: 8_700,
  credits_remaining: 1_300,
  avg_generation_time: '12s',
  peak_usage: '22:30 - 23:00',
  most_popular_style: 'Pixar'
};

Client View

// src/components/dashboards/ClientDashboard.tsx:200
const clientView = {
  photos_generated: 87,
  credits_remaining: 1_300,
  event_url: 'app.metalabia.com?event=maria-quince',
  qr_downloads: 3,
  gallery_link: '/gallery?event=maria-quince'
};
Real-time photo feed:
// src/components/EventGallery.tsx:14
const LiveGallery = ({ eventId }: { eventId: string }) => {
  const [photos, setPhotos] = useState<any[]>([]);
  
  useEffect(() => {
    // Subscribe to new generations
    const subscription = supabase
      .channel(`event_${eventId}`)
      .on(
        'postgres_changes',
        {
          event: 'INSERT',
          schema: 'public',
          table: 'generations',
          filter: `event_id=eq.${eventId}`
        },
        (payload) => {
          setPhotos(prev => [payload.new, ...prev]);
          playShutterSound();
        }
      )
      .subscribe();
    
    return () => subscription.unsubscribe();
  }, [eventId]);
  
  return (
    <div className="photo-grid">
      {photos.map(photo => (
        <img key={photo.id} src={photo.image_url} alt="Generation" />
      ))}
    </div>
  );
};
Display on Screen: Partners often connect a tablet/monitor to the live gallery URL and display it at the event for guests to see all photos in real-time.

Code References

FeatureFileLine
Event Creation Modalsrc/components/dashboards/partner/modals/CreateEventModal.tsx1
Event Validationsrc/App.tsx283
Guest Experiencesrc/components/kiosk/GuestExperience.tsx23
Branding Applicationsrc/App.tsx346
QR Generatorsrc/components/EventQRGenerator.tsx14
Live Gallerysrc/components/EventGallery.tsx14
Credit Deductionsupabase/functions/cabina-vision/index.ts~50

Best Practices

Generous Credit Allocation

Allocate 20% more than expected. Running out mid-event creates bad experience.

Test QR Before Printing

Scan the QR yourself to verify it works before printing 100 copies.

Limit Style Selection

4-8 styles max. Too many choices overwhelm guests and slow decision-making.

Monitor Live

Keep client dashboard open during event to catch issues early.

Next Steps

Credit System

Learn how atomic credits prevent race conditions

Multi-Tier System

Understand Partner → Client → Guest hierarchy

Business Models

See how events fit into the B2B2C model

Quickstart

Create your first event in 5 minutes

Build docs developers (and LLMs) love