Schedule an email once, at exactly the time you want it sent. A background worker watches for due emails, sends them, retries automatically on failure, and keeps a full delivery audit trail — no manual cron jobs, no babysitting.
- API: Node.js / Express, Postgres, JWT auth,
node-cron-driven delivery (no Bull/job-queue library — Redis here is caching/rate-limiting only, not the queue) - Customer app: Next.js (App Router, JavaScript), Tailwind + shadcn/ui, Framer Motion
- Admin console: a completely separate, standalone Next.js app for internal staff — own codebase, own container, own login, own session cookie; it never shares state with the customer app and only ever talks to the API
- Caching: Redis — cache-aside reads (user profiles, stats), Redis-backed rate limiting, refresh-token hash lookups
- API docs: Swagger UI at
/api-docs(local dev only, gated behind an explicit env flag) - Analytics: self-hosted Umami, privacy-friendly, its own database
- Reverse proxy: nginx in front of everything — one port (80), routes by hostname, and is what makes the API's
trust proxysetting trustworthy instead of spoofable - Runs on: Docker Compose — Postgres, Redis, the API, the worker, both frontends, Umami, and nginx all start with one command
cp server/.env.example server/.env # set JWT_SECRET / JWT_REFRESH_SECRET / SESSION_SECRET
docker compose -f infra/docker-compose.yml up -d --buildThen open http://app.localhost for the customer app, http://admin.localhost for the admin console (log in with admin@test.com / Password123! from the seed data) — both resolve to 127.0.0.1 in any modern browser, no /etc/hosts editing needed. This is the intended way to run the whole stack — everything (Postgres, API, worker, both frontends, nginx) runs in containers named aens-*, not from your terminal.
Prefer to run a piece directly on your machine for active development? See docs/SETUP.md for the manual, per-directory instructions (server/, client/user/, and client/admin/ each have their own package.json).
docs/SETUP.md— getting a local environment running, Docker or manualdocs/API.md— REST endpoint referencedocs/ARCHITECTURE.md— how it's built and why, including real bugs found and fixed along the waydocs/SECURITY.md— security audit test list: what's implemented and verified, what's an accepted tradeoff, what's an open gapdocs/TODO.md— prioritized, documented path to production-grade: what to add, remove, and fix, and whydocs/PLAN.md— the implementation plan this was built from
- Schedule emails with CC/BCC, priority, and a custom send time (any timezone)
- Automatic retries with a configurable attempt limit, full per-attempt audit trail
- JWT auth with email verification, password reset, and account lockout after repeated failed logins
- A customer dashboard: overview stats + delivery chart, scheduled-email list with filters, per-email detail/attempt history, profile & account settings
- A standalone admin console for internal staff: user management, system-wide email/log visibility — a separate application with its own login and session, not a route inside the customer app, and not reachable from it
- Redis-backed caching (cache-aside reads, explicit invalidation on writes) and rate limiting
- Swagger/OpenAPI docs and self-hosted Umami analytics
server/ Express API + cron worker — its own package.json, .env, Jest suite
client/
user/ Next.js customer app + marketing site — its own package.json
admin/ Next.js admin console — standalone app, its own package.json, its own login
infra/
docker-compose.yml the whole stack, one command
docker/ a Dockerfile per service
nginx/ nginx.conf, proxy_params.conf — the reverse proxy in front of everything
docs/ setup, API reference, architecture notes, security audit, the original plan
Each of server/, client/user/, and client/admin/ is self-contained: its own dependencies, its own .env. There's nothing to install at the repo root, and client/user//client/admin/ don't import from each other — client/ itself is just their parent directory, not a package.
- CI (
.github/workflows/ci.yml): lints,npm audits (fails on high/critical), and tests the server against a real Postgres service container; lints, audits, and builds both frontends; lints the nginx config withnginx -t— on every push/PR tomain/develop. - CD (
.github/workflows/cd.yml): on push tomain, builds each image (api,worker,client,admin,nginx), scans it with Trivy (fails on high/critical, blocking the push), then publishes to GitHub Container Registry (ghcr.io). Every action in both workflows is pinned to a commit SHA, not a floating tag. Pulling the images onto an actual host is left to you — no deploy target is assumed.