Metro Manila Event Discovery Platform β Find parties, thrift markets, live music, food events, and more on an interactive map.
Production: www.wheresthefx.com | API: api.wheresthefx.com
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β blead (VPS) β
β Playwright + Gemini AI β SQLite (ig-events.db) β
β Scrapes ~157 Instagram accounts daily β
βββββββββββββββββββ¬ββββββββββββββββββββββββββββββββ
β ingest script (every 6hrs)
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β WheresTheFX Server β
β Express.js + Drizzle ORM + PostgreSQL β
β REST API: /api/events, /api/venues, etc. β
β Image proxy for Instagram CDN fallback β
β JWT auth β
β Port: 3001 β
βββββββββββββββββββ¬ββββββββββββββββββββββββββββββββ
β fetch API
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β WheresTheFX Client β
β React + Vite + TypeScript + Tailwind β
β Leaflet dark-themed map β
β Category filters, search, date/price filters β
β Math verification landing page β
β Port: 5173 β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
- Node.js 18+
- PostgreSQL (OrbStack or local)
- blead repo at
../blead(for ingest)
# 1. Create PostgreSQL database
psql -c "CREATE DATABASE wheresthefx;"
# 2. Install server deps + push schema
cd server
npm install
./node_modules/.bin/drizzle-kit push
# 3. Run ingest from blead
BLEAD_DB_PATH=../blead/data/ig-events.db \
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/wheresthefx \
npx tsx src/scripts/ingest-from-blead.ts
# 4. Start server
npx tsx src/index.ts
# 5. Install frontend deps + start
cd ..
npm install
npm run devCopy .env.example to .env and configure:
| Variable | Description | Default |
|---|---|---|
DATABASE_URL |
PostgreSQL connection string | postgresql://postgres:postgres@localhost:5432/wheresthefx |
JWT_SECRET |
Secret for JWT token signing | your-secret-key-change-in-production |
BLEAD_DB_PATH |
Path to blead's SQLite database | ../blead/data/ig-events.db |
BLEAD_IMAGE_DIR |
Path to blead's cached images | ../blead/data/ig-images |
PORT |
Server port | 3001 |
CORS_ORIGIN |
Allowed CORS origin | http://localhost:5173 |
VITE_API_URL |
API base URL (frontend) | http://localhost:3001 |
wheresthefx/
βββ docs/ # Documentation
β βββ CONVENTIONS.md # Code/database/API conventions
β βββ api.md # API reference
β βββ schema.md # Database schema reference
β βββ ingest.md # Ingest pipeline docs
βββ server/ # Express.js backend
β βββ src/
β β βββ db/
β β β βββ schema.ts # Drizzle ORM schema (9 tables)
β β β βββ connection.ts # PostgreSQL connection
β β β βββ migrate.ts # Migration runner
β β βββ middleware/
β β β βββ auth.ts # JWT auth middleware
β β β βββ snakeCase.ts # camelCase β snake_case response transform
β β βββ routes/
β β β βββ auth.ts # Register, login, me
β β β βββ events.ts # Event CRUD + map/upcoming
β β β βββ venues.ts # Venue directory
β β β βββ categories.ts # Category metadata
β β β βββ users.ts # Saved events, preferences
β β βββ scripts/
β β β βββ ingest-from-blead.ts # SQLite β PG sync
β β β βββ geocode-venues.ts # Venue geocoding
β β βββ index.ts # Express server entry
β βββ drizzle.config.ts
β βββ package.json
βββ src/ # React frontend (Vite)
β βββ api/
β β βββ client.ts # API client (snake_case types)
β βββ components/ # React components
β β βββ EventMap.tsx # Leaflet map
β β βββ EventSidebar.tsx # Sidebar with nearby events
β β βββ EventPopup.tsx # Mobile event popup
β β βββ EventSidePanel.tsx # Desktop event panel
β β βββ MapFilters.tsx # Search, date, price filters
β β βββ CategoryFilter.tsx # Category chip filters
β β βββ ui/ # shadcn/ui components
β βββ hooks/ # React hooks
β β βββ useEventMarkers.ts # Fetch + cluster events
β β βββ useSavedEvents.ts # User's saved events
β β βββ useUserPreferences.ts
β βββ pages/
β β βββ Index.tsx # Landing + map view
β β βββ Auth.tsx # Login/register
β β βββ Admin.tsx # Admin dashboard
β βββ utils/
β βββ markerUtils.ts # Map marker clustering
βββ .agents/workflows/ # Agent workflow definitions
βββ .env.example
βββ package.json
blead is a separate system that scrapes Instagram daily:
- 157 accounts tracked (venues, promoters, organizers in Metro Manila)
- Playwright headless browser extracts posts
- Gemini AI classifies posts as events and extracts structured data
- Stores everything in SQLite at
data/ig-events.db
The ingest-from-blead.ts script syncs data:
- Venues β matched by
name(UNIQUE) - Accounts β matched by
username(UNIQUE) - Posts β matched by
shortcode(UNIQUE) - Events β matched by
event_hash(SHA256 of title+date+venue) - Sub-events β delete + re-insert per parent event
Run every 6 hours via cron on the VPS:
0 */6 * * * cd /path/to/wheresthefx/server && npx tsx src/scripts/ingest-from-blead.ts| Table | Count |
|---|---|
| Venues | 118 |
| Accounts | 157 |
| Posts | 1317 |
| Events | 230 |
| Sub-events | 178 |
| Geocoded | 194 |
All endpoints are prefixed with /api. All responses use snake_case keys.
See docs/api.md for full reference.
| Method | Path | Description | Auth |
|---|---|---|---|
POST |
/auth/register |
Create account | No |
POST |
/auth/login |
Login, get JWT | No |
GET |
/auth/me |
Current user | Yes |
| Method | Path | Description | Auth |
|---|---|---|---|
GET |
/events |
List events (paginated) | Optional |
GET |
/events/upcoming |
Future events | No |
GET |
/events/map |
Events with geocoding | No |
GET |
/events/:id |
Event detail | Optional |
Query params: category, search, is_free, date_from, date_to, page, limit
| Method | Path | Description | Auth |
|---|---|---|---|
GET |
/venues |
All venues | No |
GET |
/venues/:id |
Venue detail | No |
GET |
/venues/:id/events |
Events at venue | No |
| Method | Path | Description | Auth |
|---|---|---|---|
GET |
/categories |
Categories with counts | No |
| Method | Path | Description | Auth |
|---|---|---|---|
GET |
/users/me/saved |
Saved events | Yes |
POST |
/users/me/saved |
Toggle save event | Yes |
GET |
/users/me/preferences |
Get preferences | Yes |
PUT |
/users/me/preferences |
Update preferences | Yes |
| Method | Path | Description |
|---|---|---|
GET |
/images/proxy?url=...&shortcode=... |
Proxy Instagram images |
9 tables connected via foreign keys:
- venue β Canonical venue directory with geocoding
- source_account β Instagram accounts being tracked
- source_post β Raw IG posts with AI extraction data
- event β Core event data with
event_hashdedup - sub_event β Multi-act lineups and schedules
- account β Platform user accounts (bcrypt passwords)
- saved_event β User bookmarks (many-to-many)
- account_preference β Preferred categories
- sync_log β Ingest run tracking
Key design decisions:
- No dependency on blead's internal IDs
event_hash(SHA256) for deduplication- Denormalized
venue_name/source_usernamefor query performance - JSONB arrays for
artists - API responses auto-converted to snake_case by
snakeCaseResponsemiddleware
# Sync server code to VPS
rsync -avz --delete --exclude='node_modules' --exclude='.env' --exclude='drizzle' \
server/ root@217.216.72.28:/root/wheresthefx-api/
# Restart on VPS
ssh root@217.216.72.28 "pm2 restart wheresthefx-api"- API:
https://api.wheresthefx.com(port 3001 behind Caddy reverse proxy) - SSL: Let's Encrypt via Caddy
- Process manager: PM2
# Deploy to production
vercel --prod --yes- Frontend:
https://www.wheresthefx.com - Env:
VITE_API_URL=https://api.wheresthefx.com - DNS: Namecheap BasicDNS β Vercel
| Layer | Technology |
|---|---|
| Frontend | React 18, Vite, TypeScript, Tailwind CSS |
| UI Components | shadcn/ui (Radix primitives) |
| Map | Leaflet with custom markers |
| Backend | Express.js, TypeScript |
| ORM | Drizzle ORM |
| Database | PostgreSQL (OrbStack) |
| Auth | JWT (jsonwebtoken + bcryptjs) |
| Data Source | blead (Playwright + Gemini AI β SQLite) |