The event type a poster picks in Create Event never reaches the database, and the profile's My Events chips filter on a different vocabulary entirely — so a user can't filter their saved events by the type they were posted as.
Current state
Two disconnected vocabularies:
- Create Event offers 8 types in
app/components/create-event/EventDetails.tsx: general_meeting, social, career, workshop, performance, fundraiser, sports, other. app/components/create-event/OptionalExtras.tsx:57 appends event_type to the FormData.
- **The server drops it. **
server/src/routes/events.worker.ts never reads event_type, and there is no event_type column in server/schema.sql or any migration. The creator's choice goes nowhere.
- The profile chips (
shared/profileEventFilters.ts) are All / General / Academic / Social, each mapping to a set of taxonomy bucket ids and filtered with EXISTS (SELECT 1 FROM event_tags …) in server/src/routes/users.worker.ts:713 and server/src/routes/orgs.worker.ts:981.
Approach
The creator's pick is authoritative when present; the classifier's bucket covers everything else. Scraped events never go through the create flow, so the fallback is the majority path, not an edge case.
Build steps
- **Migration **
server/migrations/0020_add_event_type.sql:
ALTER TABLE events ADD COLUMN event_type TEXT;
CREATE INDEX IF NOT EXISTS idx_events_event_type ON events (event_type);
Mirror the change into server/schema.sql or test_schema_migration_parity.ts will fail.
- Shared module — move the 8 event type ids out of
EventDetails.tsx into shared/ (alongside profileEventFilters.ts) so the client picker, the server validator, and the filter mapping all read one list. Add two maps: event type id → chip, and taxonomy bucket id → chip.
- Accept it on write —
POST /events and PATCH /events/:id in events.worker.ts read event_type, validate against the shared list, reject unknown values, and persist. Unset stays valid.
- Filter SQL — in
users.worker.ts and orgs.worker.ts, prefer the column and fall back to tags:
AND (
(e.event_type IS NOT NULL AND e.event_type IN (?, ?, …))
OR (e.event_type IS NULL AND EXISTS (
SELECT 1 FROM event_tags t
WHERE t.event_id = e.id AND t.bucket_id IN (?, ?, …)
))
)
- Chips render from the shared list in
app/(tabs)/profile.tsx. app/components/org/OrgEventsTab.tsx uses the same chip row and picks the change up for free — verify both.
Notes
- **Fix the phantom buckets while in here. **
ACADEMIC_BUCKETS in shared/profileEventFilters.ts is ['science', 'education', 'tech', 'business'], but the taxonomy's 12 real ids are arts, education, food, gaming, nightlife, outdoors, performing, social, spirituality, sports, tech, travel. science and business do not exist, so the Academic chip has silently been filtering on education + tech only. general is the complement of the claimed set, so the two phantoms are otherwise harmless.
- **Known skew in the fallback. **
classifyEvent in server/src/lib/classifier.ts guarantees at least one tag by falling back to social / Meetups & Mixers, which maps to the Social chip. Scraped events with thin descriptions will pile into Social. Worth a count by source before shipping — if one scraper dominates, its descriptions are the problem, not the mapping.
- Design input needed on the chip set. The row currently holds 4 chips; the create list has 8. Confirm with design whether all 8 become chips, or whether they group into a shorter row the way the current 3 do.
- Existing events all have
event_type IS NULL, so they keep their current tag-based behaviour with no backfill. A backfill from event_tags is possible later but is not needed for this to ship.
- Splitting this into a backend ticket (steps 1–4) and a frontend one (step 5) is reasonable if it's easier to parallelise.
The event type a poster picks in Create Event never reaches the database, and the profile's My Events chips filter on a different vocabulary entirely — so a user can't filter their saved events by the type they were posted as.
Current state
Two disconnected vocabularies:
app/components/create-event/EventDetails.tsx:general_meeting,social,career,workshop,performance,fundraiser,sports,other.app/components/create-event/OptionalExtras.tsx:57appendsevent_typeto the FormData.server/src/routes/events.worker.tsnever readsevent_type, and there is noevent_typecolumn inserver/schema.sqlor any migration. The creator's choice goes nowhere.shared/profileEventFilters.ts) are All / General / Academic / Social, each mapping to a set of taxonomy bucket ids and filtered withEXISTS (SELECT 1 FROM event_tags …)inserver/src/routes/users.worker.ts:713andserver/src/routes/orgs.worker.ts:981.Approach
The creator's pick is authoritative when present; the classifier's bucket covers everything else. Scraped events never go through the create flow, so the fallback is the majority path, not an edge case.
Build steps
server/migrations/0020_add_event_type.sql:Mirror the change into
server/schema.sqlortest_schema_migration_parity.tswill fail.EventDetails.tsxintoshared/(alongsideprofileEventFilters.ts) so the client picker, the server validator, and the filter mapping all read one list. Add two maps: event type id → chip, and taxonomy bucket id → chip.POST /eventsandPATCH /events/:idinevents.worker.tsreadevent_type, validate against the shared list, reject unknown values, and persist. Unset stays valid.users.worker.tsandorgs.worker.ts, prefer the column and fall back to tags:app/(tabs)/profile.tsx.app/components/org/OrgEventsTab.tsxuses the same chip row and picks the change up for free — verify both.Notes
ACADEMIC_BUCKETSinshared/profileEventFilters.tsis['science', 'education', 'tech', 'business'], but the taxonomy's 12 real ids arearts, education, food, gaming, nightlife, outdoors, performing, social, spirituality, sports, tech, travel.scienceandbusinessdo not exist, so the Academic chip has silently been filtering oneducation+techonly.generalis the complement of the claimed set, so the two phantoms are otherwise harmless.classifyEventinserver/src/lib/classifier.tsguarantees at least one tag by falling back tosocial / Meetups & Mixers, which maps to the Social chip. Scraped events with thin descriptions will pile into Social. Worth a count by source before shipping — if one scraper dominates, its descriptions are the problem, not the mapping.event_type IS NULL, so they keep their current tag-based behaviour with no backfill. A backfill fromevent_tagsis possible later but is not needed for this to ship.