Skip to content
MobileFull-Stack

OPIC - Nightlife App

OPIC is a nightlife platform: a consumer mobile app for discovering events, offers, and booking tables, paired with a Next.js venue dashboard where venues run their own events, ticket tiers, perks, table layouts, and staff. This build is a rebuild of an already-live predecessor system, migrating its real production data as it goes.

The Problem

Venues had no self-serve way to run events, price ticket tiers, manage table layouts, or track staff and offers — v1 had to work as a content and operations tool with a dormant commerce engine underneath, so venues could build out real inventory before any consumer-facing app or traffic existed. On the consumer side, that inventory already existed with no way for guests to discover, book, or buy against it — the mobile app exists to give guests a front door to it.

My Role

Lead full-stack developer on a two-person team. I own the data layer, the shared API contract, and the web server implementation end to end — schema → contract → procedures → real data — plus the venue dashboard; a second developer builds the mobile app's screens and design system against that same typed contract.

What I Built

  • The Drizzle schema and Postgres domain model for venues, events, ticket tiers, table bookings, orders/tickets/scans, offers/vouchers/coupons, followers, and settlement
  • A typed oRPC contract layer shared by both the web dashboard and the mobile app, so both consume one typed API with no REST/OpenAPI hop for the app itself
  • The venue-facing Next.js dashboard: event CRUD with publish states, multi-tier ticketing, table-layout management, staff roles, and a public venue page
  • Stripe-based ticket checkout, moving from the predecessor's merchant-of-record model toward Stripe Connect destination charges with mandatory venue verification before any priced sale
  • A legacy-data migration importer that moved the predecessor system's real production data into the new schema

Architecture

  1. 1

    Expo Router + Next.js

    Mobile app and venue dashboard

  2. 2

    oRPC contract

    One typed API, shared by both apps

  3. 3

    Better Auth

    Venues as orgs, staff as members

  4. 4

    Drizzle ORM + PostgreSQL

    Venues, events, bookings, tickets

  5. 5

    Stripe Connect

    Ticket payments, venue payouts

A Turborepo monorepo: apps/mobile (Expo Router, React Native, NativeWind) and apps/web (Next.js 16 with Drizzle + Postgres directly, no CMS), sharing packages/contracts (a Zod-typed oRPC contract with no DB access) and packages/db. apps/web/server is the only place holding a database handle and implements the shared contract; both apps call the same procedures through typed clients. Auth is Better Auth's organization plugin, mapping venues to orgs and staff to org members.

Engineering Challenge

A partial-unique-index approach to prevent double-booking a table broke in two ways once the schema grew: two events on the same table on the same night could still collide, and a booking with no event attached never registers as a collision under a standard unique index, silently offering zero protection. Nightlife also runs across midnight — an event opening Friday 11pm and closing Saturday 5am has to group as 'Friday' everywhere, but its clock timestamps straddle two calendar days. A background job for expiring stale ticket holds also turned out to cost roughly 87,600 invocations a month against a 50k free-tier cap, on a schedule with no per-run reason to fire that often.

Solution

The booking-conflict constraint is now keyed on (table, business date) instead of (table, event), with business date stored explicitly on every relevant row rather than derived from timestamps — so the midnight-crossing problem and the double-booking bug get fixed by the same change. Money movement was deliberately kept out of a hand-rolled ledger: rather than maintain a second set of numbers that could drift from Stripe's own records, settlement is tracked in a table keyed directly to Stripe objects, with Connect verification now mandatory before a venue can take a priced sale. The hold-expiry job was moved from a recurring cron to event-driven expiry.

Outcome

Migrated real production data from the predecessor system into the new schema — 114 venues, 908 orders, 1,054 tickets, and 514 door scans — while the web dashboard shipped and moved into internal use.