Version: 1.1 Last Updated: February 2026 Status: Active Development
- Overview
- Technology Stack
- System Architecture
- Backend Design
- Frontend Design
- Database Design
- Authentication & Authorization
- AI Services
- API Reference
- Configuration
EventBoost is a multi-tenant SaaS platform for event marketing. It enables organizations to create events and generate AI-powered marketing content across multiple channels (Instagram, WhatsApp, Email, Campus Engage), along with customizable AI-generated flyers.
- Multi-organization support with role-based access
- Event creation and management
- Link tracking - trackable short links for RSVP/Engage URLs with click analytics
- AI-powered marketing content generation (GPT-4o / Azure OpenAI)
- Context-aware RAG system - learns from past content for smarter suggestions
- Per-channel iteration - refine individual content pieces with natural language
- AI-generated flyer backgrounds (DALL-E 3)
- Agent-powered flyer styling - recommendations based on similar events
- Organization branding customization
- Admin dashboard for user/org management
| Component | Technology | Version |
|---|---|---|
| Framework | FastAPI | 0.125.0 |
| Language | Python | 3.12+ |
| Server | Uvicorn | 0.38.0 |
| Validation | Pydantic | 2.12.5 |
| Database | Supabase (PostgreSQL) | - |
| Auth | Supabase Auth + JWT | - |
| AI (Text) | OpenAI / Azure OpenAI | gpt-4o-mini |
| AI (Image) | DALL-E 3 | - |
| Component | Technology | Version |
|---|---|---|
| Framework | React | 19.2.0 |
| Language | TypeScript | 5.9.3 |
| Build Tool | Vite | 7.2.4 |
| Styling | Tailwind CSS | 4.1.18 |
| Routing | React Router DOM | 7.11.0 |
| Server State | TanStack Query | 5.90.12 |
| HTTP Client | Axios | 1.13.2 |
| Canvas | html2canvas | 1.4.1 |
┌─────────────────────────────────────────────────────────────────┐
│ CLIENT BROWSER │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ React SPA (Vite) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │
│ │ │ AuthContext │ │ OrgContext │ │ React Query │ │ │
│ │ └──────┬──────┘ └──────┬──────┘ └────────┬────────┘ │ │
│ │ └────────────────┴──────────────────┘ │ │
│ │ │ │ │
│ │ Axios (JWT) │ │
│ └──────────────────────────┼────────────────────────────────┘ │
└─────────────────────────────┼───────────────────────────────────┘
│ HTTPS
▼
┌─────────────────────────────────────────────────────────────────┐
│ FASTAPI BACKEND │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ API Router (v1) │ │
│ │ /auth /admin /organizations /events /generate /flyers │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────────┴───────────────────────────────┐ │
│ │ Service Layer │ │
│ │ ┌─────────────────┐ ┌──────────────────────────────┐ │ │
│ │ │ LLM Service │ │ Flyer Generator Service │ │ │
│ │ └────────┬────────┘ └──────────────┬───────────────┘ │ │
│ └───────────┼──────────────────────────┼───────────────────┘ │
└──────────────┼──────────────────────────┼───────────────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌───────────────────────────────────┐
│ OpenAI / Azure │ │ SUPABASE │
│ ┌────────────────┐ │ │ ┌───────────┐ ┌─────────────┐ │
│ │ GPT-4o (text) │ │ │ │ PostgreSQL│ │ Storage │ │
│ │ DALL-E 3 (img) │ │ │ │ (Tables) │ │ (Buckets) │ │
│ └────────────────┘ │ │ └───────────┘ └─────────────┘ │
└──────────────────────┘ └───────────────────────────────────┘
backend/
├── app/
│ ├── main.py # FastAPI initialization + /e/{slug} redirect
│ ├── core/
│ │ ├── config.py # Pydantic Settings
│ │ ├── security.py # JWT & RBAC
│ │ └── cors.py # CORS setup
│ ├── api/v1/
│ │ ├── router.py # Route aggregation
│ │ └── endpoints/
│ │ ├── auth.py
│ │ ├── admin.py
│ │ ├── organizations.py
│ │ ├── events.py # CRUD + tracking slug generation
│ │ ├── generate.py
│ │ ├── flyers.py
│ │ └── redirect.py # Click tracking & stats
│ ├── db/
│ │ └── supabase_client.py
│ ├── services/
│ │ ├── llm.py
│ │ ├── agent.py # RAG-powered generation
│ │ ├── embeddings.py # Vector search
│ │ └── flyer_generator.py
│ └── schemas/
│ ├── admin.py
│ ├── generate.py
│ └── flyers.py
├── migrations/
│ └── 009_link_tracking.sql # tracking_slug + clicks table
└── requirements.txt
The backend follows a layered architecture with clear separation:
┌─────────────────────────────────────────┐
│ Endpoints Layer │ ← HTTP handling, validation
├─────────────────────────────────────────┤
│ Services Layer │ ← Business logic
├─────────────────────────────────────────┤
│ Database Layer │ ← Supabase client operations
└─────────────────────────────────────────┘
FastAPI's Depends() is used for auth guards and service injection:
# Pseudo-code: Auth dependency chain
def get_current_user(token: str = Depends(oauth2_scheme)):
# Verify JWT with Supabase
user = supabase.auth.get_user(token)
if not user:
raise HTTPException(401)
return user
def get_current_user_with_role(user = Depends(get_current_user)):
# Fetch app_role from database
role = db.query("app_roles", user_id=user.id)
return { ...user, app_role: role }
def require_member(user = Depends(get_current_user_with_role)):
if user.app_role not in ["member", "admin", "super_admin"]:
raise HTTPException(403)
return user
# Usage in endpoint
@router.post("/events")
def create_event(data: EventCreate, user = Depends(require_member)):
# Only members+ can create events
...Request/response validation with Pydantic models:
# Pseudo-code: Schema definitions
class EventCreate(BaseModel):
org_id: UUID
title: str
starts_at: datetime
location: str
description: str
audience: str
perks: Optional[str]
rsvp_link: Optional[HttpUrl]
tone: Literal["formal", "casual", "exciting"]
class GeneratedContent(BaseModel):
instagram: str
whatsapp: str
email_subject: str
email_body: str
engage: str
# Automatic validation on endpoint
@router.post("/events", response_model=EventResponse)
def create_event(data: EventCreate):
# data is already validated
...Business logic is encapsulated in service modules:
LLM Service (services/llm.py)
# Pseudo-code: Content generation
class LLMService:
def __init__(self, provider: "openai" | "azure"):
self.client = self._init_client(provider)
async def generate_content(self, event: Event, feedback: str = None):
prompt = self._build_prompt(event, feedback)
response = await self.client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": MARKETING_SYSTEM_PROMPT},
{"role": "user", "content": prompt}
],
response_format={"type": "json_object"}
)
return GeneratedContent.parse(response.content)
def _build_prompt(self, event, feedback):
base = f"""
Event: {event.title}
Date: {event.starts_at}
Location: {event.location}
Description: {event.description}
Target Audience: {event.audience}
Tone: {event.tone}
"""
if feedback:
base += f"\n\nUser Feedback: {feedback}"
return baseFlyer Generator Service (services/flyer_generator.py)
# Pseudo-code: AI background/flyer generation
# Format configuration
FORMAT_CONFIG = {
"instagram_post": {"size": "1024x1024", "cost": 0.04},
"instagram_story": {"size": "1024x1536", "cost": 0.08},
"general_flyer": {"size": "1024x1536", "cost": 0.08},
}
class FlyerGeneratorService:
async def generate_background(self, event: Event, format: FlyerFormat):
config = FORMAT_CONFIG[format]
prompt = f"""
Abstract background for event poster.
Theme: {event.description}
Style: Modern, vibrant, professional.
IMPORTANT: No text, no letters, no words.
"""
response = await openai.images.generate(
model="dall-e-3", # or gpt-image-1.5
prompt=prompt,
size=config["size"],
quality="standard"
)
image_b64 = self._extract_image_b64(response.data[0])
storage_path = await self._upload_to_storage(image_b64, event.id)
return { image_url, storage_path, cost: config["cost"] }
async def edit_flyer(self, source_b64, instruction, flyer_format):
config = FORMAT_CONFIG[flyer_format]
# Uses images.edit API with format-aware sizing
response = await openai.images.edit(
model="gpt-image-1.5",
image=source_buffer,
prompt=instruction,
size=config["size"]
)
return self._extract_image_b64(response.data[0])
def _extract_image_b64(self, response_data):
# Handles both b64_json and URL responses
# Converts to PNG for consistent mime type handling
...frontend/src/
├── main.tsx # Entry point
├── App.tsx # Router setup
├── pages/
│ ├── Login.tsx
│ ├── NewEvent.tsx
│ ├── EventsList.tsx
│ ├── EventDetail.tsx # Consolidated event view (content, flyers, edit modal)
│ ├── Organizations.tsx
│ ├── AdminDashboard.tsx
│ ├── Profile.tsx
│ └── Feedback.tsx
├── features/
│ ├── auth/
│ │ └── AuthContext.tsx
│ └── organizations/
│ └── OrganizationContext.tsx
├── components/
│ ├── Layout.tsx
│ ├── Navbar.tsx
│ ├── OrgSelector.tsx
│ └── flyers/
│ ├── FlyerGenerator.tsx
│ ├── FlyerCanvas.tsx
│ ├── FlyerEditor.tsx
│ ├── FlyerPreview.tsx
│ └── templates.ts
└── lib/
├── api.ts # Event, ClickStats types + update/getClickStats
└── supabase.ts
Auth and organization state managed via Context API:
// Pseudo-code: AuthContext
interface AuthState {
user: User | null
session: Session | null
appRole: "pending" | "member" | "admin" | "super_admin" | null
isApproved: boolean
loading: boolean
}
const AuthContext = createContext<AuthState>(null)
function AuthProvider({ children }) {
const [state, setState] = useState<AuthState>(initialState)
useEffect(() => {
// Subscribe to Supabase auth changes
supabase.auth.onAuthStateChange((event, session) => {
if (session) {
setState(s => ({ ...s, user: session.user, session }))
fetchAppRole(session.user.id)
} else {
setState(initialState)
}
})
}, [])
async function fetchAppRole(userId: string) {
const role = await adminApi.getMyRole()
setState(s => ({
...s,
appRole: role,
isApproved: role !== "pending"
}))
}
return (
<AuthContext.Provider value={{ ...state, signIn, signOut, signUp }}>
{children}
</AuthContext.Provider>
)
}Route-level access control using wrapper components:
// Pseudo-code: Protected routes
function ProtectedRoute({ children }) {
const { user, loading } = useAuth()
if (loading) return <Spinner />
if (!user) return <Navigate to="/login" />
return children
}
function ApprovedRoute({ children }) {
const { isApproved, loading } = useAuth()
if (loading) return <Spinner />
if (!isApproved) return <Navigate to="/pending" />
return children
}
function AdminRoute({ children }) {
const { appRole, loading } = useAuth()
if (loading) return <Spinner />
if (!["admin", "super_admin"].includes(appRole)) {
return <Navigate to="/" />
}
return children
}
// Usage in App.tsx
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/pending" element={<ProtectedRoute><PendingApproval /></ProtectedRoute>} />
<Route path="/" element={<ApprovedRoute><Layout /></ApprovedRoute>}>
<Route index element={<EventsList />} />
<Route path="events/new" element={<NewEvent />} />
<Route path="admin" element={<AdminRoute><AdminDashboard /></AdminRoute>} />
</Route>
</Routes>Typed API functions with Axios interceptors:
// Pseudo-code: lib/api.ts
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL
})
// Inject JWT on every request
api.interceptors.request.use(async (config) => {
const { data: { session } } = await supabase.auth.getSession()
if (session?.access_token) {
config.headers.Authorization = `Bearer ${session.access_token}`
}
return config
})
// Typed API namespaces
export const eventApi = {
list: (orgId: string): Promise<Event[]> =>
api.get(`/organizations/${orgId}/events`).then(r => r.data),
create: (data: EventCreate): Promise<Event> =>
api.post("/events", data).then(r => r.data),
get: (id: string): Promise<Event> =>
api.get(`/events/${id}`).then(r => r.data)
}
export const generateApi = {
generateContent: (eventId: string, feedback?: string): Promise<GeneratedContent> =>
api.post("/generate", { event_id: eventId, feedback }).then(r => r.data),
getContent: (eventId: string): Promise<GeneratedContent> =>
api.get(`/generate/${eventId}`).then(r => r.data)
}Server state management with automatic caching and refetching:
// Pseudo-code: Using React Query in components
function EventsList() {
const { selectedOrg } = useOrganization()
const { data: events, isLoading, error } = useQuery({
queryKey: ["events", selectedOrg?.id],
queryFn: () => eventApi.list(selectedOrg.id),
enabled: !!selectedOrg
})
if (isLoading) return <Spinner />
if (error) return <ErrorMessage error={error} />
return (
<ul>
{events.map(event => <EventCard key={event.id} event={event} />)}
</ul>
)
}
function NewEvent() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: eventApi.create,
onSuccess: () => {
queryClient.invalidateQueries(["events"])
navigate("/events")
}
})
return (
<form onSubmit={(e) => {
e.preventDefault()
mutation.mutate(formData)
}}>
...
</form>
)
}The flyer system uses a canvas-based approach:
// Pseudo-code: Flyer generation flow
function FlyerGenerator({ event }) {
const [template, setTemplate] = useState(TEMPLATES[0])
const [background, setBackground] = useState(null)
const canvasRef = useRef()
// Generate AI background
async function generateAIBackground() {
const result = await flyerApi.generateBackground({
event_id: event.id,
format: template.format,
prompt_hint: event.description
})
setBackground(result.image_url)
}
// Capture canvas as image
async function captureFlyer() {
const canvas = await html2canvas(canvasRef.current)
const blob = await canvasToBlob(canvas)
await flyerApi.upload({
event_id: event.id,
template_id: template.id,
image: blob
})
}
return (
<div>
<TemplateSelector value={template} onChange={setTemplate} />
<FlyerCanvas
ref={canvasRef}
event={event}
template={template}
background={background}
/>
<button onClick={generateAIBackground}>Generate AI Background</button>
<button onClick={captureFlyer}>Save Flyer</button>
</div>
)
}┌──────────────┐ ┌───────────────────────┐ ┌──────────────┐
│ auth.users │ │ organizations │ │ events │
├──────────────┤ ├───────────────────────┤ ├──────────────┤
│ id (PK) │──┐ │ id (PK) │──┐ │ id (PK) │
│ email │ │ │ name │ │ │ org_id (FK) │──┐
│ ... │ │ │ created_by (FK) │──┘ │ title │ │
└──────────────┘ │ │ created_at │ │ starts_at │ │
│ └───────────────────────┘ │ location │ │
│ │ │ description │ │
│ │ │ audience │ │
┌──────────────┐ │ ┌───────────────────────┐ │ perks │ │
│ app_roles │ │ │ organization_members │ │ rsvp_link │ │
├──────────────┤ │ ├───────────────────────┤ │ tone │ │
│ id (PK) │ │ │ org_id (FK) │ │ created_by │ │
│ user_id (FK) │──┤ │ user_id (FK) │──┐ └──────────────┘ │
│ role (enum) │ │ │ role (owner/member) │ │ │ │
│ approved_by │ │ └───────────────────────┘ │ │ │
│ approved_at │ │ │ │ │
└──────────────┘ │ │ ▼ │
│ │ ┌──────────────┐ │
│ │ │generated_assets│ │
│ │ ├──────────────┤ │
│ │ │ id (PK) │ │
│ │ │ event_id (FK)│──┘
│ │ │ type (channel)│
│ │ │ content │
│ │ │ iteration │
│ │ │ feedback │
│ │ │ model_provider│
│ │ └──────────────┘
│ │
│ ┌───────────────────────┐ │ ┌──────────────┐
│ │ org_access_requests │ │ │ flyers │
│ ├───────────────────────┤ │ ├──────────────┤
│ │ id (PK) │ │ │ id (PK) │
│ │ org_id (FK) │ │ │ event_id (FK)│
└────│ user_id (FK) │ │ │ template_id │
│ status (enum) │ │ │ image_url │
│ reviewed_by (FK) │──┘ │ storage_path │
└───────────────────────┘ │ gen_method │
│ gen_cost │
└──────────────┘
| Table | Description |
|---|---|
app_roles |
Maps users to application-level roles (pending/member/admin/super_admin) |
organizations |
Event-organizing entities (clubs, companies, etc.) |
organization_members |
Junction table for user-org membership with role |
org_access_requests |
Pending requests to join organizations |
organization_branding |
Org-level branding config (logo, colors, font) |
events |
Event details with marketing metadata + tracking_slug for link tracking |
generated_assets |
LLM-generated content per channel, with iteration tracking |
flyers |
Generated/uploaded flyer images with metadata |
clicks |
Click analytics for tracking links (event_id, timestamp, referrer, UTM params) |
CREATE TYPE app_role AS ENUM ('pending', 'member', 'admin', 'super_admin');| Role | Permissions |
|---|---|
pending |
Can view pending approval page only |
member |
Create/view events in joined orgs, generate content |
admin |
All member permissions + approve users, manage org requests |
super_admin |
All permissions + system-wide access |
┌────────────────┐ ┌─────────────────┐ ┌──────────────────┐
│ User Signs │ │ Supabase Auth │ │ Backend API │
│ Up/In │────▶│ Creates JWT │────▶│ Verifies JWT │
└────────────────┘ └─────────────────┘ └────────┬─────────┘
│
▼
┌──────────────────────────────────────┐
│ Check app_roles table │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ pending │ │ member │ ───────▶│ Access granted
│ │ │ │ admin │ │
│ │ 403 │ │super_admin │
│ └──────────┘ └──────────┘ │
└──────────────────────────────────────┘
# Pseudo-code: Token verification
async def verify_token(token: str) -> User:
# Supabase verifies signature & expiration
response = await supabase.auth.get_user(token)
if response.error:
raise HTTPException(401, "Invalid or expired token")
return response.user# Pseudo-code: Checking org membership
async def check_org_access(user_id: str, org_id: str):
membership = await db.query(
"organization_members",
user_id=user_id,
org_id=org_id
)
if not membership:
raise HTTPException(403, "Not a member of this organization")
return membership.role # "owner" or "member"Provider Configuration:
- Primary: Azure OpenAI (if configured)
- Fallback: OpenAI API
- Model: gpt-4o-mini
Channels Generated:
| Channel | Format | Character Limit |
|---|---|---|
| Caption with hashtags | ~2200 | |
| Broadcast message | ~1000 | |
| Email Subject | Subject line | ~60 |
| Email Body | HTML email content | ~5000 |
| Campus Engage | Platform-specific post | ~500 |
Iteration Tracking:
# Each generation creates a new iteration
{
"event_id": "uuid",
"type": "instagram",
"content": "...",
"iteration": 3, # Increments on regeneration
"feedback": "Make it more casual",
"model_provider": "azure",
"model_name": "gpt-4o-mini"
}Formats:
| Format | Dimensions | Use Case |
|---|---|---|
| Instagram Post | 1024x1024 | Square feed posts |
| Instagram Story | 1024x1536 | Portrait stories/reels |
| General Flyer | 1024x1536 | Portrait print/digital flyers |
Cost: ~$0.04-0.08 per image (standard quality)
Prompt Engineering:
- Always includes "NO TEXT" instruction for backgrounds
- Abstract/background focus to allow text overlay
- Event theme/description incorporated
- Element placement guidelines ensure logos/QR codes are fully visible with 30-50px margins
- Explicit instructions prevent edge cropping of important elements
Edit Flyer Support:
edit_flyer()accepts aflyer_formatparameter to output correct dimensions- Supports natural language edit instructions
- Falls back to regeneration if edit API unavailable
- Preserves original format when editing existing flyers
Image Processing:
- URL-fetched images are converted to PNG for consistent mime type handling
- All images use RGBA format for API compatibility
EventBoost includes a context-aware RAG (Retrieval-Augmented Generation) system that learns from past generated content to provide smarter suggestions.
┌─────────────────────────────────────────────────────────────────┐
│ Agent Request Flow │
├─────────────────────────────────────────────────────────────────┤
│ │
│ User: "Suggest content for my event" │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ Embedding Service │ │
│ │ - Embed event description │ │
│ │ - Query similar past assets │ │
│ └─────────────────┬───────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ Agent Service │ │
│ │ - Build context from similar │ │
│ │ - Construct RAG-enhanced prompt │ │
│ │ - Generate with LLM │ │
│ └─────────────────┬───────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ Save & Index New Content │ │
│ │ - Store in generated_assets │ │
│ │ - Create embedding for RAG │ │
│ └─────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Uses OpenAI's text-embedding-3-small model (1536 dimensions) to:
- Embed generated content for similarity search
- Embed events for finding similar past events
- Compute cosine similarity in Python using numpy
# Pseudo-code: Embedding and similarity search
class EmbeddingService:
def embed_text(self, text: str) -> list[float]:
response = openai.embeddings.create(
model="text-embedding-3-small",
input=text
)
return response.data[0].embedding
def get_similar_assets(self, org_id, channel, query_text, limit=5):
query_embedding = self.embed_text(query_text)
# Fetch assets with embeddings from Supabase
assets = supabase.table("generated_assets")
.select("*, content_embedding")
.eq("type", channel)
.execute()
# Compute cosine similarity in Python
scored = []
for asset in assets:
similarity = cosine_similarity(query_embedding, asset.embedding)
scored.append({...asset, similarity})
return sorted(scored, key=lambda x: x.similarity, reverse=True)[:limit]Orchestrates RAG-powered generation:
# Pseudo-code: Agent content suggestion
class AgentService:
async def suggest_content(self, event_id: str):
# 1. Load event context
event = await db.get_event(event_id)
branding = await db.get_org_branding(event.org_id)
# 2. Find similar past content per channel
event_text = f"{event.title}\n{event.description}"
similar_by_channel = {}
for channel in CHANNELS:
similar_by_channel[channel] = embeddings.get_similar_assets(
org_id=event.org_id,
channel=channel,
query_text=event_text,
limit=3
)
# 3. Build context-rich prompt
prompt = f"""
Event: {event.title}
Branding: {branding}
Similar past content for reference:
{similar_by_channel}
Generate content for all channels...
"""
# 4. Generate with LLM
content = await llm.generate(prompt)
# 5. Save and index for future RAG
for channel, text in content.items():
asset = await db.save_asset(event_id, channel, text)
embeddings.index_generated_asset(asset.id, text)
return content
async def iterate_content(self, event_id, channel, instruction):
# Refine specific channel based on user instruction
current = await db.get_latest_asset(event_id, channel)
similar = embeddings.get_similar_assets(...)
refined = await llm.generate(f"""
Current: {current.content}
Instruction: {instruction}
Similar examples: {similar}
""")
return refined-- Vector columns for RAG
ALTER TABLE generated_assets
ADD COLUMN content_embedding vector(1536);
ALTER TABLE events
ADD COLUMN event_embedding vector(1536);
-- Indexes for fast similarity search
CREATE INDEX idx_generated_assets_embedding
ON generated_assets USING ivfflat (content_embedding vector_cosine_ops);Agent Suggest Button:
- Calls
/agent/content/suggestfor RAG-powered suggestions - Displays count of similar assets used for context
Per-Channel Iteration:
- Each content card has a "Refine" button
- User types instruction (e.g., "Make it shorter")
- Calls
/agent/content/iteratefor that channel only
Flyer Concept Suggestion:
- "Let Agent Pick Style" button in FlyerGenerator
- Agent recommends format, template, colors based on similar events
- Shows reasoning for recommendations
EventBoost includes a link tracking system that generates short, trackable URLs for event RSVP/Engage links. This allows organizations to measure click-through rates across different marketing channels.
┌─────────────────────────────────────────────────────────────────┐
│ Link Tracking Flow │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Event Creation with RSVP Link │
│ ┌─────────────────────────────────────────────┐ │
│ │ POST /events │ │
│ │ { rsvp_link: "https://engage.edu/event/123" }│ │
│ └─────────────────────┬───────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Auto-generate tracking_slug │ │
│ │ "spring-kickoff-a1b2c3" │ │
│ └─────────────────────┬───────────────────────┘ │
│ │ │
│ 2. Tracking URL Used in Content │
│ ┌─────────────────────────────────────────────┐ │
│ │ AI generates content with tracking URL: │ │
│ │ "RSVP at https://go.eventboost.dev/e/..." │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 3. User Clicks Tracking Link │
│ ┌─────────────────────────────────────────────┐ │
│ │ GET /e/{tracking_slug} │ │
│ │ ?utm_source=instagram&utm_medium=social │ │
│ └─────────────────────┬───────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Log click metadata: │ │
│ │ - referrer, user_agent, ip_hash │ │
│ │ - utm_source, utm_medium, utm_campaign │ │
│ └─────────────────────┬───────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 302 Redirect → rsvp_link │ │
│ │ Cache-Control: no-store │ │
│ └─────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
-- Add tracking_slug to events
ALTER TABLE events ADD COLUMN tracking_slug TEXT UNIQUE;
CREATE INDEX idx_events_tracking_slug ON events(tracking_slug);
-- Clicks table for analytics
CREATE TABLE clicks (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
clicked_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
referrer TEXT,
user_agent TEXT,
ip_hash TEXT, -- Privacy-friendly (salted SHA-256, truncated)
utm_source TEXT,
utm_medium TEXT,
utm_campaign TEXT
);
CREATE INDEX idx_clicks_event_id ON clicks(event_id);
CREATE INDEX idx_clicks_clicked_at ON clicks(clicked_at);def generate_tracking_slug(title: str) -> str:
"""
Generate URL-friendly tracking slug from event title.
Format: slugified-title-xxxx (6 hex char random suffix)
"""
slug = re.sub(r'[^a-z0-9]+', '-', title.lower()).strip('-')[:40]
suffix = secrets.token_hex(3)
return f"{slug}-{suffix}"| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /e/{tracking_slug} |
Public | Redirect + log click |
| GET | /api/v1/events/{id}/clicks |
JWT | Click statistics |
{
"event_id": "uuid",
"total_clicks": 142,
"by_source": {
"instagram": 58,
"whatsapp": 45,
"email": 27,
"direct": 12
},
"by_day": {
"2026-02-01": 23,
"2026-02-02": 45,
"2026-02-03": 74
},
"recent_clicks": [...]
}# Backend environment
TRACKING_BASE_URL=https://go.eventboost.dev # or http://localhost:8000 for dev- No open redirects: Destination URLs are stored server-side, not passed as query params
- IP hashing: Client IPs are salted and hashed for privacy
- UTM passthrough: Query params passed through to destination for attribution
Production: https://api.eventboost.app/api/v1
Development: http://localhost:8000/api/v1
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /health |
- | Health check |
| POST | /auth/signup |
- | User registration |
| POST | /auth/signin |
- | User login |
| POST | /auth/signout |
JWT | Logout |
| GET | /admin/me |
JWT | Get current user role |
| GET | /admin/pending |
Admin | List pending users |
| POST | /admin/approve/{user_id} |
Admin | Approve user |
| GET | /admin/org-requests |
Admin | List org access requests |
| POST | /admin/org-requests/{id}/approve |
Admin | Approve org request |
| GET | /organizations |
Member | List user's orgs |
| POST | /organizations |
Member | Create org |
| POST | /organizations/{id}/request-access |
JWT | Request org membership |
| GET | /organizations/{id}/branding |
JWT | Get org branding |
| PUT | /organizations/{id}/branding |
Member | Update org branding |
| GET | /events |
Member | List org events |
| POST | /events |
Admin | Create event (auto-generates tracking_slug) |
| GET | /events/{id} |
Member | Get event details (includes tracking_url) |
| PUT | /events/{id} |
Admin | Update event |
| DELETE | /events/{id} |
SuperAdmin | Delete event |
| GET | /e/{tracking_slug} |
Public | Redirect + log click |
| GET | /events/{id}/clicks |
JWT | Get click statistics |
| POST | /generate |
JWT | Generate marketing content |
| GET | /generate/{event_id} |
JWT | Get existing content |
| POST | /flyers/upload |
JWT | Upload rendered flyer |
| POST | /flyers/generate-background |
JWT | Generate AI background |
| POST | /flyers/generate-full |
JWT | Generate complete AI flyer with text |
| POST | /flyers/edit |
JWT | Edit existing flyer with AI |
| GET | /flyers/event/{event_id} |
JWT | List event flyers |
| POST | /agent/content/suggest |
JWT | RAG-powered content suggestions |
| POST | /agent/content/iterate |
JWT | Refine specific channel content |
| POST | /agent/flyer/suggest |
JWT | Flyer concept recommendations |
# Supabase
SUPABASE_URL=https://xxx.supabase.co
SUPABASE_SERVICE_KEY=eyJ... # Admin operations
SUPABASE_ANON_KEY=eyJ... # Client auth
# OpenAI (optional if using Azure)
OPENAI_API_KEY=sk-...
# Azure OpenAI (preferred)
AZURE_OPENAI_ENDPOINT=https://xxx.openai.azure.com
AZURE_OPENAI_API_KEY=xxx
AZURE_OPENAI_DEPLOYMENT=gpt-4o-mini
# Azure DALL-E
AZURE_IMAGE_ENDPOINT=https://xxx.openai.azure.com
AZURE_IMAGE_API_KEY=xxx
AZURE_IMAGE_DEPLOYMENT=dall-e-3
# Link Tracking
TRACKING_BASE_URL=https://go.eventboost.dev # Base URL for tracking links
# Application
FRONTEND_URL=https://eventboost.app
ENVIRONMENT=production # or "development"VITE_API_URL=https://api.eventboost.app/api/v1
VITE_SUPABASE_URL=https://xxx.supabase.co
VITE_SUPABASE_ANON_KEY=eyJ...| Bucket | Purpose | Access |
|---|---|---|
generated-flyers |
AI-generated & uploaded flyers | Authenticated read/write |
org-uploads |
Organization logos & assets | Public read, authenticated write |
| Component | Platform | Config |
|---|---|---|
| Backend | Vercel | vercel.json |
| Frontend | Vercel | Auto-detected Vite |
| Database | Supabase | Managed PostgreSQL |
| Storage | Supabase Storage | S3-compatible |
Adds a student-facing layer that ingests campus events from Babson Engage (iCal feed) and provides personalized event recommendations via a public web feed with LLM-powered chat.
Two user modes:
- Student (default): Recommendation feed, chat, interest-based personalization
- Leader: Existing org/event/flyer creation experience
Standalone CLI script (backend/scripts/sync_engage.py) that:
- Fetches iCal from
https://engage.babson.edu/ical/babsongrad/ical_babsongrad.ics - Parses VEVENTs with structured fields:
ORGANIZER;CN="Org Name":https://engage.babson.edu/ACRONYM/→ org matchingURL→ event RSVP link (dedup key againstevents.rsvp_link)UID→ stable dedup viaevents.engage_uidCATEGORIES;X-CG-CATEGORY=event_type/event_tags→ direct tag extraction
- Smart-matches orgs by
engage_url→ name (ILIKE) → auto-create - Deduplicates events by
engage_uid→rsvp_link→ insert new
student_profiles - user_id (PK), interests[], graduation_year, onboarded
student_actions - id, user_id, event_id, action (view/save/click/mute)
Altered tables:
events+source,tags[],engage_uidcolumns;org_idnow nullableorganizations+engage_urlcolumnapp_roles+user_modecolumn (student/leader)
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /student/feed |
Optional | Personalized event recommendations |
| GET | /student/profile |
JWT | Get student profile |
| POST | /student/profile |
JWT | Create/update profile (onboarding) |
| POST | /student/actions |
JWT | Log action (view/save/click/mute) |
| GET | /student/saved |
JWT | Get saved events |
| POST | /student/chat |
Optional | LLM chat about campus events |
| GET | /student/mode |
JWT | Get user_mode |
| PUT | /student/mode |
JWT | Toggle student/leader mode |
score = tag_overlap * 0.2 (capped 0.6) + recency_boost (0.2 if <24h) + food_bonus (0.1) - muted (-1)
StudentFeed.tsx- Public event feed with Today/This Week tabs, chat panelStudentOnboarding.tsx- Interest picker grid for personalization- Navbar mode toggle: Student | Leader
- Link tracking with click analytics
- Event update functionality with edit modal
- Student layer with Engage iCal ingestion
- SMS/WhatsApp bot via Twilio
- Magic link authentication
- Real-time collaboration (Supabase Realtime)
- Enhanced analytics dashboard (beyond click stats)
- Template marketplace for flyers
- Multi-language content generation
- Scheduled social media posting integration
- Mobile app (React Native)
- Subdomain setup for tracking links (go.eventboost.dev)
This document should be updated as the system evolves.