← Back to work

4Korners Member Portal

A production membership platform for a Montreal nonprofit. It handles signup and login, Stripe memberships, donations, an activity catalogue, and an admin back office. I built it and I run it on my own, from the database to the deploy pipeline.

Architecture

4Korners portal architectureA browser reaches the Next.js portal through a Caddy TLS proxy on a VPS. The portal reads and writes PostgreSQL, receives Stripe and Zeffy webhooks, writes to the Airtable CRM, and refreshes from it asynchronously.EXTERNAL SERVICESON THE VPSHTTPSREAD/WRITEWEBHOOKWRITESREFRESHStripeZeffyAirtableBrowserCaddyPortalPostgreSQLmembershipdonationsCRMTLSNext.jsUSERPROXYAPPSTORELEGENDHTTP / webhookinternalasync refreshfocal
Payments via Stripe and Zeffy. The CRM syncs 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 read cache that refreshes once a day, takes payments through Stripe and donations through Zeffy, and puts every admin screen behind access control based on roles.

Key decisions

A SQL read cache instead of live Airtable calls

Airtable's API rate limit could not serve the portal directly. I copy the CRM data, which changes slowly, into Postgres once a day and read from there, so the pages members see 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 their payments when the page loads, not stored as a flag. That keeps one source of truth and leaves no stale status to reconcile.

The hard part

Email scanners were burning the confirmation links

Corporate email security scanners prefetch links to check them. They were opening the confirmation link before the user did and spending the token, which only works once, so real signups failed. The fix was to stop treating a bare GET as confirmation and to require an explicit action from the user.

Security and performance

  • Admins must pass a second factor (TOTP with backup codes) to reach any screen that shows personal data.
  • Login and email endpoints are rate limited, and production server actions are locked to their origin.
  • For Loi 25 compliance, consents are captured at signup, and an erasure request is honored across both the portal and the CRM.

Stack

Next.js 15TypeScriptPostgreSQLDrizzle ORMAuth.js v5StripeZeffyDockerGitHub Actions