Live · Actively maintained

Building a Better NYC Happy Hour Directory

This started with a problem my friends and I kept running into: finding a decent place for cheap food and drinks shouldn’t require digging through old Reddit threads and half-maintained spreadsheets. So I started building the directory I wanted to use myself.

Visit the live site →
1,000+ Confirmed happy hours
2,300+ Venues in the directory
4,000+ Visitors since launch
5 Boroughs covered
512 Application tests
Live Launched July 2026, maintained

Why I built it

The options I found usually had one of two problems: good information but very limited coverage, or lots of venues with data that was difficult to search and harder to keep current. Neither one helped when four people are standing on a corner trying to decide where to go.

The project really came down to three problems: getting the data right, making it easy to search, and building a way to keep it from going stale.

The product

NYCHappyHours.org now covers 1,000+ confirmed happy hours inside a directory of 2,300+ venues across all five boroughs: list and map views, live and upcoming status, last-call timing, location-aware sorting, saved spots, and venue-level deal details. None of it was designed once and shipped; it came out of dozens of iterations with real people using the site at every step.

The NYC Happy Hours homepage on desktop: the bar-lit banner, a search field, and filters showing 479 happy hours running in Manhattan.
The homepage on a weekday evening: search first, then the filters that matter at 6 p.m., with a live count of what’s running.
The homepage on a phone: search, filters, the live count, and the first venues. The list view in the light theme, with last-call countdowns and deal prices. The same list in the dark theme.
On a phone, where it’s meant to be used: the opening screen, then the list in both themes, sorted by ending soonest so last call leads each card.

The data problem

The backbone was never one clean source: it was years-old Reddit threads from people who’d tried this before, blog roundups, news posts, and the spots my friends and I had already been going to for years. The hard part was reconciling all of that into one structured dataset a site could run on.

My first instinct was a Google Sheet: editable by anyone, no infrastructure. But a happy hour can have different hours, different food specials, and different drink specials for every day of the week, across thousands of venues. Modeling that directly in a spreadsheet would have meant roughly 50 columns per venue, so I went the other way: a handful of columns holding a tightly structured schedule string, parsed later in PostgreSQL. The tradeoff was fragility. Adding a venue by hand meant one bad character could stop the schedule from parsing, and fixing that is what the admin system below was for.

Building it to outlast the launch

A directory like this usually dies the same way: someone builds it, stops maintaining it, and it goes stale. I didn’t want to build version three of that same failure, so the system for keeping it current went in before I called it an MVP. Two simple forms collect corrections and new-venue suggestions, and an admin console lets me review, edit, publish, or reject them.

I could have handled updates in code and skipped a real admin interface; for most side projects that’s the pragmatic call. Here it would’ve defeated the point: a code-only backend turns every correction into an engineering ticket instead of something an operator can fix from a phone in under a minute. The console is also what solved the fragile-entry problem from the data pipeline, since it validates and structures a submission before it ever touches the live dataset.

The console runs in the browser behind magic-link sign-in, with every read and write constrained by Postgres row-level security, so the client can never reach a row the policy forbids. Every change shows a field-level diff before it publishes, and every action is written to an append-only audit log. It’s covered by 512 node --test cases plus SQL row-level-security invariant tests, both run in CI.

The important part is that maintaining the site is an operations task now, not a coding task. Someone else maintaining it can publish a correction from their phone without touching the repository.

The public site is also built to stay up when the database doesn’t. Reads normally hit Supabase directly, but the app ships a generated per-borough snapshot of the venue data, and a dedicated checker continuously checks that snapshot against the live database. If Supabase is unreachable, the site serves the snapshot instead of an error page — slightly stale data is better than a broken page.

Runtime architecture diagram: the visitor's browser loads a static shell from Cloudflare; a data-source facade reads from Supabase and falls back to generated per-borough modules; the admin intake publishes to Supabase.
The runtime, from the project’s architecture notes. Every line of app code runs in the browser; a data-source facade reads Supabase first and falls back to the generated per-borough snapshot, so an outage costs freshness, not the page.

What changed once people used it

  • Venue cards, map mode, and filters became the three core ways into the data, rebuilt repeatedly after watching real users get stuck.
  • The same information (location, price, cuisine) got redundant access points: a feature only “exists” if it’s visible where someone’s actually looking, not just present somewhere on the page.
  • The layout is mobile-first, built for someone deciding where to go in the next twenty minutes, not browsing at a desk.
  • The correction loop — two intake forms, then publish or retire from the admin console — keeps the dataset from drifting out of date.

What I learned

The most useful lesson wasn’t technical: features that already existed didn’t count until people could find them where they were already looking, which is why the map, the cards, and the filters got rebuilt more than once. The rest was about making tradeoffs on purpose — structured strings instead of a fifty-column table, an operations console instead of a code-only backend — and then going back to pay for what those choices cost instead of pretending they were free.