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

Nothing yet. Add entries under one of the sections below as you land changes.

Added

Changed

Fixed

Taste signal & swipe storytelling

Added

  • Preference change sets. Completing a swipe stack now groups its swipes into an auditable preference_change_sets row 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_sets table + enums (preference_change_set_source, preference_change_set_status), plus user_preference_config.auto_apply_change_sets flag.
  • Swipe-card onboarding at /onboarding. Left = dislike, right = like, up = like (previously love — the love gesture 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.
  • /discover page 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 to events and deals, plus hero_image_url for 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, and onboarding_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.mjs cuts the current [Unreleased] block into a versioned heading with auto-detected bump (major if Removed/Breaking, minor if Added/Changed, else patch), rewrites a fresh empty [Unreleased] scaffold at the top, and syncs the version across every workspace package.json. Pair helpers: scripts/changelog-add.mjs appends a bullet to the right subsection, and scripts/changelog-check.mjs (wired into .githooks/pre-commit) prints a non-blocking reminder when a commit touches packages/** or infra/** without updating CHANGELOG.md. Enable the shared hook with npm 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_id UUID 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 in VIBE_CARDS; they now live in a new vibe_cards Postgres table with full admin CRUD at /admin/vibe-cards and a vibe_card_id FK from every stack card / swipe that references one. A one-shot backfill linked every pre- migration stack card and swipe by vibe_key so 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 the tagsSnapshot / intentsSnapshot arrays 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 publicId for 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.md previously listed raw tag counts under mechanical headers (### Love, ### Like, ### Not for me, ### Maybe). It now renders a narrative paragraph under a single ## Your vibe heading. 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:value convention (vibe:intimate, venue:rooftop, genre:jazz, activity:live-music, time:evening, etc.). Same-type tags cluster in the sentence and the vibe / venue / setting types 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 (see VIBE_CARDS in lib/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 ChangeSetSummary in, same markdown out — so change-set diffs remain stable across rebuilds. Implementation: renderSwipeBlock() in lib/preferences/swipe-block.ts.

  • 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/status now returns StackSummary objects (id, status, cardsTotal, cardsRemaining, weekOf) for the latest onboarding stack, the current-week stack, and the latest weekly stack, plus currentWeekOf / nextWeekOf ISO Mondays. The old hasActiveWeeklyStack boolean is gone. See deriveStackCta() in components/EventGoerHome.tsx for the CTA derivation logic.
    • Swiping and hitting "I'm done" both invalidate the home onboarding-status query, so the CTA stays accurate when you navigate back.
Event-goer first

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.
  • /events public listing page (filterable by org type) and /calendar personal calendar page with .ics subscribe.
  • Real DB-backed personal calendar:
    • event_rsvps table (one row per viewer × event, status interested / going / skipped).
    • event_calendar_plans table — replaces the previous in-memory prototype; supports both eventId and dealId plans.
    • 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.
  • /changelog page (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.
  • upsertOrgShadow accepts an optional type; /api/org/sync accepts 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).
Organizations, verification, API keys

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.
  • organizations shadow table with verification state, mirrored lazily from Neon Auth. FKs added to events, deals, locations.
  • Custom API key system (Neon Auth does not yet expose the Better Auth API Key plugin):
    • api_keys table stores only SHA-256 hashes; full key is returned to the UI exactly once.
    • Fine-grained scopes (events:write, deals:read, etc.) enforced centrally via authorizeOrgRequest.
  • 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/orgs with 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 a me debug 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.
First production cut

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

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.