A multi-platform event discovery and recommendation system for university students, built for CMU.
University students miss events they'd actually enjoy because announcements are scattered across dozens of Slack channels, Discord servers, email lists, and Instagram stories. Universify solves this by aggregating events from multiple platforms into a single, searchable calendar with personalized recommendations.
The system ingests event announcements from Slack workspaces and Discord servers, uses NLP-based parsing to extract structured data (title, date/time, location, categories), deduplicates entries, and stores everything in a shared Postgres database. A cross-platform mobile and web client lets students browse, filter, search, and RSVP to events β with a recommendation engine that learns which events they're likely to attend based on category affinity, time-of-day preferences, and popularity signals.
Universify is developed under the ScottyLabs organization as part of the Labrador team at Carnegie Mellon University. Authentication is scoped to @andrew.cmu.edu and @cmu.edu email domains via Google OAuth.
- Multi-source event ingestion β Slack bot (real-time Socket Mode + REST API) and Discord bot (announcement channel monitoring) independently parse and submit events to a shared Supabase database.
- NLP-based message parsing β Custom regex + chrono-node parsers extract dates, times, locations, and categories from free-text announcements with confidence scoring and missing-field detection.
- Human-in-the-loop review β Discord: parsed events surface as interactive embeds with Approve / Edit / Reject buttons and a correction modal. Slack: an optional review channel with Approve / Reject buttons before events go live.
- Cross-source deduplication β Both bots and the client detect when the same announcement arrives from multiple platforms (title-token similarity within a Β±2h window), with an optional LLM tie-break via OpenRouter.
- Personalized recommendation engine β Builds interest profiles from the events a user scheduled, RSVP'd to, or created: category affinity, n-gram title matching (1β3 grams), RSVP popularity weighting (
log1p(going) + 1), and time-of-day buckets. Powers the ranked Home feed and calendar time-slot suggestions. - Google Calendar sync β Bidirectional: reads personal calendar events into the app and writes Universify events back to Google Calendar via the Calendar API with OAuth scopes.
- Cross-platform client β React Native (Expo) app with web support via
react-native-web, responsive layout with desktop navigation, and haptic feedback on native. - Week-view calendar with overlap layout β Google Calendar-style column algorithm that detects simultaneous events and renders them side-by-side with calculated widths.
- Advanced filtering and search β Multi-axis filtering by category, event type, time-of-day, date range, location, and availability. Three search modes: name-only, full-text, and weighted semantic scoring.
- RSVP and attendance tracking β Per-user RSVPs stored in their own table (so any signed-in user's RSVP persists under RLS), with trigger-maintained going/maybe/not-going aggregates, capacity tracking, and availability filtering.
- Event scheduling & reminders β Users pin events to weekly calendars; state syncs to Supabase when authenticated or falls back to local storage. On web, opt-in browser notifications fire ~30 minutes before scheduled events.
- Recurring events & flyer images β Daily/weekly/monthly recurrence with interval and end date, expanded into calendar occurrences; URL-based flyer images on cards and detail views.
- CMU-only authentication β Google OAuth via Supabase Auth with email domain enforcement (
@andrew.cmu.edu,@cmu.edu), automatic user profile creation via database trigger, and session caching. - Dark mode and accessibility β System/light/dark themes, configurable font sizes, high-contrast mode, and reduced-motion support.
| Technology | Version | Purpose |
|---|---|---|
| React Native | 0.81 | Cross-platform UI framework |
| Expo | 54 | Build toolchain, routing, native APIs |
| Expo Router | 6 | File-based routing with typed routes |
| React Navigation | 7 | Tab and stack navigation |
| React Native Reanimated | 4.1 | Gesture-driven animations |
| TypeScript | 5.9 | Type safety across the client |
| Technology | Version | Purpose |
|---|---|---|
| Slack Bolt.js | 4.1 | Real-time Slack event streaming (Socket Mode) |
| Slack Web API | 7.8 | Channel history and user info lookups |
| Express | 4.21 | REST API serving parsed events to the client |
| Discord.js | 14 | Discord gateway, message parsing, interactive embeds |
| chrono-node | 2.7 | Natural language date/time extraction (Discord bot) |
| Luxon | 3.7 | Timezone-aware date formatting (Discord bot) |
| better-sqlite3 | 12.8 | Local review session persistence (Discord bot) |
| Technology | Purpose |
|---|---|
| Supabase (Postgres) | Events, user profiles, scheduled events; Row Level Security |
| Supabase Auth | Google OAuth with @cmu.edu domain enforcement |
| AsyncStorage | Offline-capable local storage on native platforms |
| Technology | Purpose |
|---|---|
| pnpm Workspaces | Monorepo dependency management |
| GitHub Actions | CI: typecheck, lint, unit tests, web bundle check, bot checks |
| node:test + tsx | Unit tests for the pure utilities (dedupe, recurrence, layout, recommendations) |
| Playwright | End-to-end browser testing |
| ESLint | Code linting (Expo config) |
| Vercel | Web deployment target (vercel.json configured) |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DATA SOURCES β
β β
β Slack Workspace Discord Servers Google Calendar β
β (announcement channels) (announcement channels) (personal) β
ββββββββββ¬βββββββββββββββββββββββββββ¬βββββββββββββββββββββββ¬βββββββββββ
β β β
βΌ βΌ β
ββββββββββββββββββ βββββββββββββββββββββββ β
β Slack Bot β β Discord Bot β β
β β β β β
β β’ Socket Mode β β β’ Gateway listener β β
β listener β β β’ chrono-node NLP β β
β β’ Regex parser β β β’ Confidence scorer β β
β β’ Express API β β β’ Review UI (embeds β β
β β’ In-memory β β + modals) β β
β event store β β β’ SQLite sessions β β
βββββββββ¬βββββββββ ββββββββββββ¬βββββββββββ β
β β β
β ββββββββββββββββββββββ β
β β (approved events only) β
βΌ βΌ β
ββββββββββββββββββββββββββββββββββββββββ β
β Supabase (Postgres) β β
β β β
β events user_profiles β β
β ββ id (PK) ββ id (FKβauth) β β
β ββ title ββ email β β
β ββ start_time ββ name β β
β ββ end_time ββ university β β
β ββ location ββ preferences{} β β
β ββ categories[] β β
β ββ rsvp_counts{} user_scheduled_ β β
β ββ organizer_id events β β
β ββ tags[] ββ user_id (FK) β β
β ββ event_id (FK) β β
β RLS: public read ββ week_key β β
β auth'd write, owner update/delete β β
ββββββββββββββββββββ¬ββββββββββββββββββββ β
β β
βΌ βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Expo Client (Web + Native) β
β β
β ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββββββββ β
β β Home β β Calendar β β Find β β Recommendationsβ β
β β (feed) β β (week β β (search β β (interest β β
β β β β view) β β +filter)β β engine) β β
β ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββββββββ β
β β
β Auth (Google OAuth) βββΊ Google Calendar API (read/write) β
β Supabase client βββΊ Events CRUD, user profiles, scheduling β
β Slack client lib βββΊ Slack bot REST API (channel import) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- Ingestion β Bots listen for new messages in announcement channels. The Slack bot receives events via Bolt.js Socket Mode; the Discord bot monitors
GuildAnnouncementchannels and text channels matching*announcements*. - Parsing β Raw message text passes through platform-specific parsers. The Slack parser uses hand-written regex for dates (
MM/DD/YYYY,Month Day, Year), time ranges (3-5pm,3:00 PM - 5:00 PM), and location labels (Location:,at). The Discord parser uses chrono-node for flexible NLP date extraction, with Luxon for timezone handling, plus regex for room codes (WEH 5302) and labeled fields. - Review β Discord: parsed drafts are posted as rich embeds in a dedicated
#event-approvalchannel with confidence scoring; missing or low-confidence fields are flagged, and authors approve, edit via Discord modal, or reject. Slack: whenSLACK_REVIEW_CHANNELis configured, parsed events are posted there with Approve/Reject buttons; without it they are stored automatically. - Storage & deduplication β Approved events are inserted into the shared Supabase
eventstable. Before inserting, both bots run a cross-source duplicate check (title-token Jaccard similarity within a Β±2h start window; the Slack bot can optionally escalate borderline pairs to an LLM via OpenRouter). The Slack bot also maintains an in-memory store for its REST API. Both bots tag events with their source (slack,discord) for traceability. - Serving β The Expo client fetches events from Supabase on load, with a static JSON fallback for offline/demo use. Google Calendar events are fetched separately via the Calendar API and merged client-side (prefixed
gcal-). - Recommendations β The engine (
utils/recommendationEngine.ts) mines an interest profile from the events the user engaged with (scheduled, RSVP'd, created): title 1β3 grams, category frequencies, and time-of-day buckets, weighted by RSVP popularity. The Home feed ranks upcoming events by that profile plus the user's explicit category preferences; drag-selecting a time range in the calendar surfaces the top suggestions that fit the window (interest match + popularity + duration fit).
universify/
βββ apps/
β βββ client/ # Expo React Native app (web + native)
β β βββ app/ # File-based routes (Expo Router)
β β β βββ (auth)/ # Login, signup, OAuth callback screens
β β β βββ (tabs)/ # Main tab screens: home, calendar, find, create, profile
β β β βββ settings/ # Account, appearance, preferences screens
β β βββ components/ # React components
β β β βββ calendar/ # WeekView, CalendarHeader, EventDisplayCard, TimeColumn
β β β βββ events/ # EventCard, EventDetailSidebar, CreateEventForm
β β β βββ layout/ # DesktopNav, Header, FilterDrawer, ResponsiveLayout
β β β βββ recommendations/ # RecommendationCard, RecommendationsList
β β β βββ ui/ # Button, Input, Modal, SearchBar, CategoryPill, DateTimePicker
β β βββ contexts/ # React contexts (Auth, Events, Filters, GoogleCalendar, Slack, Settings)
β β βββ hooks/ # Custom hooks (useAppTheme, useRecommendations, useEventReminders, etc.)
β β βββ lib/ # API clients: Supabase, Slack REST, Google Calendar, storage
β β βββ types/ # TypeScript type definitions (Event, User, Settings)
β β βββ utils/ # Recommendation engine, dedupe, recurrence, layout, date math
β β βββ data/ # Static JSON: allEvents, categories, mock data
β β βββ tests/ # Unit tests (node:test via tsx)
β β βββ e2e/ # Playwright end-to-end tests
β β βββ scripts/ # Seed data, health checks, backend testing
β β
β βββ slack-bot/ # Slack integration service (TypeScript)
β β βββ src/
β β βββ index.ts # Entry: Express server + Bolt.js Socket Mode
β β βββ listener.ts # Real-time message handler + review-button actions
β β βββ parser.ts # Regex-based event extraction (date, time, location, categories)
β β βββ dedupe.ts # Cross-source duplicate detection (+ optional LLM tie-break)
β β βββ supabaseSubmit.ts # Parsed event β Supabase insert (service role)
β β βββ routes.ts # REST API: /health, /channels, /events, /cached
β β βββ store.ts # In-memory event store with channel-based querying
β β
β βββ discord-event-bot/ # Discord integration service (JavaScript)
β βββ src/
β βββ bot.js # Gateway client, NLP parsing, review UI, SQLite sessions
β βββ dedupe.js # Cross-source duplicate detection
β βββ supabaseSubmit.js # Approved event β Supabase insert with RLS bypass
β
βββ supabase/
β βββ migrations/
β βββ 001_initial_schema.sql # events, user_profiles, user_scheduled_events tables + RLS + indexes
β βββ 002_handle_new_user.sql # Trigger: auto-create profile on auth.users insert
β βββ 003_event_rsvps.sql # Per-user RSVPs + trigger-maintained aggregates on events
β
βββ .github/workflows/ci.yml # CI: typecheck, lint, unit tests, bundle check, bot checks
βββ pnpm-workspace.yaml # Monorepo workspace config
βββ package.json # Root scripts: dev, build, lint, typecheck, test
- Node.js β₯ 18
- pnpm β₯ 10 (
npm install -g pnpm) - A Supabase project (supabase.com)
- (Optional) Slack app credentials for the Slack bot
- (Optional) Discord bot token for the Discord bot
git clone https://github.com/YOUR_USERNAME/universify.git
cd universify
pnpm installRun the SQL migrations in your Supabase project's SQL Editor:
# In order:
# 1. supabase/migrations/001_initial_schema.sql
# 2. supabase/migrations/002_handle_new_user.sql
# 3. supabase/migrations/003_event_rsvps.sqlEnable Google OAuth in Supabase Auth settings (Dashboard β Authentication β Providers β Google). Configure the allowed redirect URL to include your app's callback route.
Client (apps/client/.env):
EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
EXPO_PUBLIC_SUPABASE_ANON_KEY=your_anon_public_key
# Optional: for seed script
# SUPABASE_SERVICE_ROLE_KEY=your_service_role_keySlack Bot (apps/slack-bot/.env):
SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_SIGNING_SECRET=your_signing_secret
SLACK_APP_TOKEN=xapp-your-app-token
PORT=3001
# Supabase persistence (omit to keep events in memory only)
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
SUPABASE_ORGANIZER_ID=uuid_of_bot_user
# Optional: human-in-the-loop review channel (Approve/Reject buttons)
# SLACK_REVIEW_CHANNEL=C0123456789
# Optional: LLM tie-break for borderline duplicate detection
# OPENROUTER_API_KEY=your_openrouter_keyDiscord Bot (apps/discord-event-bot/.env):
DISCORD_TOKEN=your_discord_bot_token
TIMEZONE=America/New_York
REVIEW_CATEGORY_NAME=Event Review
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
SUPABASE_ORGANIZER_ID=uuid_of_bot_user# Start the Expo client (web)
pnpm dev
# Start the Slack bot (separate terminal)
cd apps/slack-bot && pnpm dev
# Start the Discord bot (separate terminal)
pnpm dev:discordcd apps/client && pnpm seed| Service | Role in Universify |
|---|---|
| Supabase (Postgres + Auth) | Primary database for events, user profiles, and scheduled events. Handles authentication via Google OAuth with RLS policies enforcing ownership. Auto-provisions user profiles via a database trigger on signup. |
| Slack (Bolt.js + Web API) | The Slack bot connects via Socket Mode for real-time message streaming and exposes a REST API (/api/slack/events, /channels, /cached) that the Expo client calls to import events from specific channels. |
| Discord (Discord.js) | Monitors announcement channels in Discord servers, parses messages into event drafts, and surfaces them for human review via interactive embeds. Approved events are inserted directly into Supabase. |
| Google Calendar API | Reads the user's personal calendar events (next 4 weeks) and converts them to the Universify event format for unified display. Also supports writing Universify events back to Google Calendar. |
| OpenRouter (optional) | LLM tie-break for borderline duplicate pairs in the Slack bot's dedupe pipeline. Without an API key, deduplication falls back to the heuristic (title similarity + time window) alone. |
The Slack and Discord bots independently solve the same problem β extracting structured event data from free-text messages β but take different approaches. The Slack parser uses hand-written regex chains: first it tries MM/DD/YYYY patterns, then falls back to named months ("December 5th"), then extracts time ranges with meridian inference (if a message says "3-5pm", the parser correctly infers 3 PM, not 3 AM). The Discord parser delegates date/time extraction to chrono-node for broader NLP coverage, but adds a custom layer for CMU-specific patterns like room codes (WEH 5302, GHC 4307) and labeled fields (Location:, Scheduled for:). Both parsers infer event categories from keyword matching against the message body (e.g., "pizza" β Food, "hackathon" β Tech).
The Discord bot introduces a confidence scoring system (high / medium / low) based on which fields were successfully parsed, and tracks missing fields explicitly. This drives the review UI: low-confidence events prompt the author to click Edit, while high-confidence events can be approved with one click. Review sessions are persisted in a local SQLite database so they survive bot restarts.
Rather than requiring users to manually select interests, the recommendation engine builds implicit interest profiles from the events a user actually engaged with β scheduled, RSVP'd, or created. It tokenizes those event titles into 1-grams, 2-grams, and 3-grams (after per-token normalization and stopword filtering), then scores each gram by frequency weighted by event popularity (log1p(going_count) + 1). Categories and time-of-day buckets are scored separately and merged into a top-10 composite profile. The Home feed ranks all upcoming events against this profile (explicit category preferences from Settings get the strongest boost); time-range suggestions additionally score temporal fit, so events that fit within the drag-selected calendar window rank higher. Users with no engagement history fall back gracefully to explicit preferences + popularity + date.
The client handles three storage tiers: Supabase for authenticated persistent state, AsyncStorage for native offline caching, and localStorage for web. The storage.ts abstraction detects the platform at runtime and routes accordingly. For event scheduling (pinning events to weekly calendars), the system first tries the Supabase user_scheduled_events table if the user is authenticated, then falls back to local storage β ensuring the app remains functional without network access. Authentication uses Supabase's Google OAuth flow with a strict domain check: the GoogleAuthContext validates that the signed-in email ends with @andrew.cmu.edu or @cmu.edu and automatically signs out non-CMU users.
Universify is developed as a ScottyLabs Labrador project.
Working:
- Expo client with full tab navigation (Home, Calendar, Find, Create, Profile) and a full event detail page (
/event/:id) - Supabase-backed event CRUD with Row Level Security, including per-user RSVP persistence (
event_rsvpstable with trigger-maintained aggregates) - Google OAuth with CMU domain enforcement
- Google Calendar bidirectional sync (the UniversifyβGoogle event mapping survives reloads)
- Slack bot with real-time Socket Mode, REST API, Supabase persistence, and an optional Approve/Reject review channel
- Discord bot with announcement parsing, review workflow, and Supabase submission
- Cross-source duplicate detection in both bots and the client (title similarity + time window; optional LLM tie-break via OpenRouter in the Slack bot)
- Recommendation engine wired into the product: ranked Home feed (interest profile mined from scheduled/RSVP'd/created events + explicit category preferences + popularity) and time-range suggestions when drag-selecting in the calendar
- Recurring events (daily/weekly/monthly with interval and end date) rendered as occurrences in the calendar
- Event flyer images (URL-based) shown on cards and detail views
- Week-view calendar with overlap-aware column layout and drag-to-select
- Multi-axis event filtering (categories, event types, date range, time of day, location, availability) and search (3 modes)
- Full theming: light/dark/system, font-size scaling, high-contrast mode, and reduced-motion support applied across the app
- Event reminders via browser notifications ~30 minutes before scheduled events (web, opt-in preference)
- Unit tests (
pnpm test), Playwright E2E suite, and GitHub Actions CI (typecheck, lint, tests, bundle check) - Responsive web layout with desktop navigation
Not implemented (requires external infrastructure):
- Email notification delivery β the preference toggle exists, but sending requires an email provider integration
- Native push notifications β requires an EAS build and a push service; reminders are web-only today
- Instagram event scraping
Developers: Haresh Muralidharan, Sam Mathew, Tiffany Ahn, Allan Fang Project Manager: Geethika Gupta Designers: Russell Sang, Camille Bove
Built at Carnegie Mellon University under ScottyLabs.