/work · LLC · Passion project
Fully functional · seeded data · never launched
CommuniFit
A passion project carried to completion: a full neighborhood fitness platform with auth, a database, realtime notifications, transactional email, scheduled jobs and an admin suite.
Live site · 1
01 · Context
“Neighbors Moving Together”: free, local, membership-free group fitness. Neighbors browse nearby activities, RSVP in a tap, or host their own sessions without needing a certification. It is a passion project, it works end to end, and it was never launched. The application at communifit.app is complete and fully functional, and everything inside it is mock: the accounts, the hosts, the events, all seeded. There are no clients and no users, because I never went looking for any. No recruiting of hosts, no neighborhood outreach, no marketing.
02 · What I built
The content is seeded, but this is not a front end over mock data. The full stack, shipped:
- Supabase Postgres behind 40 SQL migrations: schema, indexes, row-level security, views, triggers, status lookup tables, and slug generation
- Supabase Auth with both password and magic-link sign-in, session refresh in middleware, password reset, and full account deletion
- 39 Next.js route handlers covering RSVPs, messaging, reports, feedback, admin actions, account deletion, and every transactional email
- 14 Resend email flows: welcome, RSVP confirmation and cancellation, waitlist confirmation and promotion, new event, event updated, event canceled, message-attendees, contact, and admin reports
- In-app notifications delivered live over Supabase Realtime: the navbar badge and notifications page subscribe to Postgres changes
- A daily Vercel cron that sends event reminders
- Capacity and waitlists: events carry a participant cap, and RSVPs past the cap become waitlist entries that promote automatically when someone drops
- Trust and safety as first-class features: user reporting, host verification, and user blocking
- A full admin suite: activities, activity types, events, hosts, neighborhoods, users, plus moderation queues for reports and feedback
- Public directories for activities, neighborhoods, and hosts; a calendar view; favorites; member and host dashboards; and a four-step event creation flow
03 · The senior-engineer part
Two decisions are worth pulling out, both the kind that only look obvious after something has gone wrong once:
- Notifications live in database triggers, not in application code. Migration 035 moved every notification (new user, new host, new event, event canceled, event details changed, RSVP, new follower) into Postgres triggers so they fire atomically with the data change, regardless of which code path caused it. An API route, an admin action, and a SQL fix all produce the same notifications, because none of them are responsible for producing notifications.
- Capacity is enforced by a trigger that reroutes rather than rejects. When an RSVP would exceed an event's cap, the database does not throw an error for the UI to interpret: it writes the RSVP as a waitlist entry instead. The invariant holds no matter what calls it, and the race between two people tapping “going” on the last spot resolves in the one place that can actually resolve it.
- Row-level security carries the authorization model, so a missed check in a route handler cannot leak another neighbor's data.
- Status values live in lookup tables rather than string columns, which is why 40 migrations could reshape the domain (dropping locations, renaming court sports to team sports, simplifying the schema) without a rewrite each time.
The second one, in the source. It is short, which is the point: the whole capacity rule is thirty lines in the only place that can win a race.
-- Trigger to enforce max_participants on RSVPs.
-- When an RSVP is inserted or updated to "going", check capacity.
-- If full, auto-assign to waitlist instead of rejecting.
CREATE OR REPLACE FUNCTION enforce_max_participants()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
[excerpt: local declarations, the lookup of the "going" status, and the
early return for events with no cap]
-- Count current going RSVPs (excluding this user in case of update)
SELECT COUNT(*) INTO v_going_count
FROM rsvps r
JOIN rsvp_statuses rs ON r.rsvp_status_id = rs.id
WHERE r.event_id = NEW.event_id
AND rs.slug = 'going'
AND r.user_id != NEW.user_id;
IF v_going_count >= v_max THEN
-- Auto-assign to waitlist
SELECT id INTO v_waitlist_slug_id FROM rsvp_statuses WHERE slug = 'waitlist';
NEW.rsvp_status_id := v_waitlist_slug_id;
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_enforce_max_participants ON rsvps;
CREATE TRIGGER trg_enforce_max_participants
BEFORE INSERT OR UPDATE ON rsvps
FOR EACH ROW
EXECUTE FUNCTION enforce_max_participants();04 · How AI was used
Built the way everything else here is built: an 11-document specification package in the repo (vision and values, roles and guidelines, activities, features and user flows, the data model, the tech stack, growth, a gap analysis, a backlog, a frontend map, and a phase plan), plus a CLAUDE.md, with agents executing against those documents.
The interesting evidence is the migration sequence. Forty numbered migrations that drop tables, rename domain concepts, and simplify the schema are the fingerprint of a build that changed its mind repeatedly and carried the database along properly each time, which is exactly the work that gets skipped when a project is a demo rather than a system.