Skip to content

System map

EverDuet is a monorepo and a single versioned production release, but it is not one undifferentiated application. The architecture separates user experience, administration, domain logic, durable state, asynchronous transport, and platform concerns.

The production topology

text
Browser

  ├── www.everduet.com ─────► web :3000 ──────┐
  │                            Next.js          │
  │                                             ▼
  ├── admin.everduet.com ───► admin :3001 ──► PostgreSQL
  │                            Next.js          ▲
  │                                │            │
  │                                └─ revalidate public prompt cache

  └── docs.everduet.com ─────► docs :3003
                               static HTML, CSS, JS

                         Redis :6379 ◄──── BullMQ worker :3002
                              private             private health

Only web, admin, and docs receive public routes. PostgreSQL, Redis, and the worker stay on the private Compose network.

Why two Next.js applications?

The web and admin apps share a database and domain packages but deploy as separate processes.

ConcernWebAdmin
AudienceCouplesExplicit allowlisted administrators
PrioritySpeed, calmness, privacyReliability, auditability, safe curation
Typical readsDaily prompt, responses, notificationsPrompt lifecycle, quality review, audit
Failure posturePreserve a clear user flowFail closed on authorization and lifecycle rules

A single app could share more UI code, but it would also make every web deployment carry admin routes and permission assumptions. Two apps preserve a visible security and deployment boundary without introducing separate repositories or databases.

The workspace boundaries

text
apps/
  web/          user-facing Next.js application
  admin/        separately authorized Next.js application
  worker/       private BullMQ consumer and health server
  docs/         this static VitePress learning site

packages/
  core/         relationship, prompt, notification, audit rules
  db/           Prisma schema, migrations, shared client
  auth/         shared authorization helpers
  queue/        typed job names, payloads, retry policy, Redis boundary
  observability/ structured logging and request-boundary helpers
  api-client/   generated OpenAPI client and contract validation

The rule of thumb is simple: move code into a package when two application surfaces need the same invariant or contract—not merely because it might be reusable someday.

One release graph

Coolify builds compose.coolify.yaml. Compose expresses release ordering rather than relying on a human checklist:

text
release-build


 verify ──success──┬──► docs
                   └──► migrate ──success──► web ──healthy──► admin
                                  │         │
                                  │         └──────────────► deploy watcher
                                  └──► worker ◄──healthy── redis

The image cannot exist unless lint, typechecks, unit tests, and all production builds pass. The migration is one-shot. The schema-coupled service group starts only after it succeeds; the static learning site waits for verification but does not pretend to need PostgreSQL.

Why not deploy every service independently?

Independent deployment is valuable when services have different teams, scaling curves, or availability requirements. Here, schema, domain packages, web, admin, and worker evolve together. A single release removes incompatible version combinations and keeps the operational model inspectable.

Sources of truth

The system deliberately has few:

  • PostgreSQL: relationships, prompts, responses, notifications, admin audits.
  • WorkOS: identity, sessions, and bearer-token signing keys.
  • Git + migrations: application and schema evolution.
  • Redis: temporary job transport only; never authoritative product state.
  • Next.js cache: a derived optimization for public prompt content.

TIP

When reviewing a new feature, ask “where is the source of truth?” If the answer is both PostgreSQL and Redis, or both server state and client state, the design needs a reconciliation story.

Built as a living architecture notebook for a personal project.