Overview
PassTru’s template system allows complete customization of all attendee-facing pages: the Attendee Portal, Check-In Page, and Post-Check-In screen. Control branding, content, and layout without writing code.
Template Types
PassTru provides three distinct template types:
Attendee Portal Personal portal page each attendee sees after logging in
Check-In Page Public self-service check-in interface
Post Check-In Success message shown after check-in completion
Accessing Branding Settings
Navigate to Event Portal
From your event list, click on an event to open its portal.
Open Branding Tab
Click Branding in the event portal sidebar.
Select Template
Choose which template to edit from the tabs: Portal, Check-In, Post Check-In, or Email.
Customize Content
Edit fields, upload images, and configure settings.
Save and Publish
Click Save Draft to save changes, or Publish to make them live.
Draft & Publish Workflow
PassTru uses a draft/publish system to safely preview changes:
src/pages/event/EventBranding.tsx
const handleSaveDraft = async () => {
const { error } = await supabase . from ( "events" ). update ({
draft_branding_portal: portalData ,
draft_branding_checkin: checkinData ,
draft_branding_postcheckin: postCheckinData ,
}). eq ( "id" , eventId );
};
const handlePublish = async () => {
const { error } = await supabase . from ( "events" ). update ({
// Save drafts
draft_branding_portal: portalData ,
draft_branding_checkin: checkinData ,
draft_branding_postcheckin: postCheckinData ,
// Copy to live
branding_portal: portalData ,
branding_checkin: checkinData ,
branding_postcheckin: postCheckinData ,
}). eq ( "id" , eventId );
};
Draft Mode : Changes saved as drafts are visible in the preview but not on public pages until published.
Variable System
Templates support dynamic variables that are replaced with actual data:
Event Variables
Available in all templates:
{{ event_name }} // "Annual Gala 2026"
{{ event_date }} // "2026-06-15"
{{ event_venue }} // "Grand Ballroom, KL Convention Centre"
{{ org_name }} // "Acme Events"
Attendee Variables
Available in Portal and Post Check-In templates:
{{ attendee_name }} // "John Doe"
{{ attendee_email }} // "[email protected] "
{{ attendee_unique_id }} // "AB12CD34"
{{ attendee_status }} // "Checked In" or "Pending"
Custom Field Variables
Any custom attendee field becomes a variable:
{{ Department }} // If Department field is configured
{{ Table }} // If Table field is configured
{{ Seat }} // If Seat field is configured
{{ Dietary }} // If Dietary field is configured
Variable Replacement
Variables are replaced using a regex-based system:
src/components/branding/types.ts
export function replaceVars ( text : string , vars : BrandingVariables ) : string {
return text . replace ( / \{\{ ( \w + ) \}\} / g , ( match , key ) => vars [ key ] ?? match );
}
Attendee Portal Template
The most comprehensive template with multiple sections:
Available Fields
Portal Template Type
src/components/branding/types.ts
export interface PortalBranding {
logo_url : string ;
headline : string ;
subtitle : string ;
banner_url : string ;
qr_section_label : string ;
section1_title : string ;
section1_body : string ;
pdf1_url : string ;
pdf1_label : string ;
pdf2_url : string ;
pdf2_label : string ;
section2_title : string ;
section2_body : string ;
button1_label : string ;
button1_url : string ;
button2_label : string ;
button2_url : string ;
footer : string ;
}
Check-In Page Template
Simpler template for the public check-in interface:
Available Fields
Event Logo : PNG, max 1MB (100px h × 300px w)
Headline : Welcome message
Subtitle : Instructions or tagline
Event Banner : PNG, max 1MB (400px h × 800px w)
Body Text : Additional information or agenda
Event Poster : PNG, max 1MB (800px h × 400px w)
Footer : Footer text
The QR scanner and manual entry components are system-controlled and cannot be customized via templates.
Check-In Template Type
src/components/branding/types.ts
export interface CheckinBranding {
logo_url : string ;
headline : string ;
subtitle : string ;
banner_url : string ;
body : string ;
poster_url : string ;
footer : string ;
}
Post Check-In Template
Minimal template for the success message:
Available Fields
Headline : Success message
Body Text : Additional information
Visible Fields : Checkboxes to select which attendee fields to display
Field Visibility
Control which attendee information is shown after check-in:
src/components/branding/BrandingForm.tsx
const allFields = [
"Name" ,
"Email" ,
"Unique ID" ,
... attendeeFields . filter ( f => ! [ "Name" , "Email" ]. includes ( f ))
];
const toggleField = ( field : string ) => {
const next = visibleFields . includes ( field )
? visibleFields . filter (( f ) => f !== field )
: [ ... visibleFields , field ];
update ( "visible_fields" , next );
};
Post Check-In Template Type
src/components/branding/types.ts
export interface PostCheckinBranding {
headline : string ;
body : string ;
visible_fields : string [];
}
Image Upload System
All images are uploaded to Supabase Storage:
Upload Process
src/components/branding/BrandingForm.tsx
const handleFileUpload = async ( field : string , file : File ) => {
// Validate PNG format and size
const err = await validatePng ( file , 1 ); // 1MB limit
if ( err ) {
toast . error ( err );
return ;
}
// Upload to storage
const path = ` ${ eventId } / ${ Date . now () } - ${ file . name } ` ;
const { error } = await supabase . storage
. from ( "branding-assets" )
. upload ( path , file );
// Get public URL
const { data : urlData } = supabase . storage
. from ( "branding-assets" )
. getPublicUrl ( path );
update ( field , urlData . publicUrl );
};
Image Validation
Images are validated using magic byte detection:
async function validatePng ( file : File , maxSizeMb : number ) : Promise < string | null > {
if ( file . type !== "image/png" ) return "Only PNG files are allowed" ;
if ( file . size > maxSizeMb * 1024 * 1024 ) return `File must be under ${ maxSizeMb } MB` ;
// PNG magic bytes: 137 80 78 71 13 10 26 10
const header = await readFileHeader ( file , 8 );
const pngMagic = [ 137 , 80 , 78 , 71 , 13 , 10 , 26 , 10 ];
if ( pngMagic . some (( b , i ) => header [ i ] !== b )) {
return "File is not a valid PNG image" ;
}
return null ;
}
Format Restrictions :
Images: PNG only, max 1MB
PDFs: PDF only, max 5MB
Files validated by magic bytes, not just extension
PDF Upload System
PDF downloads for attendee portals:
Upload & Configuration
< PdfUploadField
label = "PDF 1"
urlValue = { data . pdf1_url }
labelValue = { data . pdf1_label }
onUpload = { ( file ) => handleFileUpload ( "pdf1_url" , file , "pdf" ) }
onClearUrl = { () => update ( "pdf1_url" , "" ) }
onLabelChange = { ( v ) => update ( "pdf1_label" , v ) }
/>
PDF Validation
async function validatePdf ( file : File ) : Promise < string | null > {
if ( file . type !== "application/pdf" ) return "Only PDF files are allowed" ;
if ( file . size > 5 * 1024 * 1024 ) return "File must be under 5MB" ;
// PDF magic bytes: %PDF (0x25 0x50 0x44 0x46)
const header = await readFileHeader ( file , 4 );
const pdfMagic = [ 0x25 , 0x50 , 0x44 , 0x46 ];
if ( pdfMagic . some (( b , i ) => header [ i ] !== b )) {
return "File is not a valid PDF document" ;
}
return null ;
}
Storage Location
All branding assets are stored in the branding-assets bucket:
branding-assets/
├── {event-id}/
│ ├── {timestamp}-logo.png
│ ├── {timestamp}-banner.png
│ ├── {timestamp}-poster.png
│ └── {timestamp}-document.pdf
└── general/
└── {timestamp}-image.png
Template Rendering
Templates are rendered by specialized components:
src/components/branding/TemplateRenderer.tsx
< TemplateRenderer
pageType = "portal"
branding = { event . branding_portal }
variables = { {
attendee_name: attendee . name ,
attendee_email: attendee . email ,
attendee_unique_id: attendee . unique_id ,
event_name: event . name ,
event_date: event . date ,
event_venue: event . venue ,
} }
/>
Live Preview
The branding editor includes a live preview panel:
src/pages/event/EventBranding.tsx
< div className = "grid gap-6 lg:grid-cols-2" >
< Card >
< CardHeader >
< CardTitle > Attendee Portal Content </ CardTitle >
</ CardHeader >
< CardContent >
< BrandingForm
pageType = "portal"
data = { portalData }
onChange = { setPortalData }
/>
</ CardContent >
</ Card >
< BrandingPreview
pageType = "portal"
branding = { portalData }
orgSlug = { orgSlug }
eventSlug = { eventSlug }
/>
</ div >
Default Templates
New events start with sensible defaults:
src/components/branding/types.ts
export const DEFAULT_PORTAL_BRANDING : PortalBranding = {
logo_url: "" ,
headline: "Welcome, {{attendee_name}}!" ,
subtitle: "{{event_name}}" ,
banner_url: "" ,
qr_section_label: "Your Check-In QR Code" ,
section1_title: "" ,
section1_body: "" ,
pdf1_url: "" ,
pdf1_label: "" ,
pdf2_url: "" ,
pdf2_label: "" ,
section2_title: "" ,
section2_body: "" ,
button1_label: "" ,
button1_url: "" ,
button2_label: "" ,
button2_url: "" ,
footer: "" ,
};