انتقل إلى المحتوى
Mohamed Tahiri
Miami --:--
2024 → · شخصي

Course platform

An association wants to make classes and learning centres near you easy to find. With a team of volunteers, I have been carrying the backend, search sync and the whole infrastructure as code since 2024. Top contributor to the project.

Role
Backend, search, infra
Team
About fifteen volunteers
Volume
1,100+ commits, ~100 PRs
Environments
dev, staging, prod

دراسة الحالة هذه متاحة بالفرنسية والإنجليزية فقط.

commits, top contributor
1,100+
Terraform modules
14
identical environments, in code
3
releases shipped since v1
73

The product

People look for classes near them, and the information is scattered across ageing websites, social networks and word of mouth. The platform brings together learning centres, their classes and schedules, with search by place, topic and level. It runs on the web, as a PWA and as iOS and Android apps.

Architecture

VERCEL · PARISBrowserSITE · PWAMobile appsIOS · ANDROIDCloudflareDNS · WAF · ACCESSFront endNEXT.JS 16 · SSRAPINESTJS 11 · TYPEORMSearchALGOLIA · PUBLIC ONLYSupabasePOSTGRES · AUTH · STORAGEServicesRESEND · MAPS · PUSHGoogle CloudLOGS · SECRETSSentryERRORS · UPTIMEGitHub ActionsTESTS → MIGRATIONS → PRODTerraformTERRAGRUNT · 14 MODULESAFTER COMMITKEYLESS · OIDC
This whole diagram is described in Terraform: three identical environments, dev, staging and prod.

The Next.js front end and the NestJS API are deployed on Vercel, behind Cloudflare. Data lives in Postgres on Supabase, which also handles auth and files. The front end queries Algolia directly for search, and falls back to a Postgres engine if Algolia doesn’t answer.

What was hard

A search index that never lies

The index must only hold what is public, stay current, and never make a database write fail. I designed the sync: after each write, a TypeORM subscriber reloads the entity with its relations, publishes it if it is public and removes it otherwise. The default is to hide. A class embeds its centre’s name, so renaming a centre updates all its classes. And every night, a full reindex swaps whole collections at once to purge orphans.

The sneakiest trap came from elsewhere: TypeORM hands the subscriber the update payload, not the row. An update without an id therefore never triggers the resync, without a single error. I wrote an ESLint rule that uses the type checker to forbid it:

centers.service.ts
// TypeORM hands the subscriber the update payload, not the row:
// without an id, the search index is never told and silently goes stale.
await centers.update({ slug }, { isPublic: false })
//    ^ lint error: require-id-in-typeorm-update

await centers.update({ id }, { isPublic: false }) // ok: the index follows

A teammate later generalised the mechanism: changes are buffered per transaction and sent only after the commit, never on rollback.

Google Cloud logs, without a single key

A Vercel function has no Google identity, and nobody wanted a service account key lying around. On each request, the logger exchanges Vercel’s OIDC token for short-lived Google credentials (Workload Identity Federation), then writes after the response. Five log sinks each have their own retention, and derived metrics feed a dashboard.

A whole environment in one call

I wrote most of the infrastructure: 82 of the repo’s 103 commits. Terraform and Terragrunt describe Vercel, Cloudflare, Supabase, Algolia, Sentry, GitHub and Google Cloud. One instance of the main module is a whole environment:

terragrunt.hcl
# live/app/prod/terragrunt.hcl: a whole environment in one call
include "root" { path = find_in_parent_folders("root.hcl") }
terraform { source = "../../../modules/app" }

inputs = {
  environment = "prod"
  # two Vercel projects, DNS, Maps keys, search indexes, secrets,
  # log sinks, dashboards, uptime monitors and access policies
}

CI authenticates without keys and can only read the secrets it needs. Every day, a plan detects any drift between the code and reality, and every PR gets a comment summarising what will change.

Shipping without breaking

The database keeps its own rules

CHECK constraints, partial indexes, enum arrays instead of join tables. Registration dates are `date`, no longer `timestamptz`: no more day slipping with the time zone.

No forgotten migration

Before every deploy, CI applies the migrations, then tries to generate a new one. If anything comes out, the build fails and prints the diff.

Scheduled jobs without a server

Crons are HTTP routes called by Vercel, guarded by a secret compared in constant time. Every job is idempotent and logged.

What I took away

Carrying a product with volunteers means making contributions easy: a lint rule that catches a trap beats a page of documentation. And infrastructure as code is the only documentation that doesn’t go stale.

التقنيات

NestJSTypeORMPostgreSQLSupabaseNext.jsAlgoliaVercelCloudflareTerraformTerragruntGoogle CloudSentryGitHub Actions
المشروع التاليSDKs, Alexa and Home Assistant →
تواصل

لنبنِ شيئًا معًا.

أو اترك رسالة