- Node.js 20 or newer.
- Docker (only for the integration tests, which spin up real Postgres and RabbitMQ containers).
npm install
cp .env.example .env # then edit DATABASE_URL and RABBITMQ_URL
npm run dev # tsx watchFor a development RabbitMQ + Postgres, the easiest option is the docker-compose file from the common-settings repo. It already exposes the broker on amqp://guest:guest@localhost:5672 and provisions the settings exchange. Point your local DATABASE_URL at a throwaway Postgres with a fixture meet_user table — the integration tests show the minimal schema.
src/
index.ts Process entrypoint: load config, wire deps, run consumer + health server, handle SIGTERM
config.ts Env-var parsing with Zod
consumer.ts RabbitMQ subscribe loop, ack/throw semantics
handler.ts Per-message logic. The interesting part.
db.ts Drizzle ORM (postgres-js) wrapper around the single UPDATE
schema.ts Zod schema for the message envelope and payload
schema/
meet-user.ts Drizzle table definition — the subset of Meet's meet_user we touch
language.ts ISO 639-1 → Django LANGUAGES mapping
metrics.ts prom-client registry and counters
logger.ts pino instance plus email hashing helper
health.ts HTTP server for /healthz, /readyz, /metrics
tests/
unit/ Fast, no docker. Pure-function tests with mocked db.
integration/ testcontainers spin up Postgres; verifies SQL behaviour.
The shape is deliberately flat. Every file has one job and the call graph is shallow. If you find yourself adding a sixth or seventh kind of dependency, the abstraction is probably wrong.
npm run test:unit # fast, no docker
npm run test:integration # requires docker (testcontainers)
npm test # bothThe unit tests cover the handler exhaustively — every outcome label has at least one test, including the error-classification branches. Integration tests verify the actual SQL runs against a real Postgres image.
When changing behavior:
- Add a unit test for the new outcome.
- If the change touches the SQL, add an integration test that exercises it.
- Run
npm testlocally before pushing. CI runs both.
npm run lint # eslint
npm run format # prettier --write
npm run typecheck # tsc --noEmitThe pre-merge gate is: typecheck clean, lint clean, unit tests pass. Integration tests run on every push but are not blocking by default (they need Docker in CI).
docker build -t meet-common-settings:dev .The Dockerfile is a two-stage build. The runtime is gcr.io/distroless/nodejs20-debian12:nonroot — no shell, no package manager, no root user. If you need to debug a running container, you can swap the base image temporarily to node:20-bookworm-slim for that build.
- Bump the version in
package.jsonand add a line toCHANGELOG.md. - Commit and tag:
git tag vX.Y.Z && git push origin vX.Y.Z. - Push the image to the registry (CI handles this on tag push if configured).
Updating the deployment to pick up the new image is handled separately by whichever tool owns the deployment.
The shortest path:
- Add the field to the Zod schema in
src/schema.ts. - Add the column to the drizzle table in
src/schema/meet-user.ts(type and constraints). - Add the field to
UserSettingsUpdateand to the dynamic SET builder insrc/db.ts. - Extend
handler.tsto copy the field frompayloadintoupdates, with any validation or mapping you need. - Add tests in
tests/unit/handler.spec.tsandtests/integration/db.spec.ts. - Update the architecture doc's "Which fields we sync" table.
- Add the column to the PostgreSQL grant in the operations doc and in production.
Don't ship a new field without granting it. The role is least-privilege by design, so the UPDATE will fail loudly rather than silently drop the column from the write.