ClubRide
A multi-tenant platform for cycling clubs where members never install anything. They text
IN to join a ride, ROLLING when they leave, DONE when they're back. Leaders get a web
dashboard. First tenant is Toledo Area Bicyclists.
The constraint drives everything: the median club member is not going to download an app for six group rides a year, and a platform that requires one is a platform with no members.
Multi-tenancy you cannot forget to apply
The usual way to scope a multi-tenant app is WHERE org_id = ? on every query, enforced by
discipline. Discipline fails once and leaks another club's roster.
Instead the guard hooks SQLAlchemy's do_orm_execute and injects
with_loader_criteria(OrgScoped, cls.org_id == org_id) into every select, update, and delete
that touches a scoped model. Two details make it real:
org = current_org.get()
if org is None:
raise RuntimeError(
"Attempted a query with no current_org context. "
"Use `with unscoped():` for cross-tenant operations."
)
A query with no tenant context raises rather than quietly returning every row — the
failure mode is a 500 in development, not a data leak in production. And cross-tenant work
has to say so out loud with with unscoped():, which turns "I forgot" into something that
shows up in review. A before_flush listener stamps org_id onto new rows so inserts can't
miss it either.
There's a scar in that file worth keeping: the org UUID is unwrapped from the ORM object before being closed over, because SQLAlchemy's lambda tracker recurses on ORM instances.
SMS is a stateless protocol
Tenant resolution happens two different ways. Normal requests resolve by subdomain. Twilio
webhooks resolve by the To number in the form body — which forces reading
await request.body() rather than request.form(), so the raw stream survives for the route
handler downstream.
Text messages have no session, so disambiguation is bolted on with a ten-minute Redis key.
Text IN with two rides today and you get a numbered menu; a bare 2 in reply is routed
back to the pending choice and completes the check-in.
Two details I'd defend in review
Safety-alert dedup lives in Postgres, not application logic:
stmt = (pg_insert(RideSafetyAlert)
.values(org_id=org.id, ride_id=ride.id, user_id=user.id,
alert_type="overdue_rolling")
.on_conflict_do_nothing(constraint="uq_safety_alert"))
result = await db.execute(stmt)
if result.rowcount == 0:
return # already alerted — conflict on uq_safety_alert
A cron that runs every five minutes will double-fire eventually. A unique constraint is the only version of "only alert once" that survives two workers racing.
And STOP sends the unsubscribe confirmation before recording the opt-out — commented
# TCPA: — because writing the flag first would cause the opt-out to suppress the
legally-required confirmation of the opt-out.
5,934 lines of Python, 6,192 of TypeScript, 51 endpoints, 105 tests. Most of it landed in 48 hours.