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
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'
}
});
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"
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).
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.
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.
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
}));
};
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
Guest Scans QR
https://app.metalabia.com?event=maria-quince
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 );
};
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.
Load Guest Experience
// src/App.tsx:1137
if ( eventConfig && ! isStaff ) {
return < GuestExperience eventConfig ={ eventConfig } supabase ={ supabase } />;
}
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
};
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 >
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' );
}}
/>
));
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 ));
};
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).
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'
};
Live Gallery
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
Feature File Line Event Creation Modal src/components/dashboards/partner/modals/CreateEventModal.tsx1 Event Validation src/App.tsx283 Guest Experience src/components/kiosk/GuestExperience.tsx23 Branding Application src/App.tsx346 QR Generator src/components/EventQRGenerator.tsx14 Live Gallery src/components/EventGallery.tsx14 Credit Deduction supabase/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