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
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 healthOnly 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.
| Concern | Web | Admin |
|---|---|---|
| Audience | Couples | Explicit allowlisted administrators |
| Priority | Speed, calmness, privacy | Reliability, auditability, safe curation |
| Typical reads | Daily prompt, responses, notifications | Prompt lifecycle, quality review, audit |
| Failure posture | Preserve a clear user flow | Fail 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
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 validationThe 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:
release-build
│
▼
verify ──success──┬──► docs
└──► migrate ──success──► web ──healthy──► admin
│ │
│ └──────────────► deploy watcher
└──► worker ◄──healthy── redisThe 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.