Back to work

4Korners Member Portal

A production membership platform for a Montreal non-profit: sign-up and login, Stripe memberships, donations, an activity catalogue, and an admin back office. I built and operate it on my own, from the database to the deploy pipeline.

Architecture

MembersNext.js portalPostgres
Payments via Stripe and Zeffy. CRM synced from Airtable through a daily read-cache.

A Next.js app backed by Postgres with Drizzle. It syncs with the organization's Airtable CRM through a daily-refreshed read-cache, takes payments through Stripe and donations through Zeffy, and gates every admin screen behind role-based access control.

Key decisions

A SQL read-cache instead of live Airtable calls

Airtable's API rate limit could not serve the portal directly. I mirror the slow-changing CRM data into Postgres once a day and read from there, so member-facing pages stay fast and never hit the limit.

Derived membership state, computed not stored

Whether a member is active, expired, or in grace is computed from payments on read, not stored as a flag. One source of truth, and no stale status to reconcile.

The hard part

Email scanners were burning one-time links

Corporate email security scanners prefetch links to check them. They were opening the confirmation link before the user did and consuming the one-time token, so real sign-ups failed. The fix was to stop treating a bare GET as confirmation and require an explicit user action.

Security and performance

  • Admin two-factor authentication (TOTP with backup codes) gates every screen that shows personal data.
  • Login and email endpoints are rate-limited, and production server actions are locked to their origin.
  • Loi 25 compliance: consents are captured at sign-up, and an erasure request is honored across both the portal and the CRM.

Stack

Next.js 15TypeScriptPostgreSQLDrizzle ORMAuth.js v5StripeZeffyDockerGitHub Actions