Summary: The elixir-backend is structured as a modulith — a single deployable unit organized as a set of strictly isolated domains that communicate only through events and explicit public interfaces. Sources: direct code inspection (lib/backend/, guides/structure.md, mix.exs) Last updated: 2026-05-15


What it is

A modulith applies microservice design discipline (domain isolation, explicit interfaces, no shared state) inside a single Elixir release. There is one deployed binary, one Kubernetes deployment, one CI pipeline — but internally the code is partitioned into domains that are not allowed to call each other directly.

The 7Mind engineering team describes this as “delightfully boring”: the complexity budget goes into product problems, not distributed systems plumbing.

Domain list

Each domain lives at lib/backend/<domain>/ in elixir-backend:

DomainResponsibility
accessAuthorization and access control
activationUser onboarding workflows
analyticsEvent forwarding to Rudderstack
contentCMS content delivery, Algolia search, Provider pattern for dual-brand
externalThin HTTP wrappers around third-party services (Braze, Aury, Rudderstack)
monetizationSubscriptions and billing (Chargebee, Fastspring, Barmer, Mondia)
practiceSession tracking, streaks, progress, history
preventionZPP insurance certificates (PDF generation, Cloud Storage)
sharedEvent bus, observability, HTTP pools, config helpers
user_identityAuthentication, sessions, SSO (Apple, Google, Facebook), SuperTokens
webPhoenix endpoints and routers only — no business logic lives here

How domains communicate

Domains must not import from each other directly. Two allowed coupling mechanisms:

  1. Event bus (primary) — domain publishes a struct to Google Pub/Sub; other domains or external consumers subscribe via Broadway. See backend-event-bus.
  2. Explicit public interface — a module exposes a narrow public API surface; other domains call only that surface, never internal modules.

Direct cross-domain function calls to internal modules are a convention violation caught in code review.

Per-domain databases

Each domain has its own PostgreSQL database and its own Ecto repo. They do not share tables. Environment variables follow the pattern BACKEND_<DOMAIN>_DATABASE_URL.

Known domain databases:

  • BACKEND_PREVENTION_DATABASE_URL
  • BACKEND_PRACTICE_DATABASE_URL
  • BACKEND_MONETIZATION_DATABASE_URL
  • BACKEND_USER_IDENTITY_IDENTITY_DATABASE_URL
  • BACKEND_USER_IDENTITY_APIV1_DATABASE_URL
  • BACKEND_MICROPAYMENTS_DATABASE_URL

Migrations are domain-scoped and run per-database. The Kubernetes init container runs all migrations before the main container starts.

Why this matters for agents

  • When editing code in one domain, do not reach into another domain’s internals. Find the public interface or publish an event.
  • Migrations belong to their domain. Never write a migration that joins or alters another domain’s tables.
  • Adding a new external service dependency? It goes in the external domain first as a thin wrapper, then consumed by the domain that needs it.