Nothing yet. Add entries under one of the sections below as you land changes.
Product
Changelog
Every meaningful change we ship — new features, updates, fixes, and the occasional removal. Source of truth lives in CHANGELOG.md at the repo root.
All notable changes to Early Birdy are captured here. The format follows Keep a Changelog and this project adheres to loose Semantic Versioning for user-visible releases.
New entries go at the top. Dates are YYYY-MM-DD in the project's local
tz. Internal-only refactors and typo fixes don't need an entry.
Authoring entries
Use one of the sections: Added, Changed, Deprecated, Removed, Fixed, Security.
Lead with the user-visible benefit, then (when useful) the implementation hook so future-you can find the code quickly.
Add an entry with either approach:
# quick one-liner (auto-creates the section if missing) npm run changelog:add -- Added "Admin export button on /admin/analytics." # or edit [Unreleased] directly in this file for longer narrative entries
Cutting a release
scripts/release.mjs rolls the current [Unreleased] block into a
versioned heading, inserts a fresh empty [Unreleased] scaffold, and
syncs the version across every workspace package.json:
# preview what would happen — writes nothing
npm run release:dry -- --title "Short release name"
# auto-detected bump (most common)
npm run release -- --title "Short release name"
# explicit bump
npm run release:minor -- --title "Short release name"
Bump auto-detection (looks at subsection headings in [Unreleased]):
| Sections present | Default bump |
|---|---|
### Removed or ### Breaking |
major |
### Added or ### Changed |
minor |
### Fixed / ### Security / ### Deprecated only |
patch |
Override with --bump patch|minor|major. The script prints the exact
git add / commit / tag commands to run — tagging and pushing stay
manual on purpose.
Pre-commit reminder
scripts/changelog-check.mjs (wired into .githooks/pre-commit) prints
a non-blocking nudge when a commit touches packages/** or infra/**
without also updating CHANGELOG.md. Enable shared hooks once per
clone:
npm run setup:hooks
Added
- Preference change sets. Completing a swipe stack now groups its swipes
into an auditable
preference_change_setsrow that auto-applies to your preferences markdown — so every deck you swipe tightens the document we use for matching, visibly. Each change set keeps the before/after markdown snapshot plus a structured summary (top tags/intents by score) so you can roll one back at any time.- New page:
/preferences/changes— timeline of change sets with per-set apply / roll-back / discard actions and a "rebuild from all swipes" button. - The swipe-managed block lives inside your preferences markdown between
<!-- swipes:start -->and<!-- swipes:end -->markers, so your own prose above/below is never rewritten. - New APIs:
GET/POST /api/me/preferences/change-sets,POST /api/me/preferences/change-sets/[id]/apply,POST /api/me/preferences/change-sets/[id]/discard,GET/PUT /api/me/preferences/settings(auto-apply toggle). - New schema:
preference_change_setstable + enums (preference_change_set_source,preference_change_set_status), plususer_preference_config.auto_apply_change_setsflag.
- New page:
- Swipe-card onboarding at
/onboarding. Left = dislike, right = like, up = like (previously love — thelovegesture is now derived, not surfaced), down = later. Each swipe records tag + intent signal and (for event cards) mirrors into an RSVP automatically. - Polymorphic card model: a swipe card can wrap a real event, a real deal, or an abstract "vibe" fallback — all share the same render path, tags, intents, and imagery fields.
/discoverpage surfacing a fresh weekly stack of 10 upcoming events, lazily generated per ISO week and deterministically shuffled per user so heartbeats / crons can be idempotent later.- Tags + intents (
text[]) added toeventsanddeals, plushero_image_urlfor card media. Tags describe the what (live-jazz,walkable); intents describe the why (date-night,family-friendly). - Three new tables:
onboarding_stacks,onboarding_stack_cards, andonboarding_swipes. Snapshotted tag/intent arrays live on the swipe row so preference aggregation never needs a join back to the source event. - New APIs:
GET /api/onboarding/stack?kind=onboarding|weekly— fetch or lazily generate the active deck.POST /api/onboarding/swipe— record a swipe, mirror RSVP, auto-complete the stack on the last card.POST /api/onboarding/stack/[id]/complete— "I'm done" escape hatch.GET /api/onboarding/status— read-only lifecycle snapshot (no side-effect stack creation).GET /api/me/preferences/tags— aggregated tag + intent scores.
- Event-goer home now shows an onboarding banner until the user has
completed their first stack, then replaces it with a This week's picks
tile linking to
/discover. - Release automation. New
scripts/release.mjscuts the current[Unreleased]block into a versioned heading with auto-detected bump (major ifRemoved/Breaking, minor ifAdded/Changed, else patch), rewrites a fresh empty[Unreleased]scaffold at the top, and syncs the version across every workspacepackage.json. Pair helpers:scripts/changelog-add.mjsappends a bullet to the right subsection, andscripts/changelog-check.mjs(wired into.githooks/pre-commit) prints a non-blocking reminder when a commit touchespackages/**orinfra/**without updatingCHANGELOG.md. Enable the shared hook withnpm run setup:hooks. Full workflow and bump-detection rules are documented at the top of this file. - Every swipe is now database-addressable. Onboarding stacks, stack cards,
and swipes all carry a stable
public_idUUID in addition to their internal serial ids, so any row in the system can be referenced externally for analytics, audit, or drill-down links. Vibe cards (the fallback taste prompts a deck falls back to when real events are thin) were previously hard-coded inVIBE_CARDS; they now live in a newvibe_cardsPostgres table with full admin CRUD at/admin/vibe-cardsand avibe_card_idFK from every stack card / swipe that references one. A one-shot backfill linked every pre- migration stack card and swipe byvibe_keyso historical analytics stay whole. - Admin analytics dashboard at
/admin/analytics. Four sections, all window-filterable (7d / 30d / 90d / 1y):- Overview KPIs — users swiping, swipes, stacks created/completed/active, completion rate, avg swipes per user, avg stack size, and the love / like / later / dislike breakdown as a single stacked bar.
- Taste signal — top and bottom tags + intents, ranked by a net score
(
love × 2 + like − dislike) with per-action breakdowns pulled straight from thetagsSnapshot/intentsSnapshotarrays on each swipe. - Content performance — per-event / per-deal / per-vibe appearance count,
swipe count, like rate, and action histogram, sorted by volume then taste.
Each row shows its stable
publicIdfor deep-linking. - Trends — daily and weekly series of swipes, stacks started, and stacks completed as a stacked bar chart.
- New lib modules:
lib/admin/analytics.ts,lib/admin/vibe-cards.ts. - New endpoints under
/api/admin/analytics/*(overview/taste/content/trends) plus/api/admin/vibe-cards[ /[publicId]].
- Admin sub-navigation. Every
/admin/*page now shows a compact section bar linking to Organizations, Vibe cards, Analytics, and Feedback so admins aren't URL-guessing anymore.
Changed
Preferences document now reads like a short profile of you. The swipe-managed block inside
PREFERENCES.mdpreviously listed raw tag counts under mechanical headers (### Love,### Like,### Not for me,### Maybe). It now renders a narrative paragraph under a single## Your vibeheading. Intensity is implied by phrasing ("you keep coming back to" vs "you also enjoy"), so the word "love" never appears in user-facing copy even though the ≥2 threshold still drives which tags get the strong treatment. The renderer supports two features beyond bucket-to-prose translation:- Typed tags. Tag keys now use a
type:valueconvention (vibe:intimate,venue:rooftop,genre:jazz,activity:live-music,time:evening, etc.). Same-type tags cluster in the sentence and thevibe/venue/settingtypes get a short noun suffix when they have 2+ members — e.g. "intimate and cozy vibes, and rooftop spots" — so the phrase reads as coherent noun groups instead of a flat list. All 10 seed vibe cards were retyped accordingly (seeVIBE_CARDSinlib/onboarding/types.ts); legacy untyped tags on historical swipes still render correctly as a catch-all group. - Intent-led narratives. When the top intent has a strong score
(≥
INTENT_LEAD_THRESHOLD= 3), the paragraph opens with the intent — "Your go-to is date night, with hints of slow down. That usually looks like intimate and cozy vibes, and rooftop." — instead of relegating it to the trailing sentence. Below the threshold the prior structure is preserved (positive tags lead, intent sentence tails).
Output stays fully deterministic — same
ChangeSetSummaryin, same markdown out — so change-set diffs remain stable across rebuilds. Implementation:renderSwipeBlock()inlib/preferences/swipe-block.ts.- Typed tags. Tag keys now use a
Home page CTA now tracks your real stack state. The tile on the logged-in home previously said "Your weekly stack is waiting" even after you'd swiped through every card. It now reflects the cached state of your suggestions deck and renders one of five targeted variants:
- Onboarding start — never swiped before.
- Onboarding resume (N cards to go) — partial onboarding stack.
- Weekly resume (N cards left) — mid-week deck in progress.
- Weekly start — no current-week stack yet.
- Weekly done — current-week stack complete, with the date the next stack lands.
- Implementation:
GET /api/onboarding/statusnow returnsStackSummaryobjects (id,status,cardsTotal,cardsRemaining,weekOf) for the latest onboarding stack, the current-week stack, and the latest weekly stack, pluscurrentWeekOf/nextWeekOfISO Mondays. The oldhasActiveWeeklyStackboolean is gone. SeederiveStackCta()incomponents/EventGoerHome.tsxfor the CTA derivation logic. - Swiping and hitting "I'm done" both invalidate the home
onboarding-statusquery, so the CTA stays accurate when you navigate back.
Added
- Event-goer home is now the primary logged-in experience (
/). Leads with a suggested-events feed and a horizontal Upcoming calendar strip. Primary CTAs on every event are I'm going, Interested, and + Calendar.- New components:
EventGoerHome,EventCard,UpcomingCalendarStrip.
- New components:
/eventspublic listing page (filterable by org type) and/calendarpersonal calendar page with.icssubscribe.- Real DB-backed personal calendar:
event_rsvpstable (one row per viewer × event, statusinterested/going/skipped).event_calendar_planstable — replaces the previous in-memory prototype; supports botheventIdanddealIdplans.- RSVP auto-mirrors into the calendar on
going/interested.
- Organization type catalog (
venue,restaurant,bar,retail,brand,nonprofit,service,other). Surfaced in the org console header, admin verification list, and as filter chips on the event feed. - Hybrid feed ranking: boosts events from orgs the viewer has previously RSVPd, excludes events they skipped, and falls back to sorted upcoming.
- New APIs:
GET /api/me/feed— authenticated feed with viewer state.GET /api/events/upcoming— public listing.POST /api/events/[id]/rsvp/DELETE— RSVP upsert + clear.GET/POST /api/me/calendar+DELETE /api/me/calendar/[id].GET /api/me/calendar/ics— real ICS feed per signed-in user.
/changelogpage (this file, rendered statically).
Changed
- Header nav is split by role:
- Signed out: Sign in / Sign up.
- Signed in: Events / My calendar / Preferences, with the org switcher + user button kept as utility controls.
upsertOrgShadowaccepts an optionaltype;/api/org/syncaccepts and validates the same.- Admin org list gained a Type column and badge.
Removed
- Top-level Event designer link from the marketing header (the
sandbox still lives at
/event-designer, just not in primary nav).
Added
- Organization-based auth built on Neon Auth / Better Auth's Organization plugin. Users can self-serve create orgs; unverified orgs can create content that stays hidden until an admin verifies the org.
organizationsshadow table with verification state, mirrored lazily from Neon Auth. FKs added toevents,deals,locations.- Custom API key system (Neon Auth does not yet expose the Better Auth
API Key plugin):
api_keystable stores only SHA-256 hashes; full key is returned to the UI exactly once.- Fine-grained scopes (
events:write,deals:read, etc.) enforced centrally viaauthorizeOrgRequest.
- Org console at
/org/*: dashboard, events / deals / locations CRUD, members, API keys, and settings. Pending-verification banner stays visible until admin approves. - Admin UI
/admin/orgswith verify / unverify actions; verification bulk-publishes pending org-owned content. - Public
/api/v1/*routes gated by Bearer tokens with scope checks (events, deals, locations, and amedebug endpoint). - Session-gated
/api/org/*routes powering the console. - Organization switcher in the header.
Security
- Public event/deal queries now apply a visibility predicate
(
lib/visibility.ts) so unverified-org content never leaks into listings or OG images.
Added
- Taste-stack UI for the preference swipe experience.
- Neon Auth sign-in / sign-up flow wired through the marketing landing.
- Interactive discover map powered by MapLibre.
- Open Graph share images for events.
- Deployed to production on
earlybirdy.org.
Fixed
- Signup flow edge cases when a member email was already subscribed to the Beehiiv newsletter list.
Added
- Project scaffolding: SST infra, Next.js web app, Hono API prototype, Drizzle schema, seed data for LA-area cohorts and locations.
- Prototype landing with newsletter signup + members-area discover map.
