Skip to content

Latest commit

 

History

History
1279 lines (1037 loc) · 49.6 KB

File metadata and controls

1279 lines (1037 loc) · 49.6 KB

EventBoost - Software Design Document

Version: 1.1 Last Updated: February 2026 Status: Active Development


Table of Contents

  1. Overview
  2. Technology Stack
  3. System Architecture
  4. Backend Design
  5. Frontend Design
  6. Database Design
  7. Authentication & Authorization
  8. AI Services
  9. API Reference
  10. Configuration

Overview

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.

Core Features

  • 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

Technology Stack

Backend

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 -

Frontend

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

System Architecture

┌─────────────────────────────────────────────────────────────────┐
│                         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 Design

Directory Structure

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

Pattern: Layered Architecture

The backend follows a layered architecture with clear separation:

┌─────────────────────────────────────────┐
│            Endpoints Layer              │  ← HTTP handling, validation
├─────────────────────────────────────────┤
│            Services Layer               │  ← Business logic
├─────────────────────────────────────────┤
│           Database Layer                │  ← Supabase client operations
└─────────────────────────────────────────┘

Pattern: Dependency Injection

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
    ...

Pattern: Pydantic Schema Validation

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
    ...

Service Layer

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 base

Flyer 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 Design

Directory Structure

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

Pattern: React Context for Global State

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>
  )
}

Pattern: Protected Route Components

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>

Pattern: API Abstraction Layer

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)
}

Pattern: React Query for Server State

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>
  )
}

Component: Flyer Generator

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>
  )
}

Database Design

Entity Relationship

┌──────────────┐       ┌───────────────────────┐       ┌──────────────┐
│   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     │
                                                       └──────────────┘

Tables

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)

Role Enum

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

Authentication & Authorization

Flow Diagram

┌────────────────┐     ┌─────────────────┐     ┌──────────────────┐
│   User Signs   │     │  Supabase Auth  │     │   Backend API    │
│   Up/In        │────▶│  Creates JWT    │────▶│  Verifies JWT    │
└────────────────┘     └─────────────────┘     └────────┬─────────┘
                                                        │
                                                        ▼
                              ┌──────────────────────────────────────┐
                              │         Check app_roles table        │
                              │                                      │
                              │  ┌──────────┐  ┌──────────┐         │
                              │  │ pending  │  │  member  │ ───────▶│ Access granted
                              │  │          │  │  admin   │         │
                              │  │   403    │  │super_admin         │
                              │  └──────────┘  └──────────┘         │
                              └──────────────────────────────────────┘

JWT Verification

# 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

Org-Level Access

# 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"

AI Services

Content Generation

Provider Configuration:

  • Primary: Azure OpenAI (if configured)
  • Fallback: OpenAI API
  • Model: gpt-4o-mini

Channels Generated:

Channel Format Character Limit
Instagram Caption with hashtags ~2200
WhatsApp 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"
}

Image Generation (DALL-E 3 / gpt-image-1.5)

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 a flyer_format parameter 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

Agent + RAG Architecture

EventBoost includes a context-aware RAG (Retrieval-Augmented Generation) system that learns from past generated content to provide smarter suggestions.

Overview

┌─────────────────────────────────────────────────────────────────┐
│                     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      │                           │
│  └─────────────────────────────────┘                           │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Embedding Service (services/embeddings.py)

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]

Agent Service (services/agent.py)

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

Database Schema (pgvector)

-- 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);

Frontend Integration

Agent Suggest Button:

  • Calls /agent/content/suggest for 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/iterate for 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

Link Tracking System

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.

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                     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                      │            │
│     └─────────────────────────────────────────────┘            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Database Schema

-- 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);

Tracking Slug Generation

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}"

Endpoints

Method Endpoint Auth Description
GET /e/{tracking_slug} Public Redirect + log click
GET /api/v1/events/{id}/clicks JWT Click statistics

Click Stats Response

{
  "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": [...]
}

Configuration

# Backend environment
TRACKING_BASE_URL=https://go.eventboost.dev  # or http://localhost:8000 for dev

Security

  • 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

API Reference

Base URL

Production: https://api.eventboost.app/api/v1
Development: http://localhost:8000/api/v1

Endpoints Summary

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

Configuration

Backend Environment Variables

# 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"

Frontend Environment Variables

VITE_API_URL=https://api.eventboost.app/api/v1
VITE_SUPABASE_URL=https://xxx.supabase.co
VITE_SUPABASE_ANON_KEY=eyJ...

Supabase Storage Buckets

Bucket Purpose Access
generated-flyers AI-generated & uploaded flyers Authenticated read/write
org-uploads Organization logos & assets Public read, authenticated write

Deployment

Component Platform Config
Backend Vercel vercel.json
Frontend Vercel Auto-detected Vite
Database Supabase Managed PostgreSQL
Storage Supabase Storage S3-compatible

Student Layer (v1.2)

Overview

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

Engage iCal Ingester

Standalone CLI script (backend/scripts/sync_engage.py) that:

  1. Fetches iCal from https://engage.babson.edu/ical/babsongrad/ical_babsongrad.ics
  2. Parses VEVENTs with structured fields:
    • ORGANIZER;CN="Org Name":https://engage.babson.edu/ACRONYM/ → org matching
    • URL → event RSVP link (dedup key against events.rsvp_link)
    • UID → stable dedup via events.engage_uid
    • CATEGORIES;X-CG-CATEGORY=event_type/event_tags → direct tag extraction
  3. Smart-matches orgs by engage_url → name (ILIKE) → auto-create
  4. Deduplicates events by engage_uidrsvp_link → insert new

New Database Tables

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_uid columns; org_id now nullable
  • organizations + engage_url column
  • app_roles + user_mode column (student/leader)

Student API Endpoints

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

Recommendation Scoring

score = tag_overlap * 0.2 (capped 0.6) + recency_boost (0.2 if <24h) + food_bonus (0.1) - muted (-1)

Frontend Pages

  • StudentFeed.tsx - Public event feed with Today/This Week tabs, chat panel
  • StudentOnboarding.tsx - Interest picker grid for personalization
  • Navbar mode toggle: Student | Leader

Future Considerations

  • 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.