Files
Emberfall/docs/ARCHITECTURE.md
T

4.2 KiB

ARCHITECTURE.md

Layering (strict, one-way)

routes  →  services  →  game (pure rules)
   ↓          ↓
middleware    db (prepared statements)
  • routes/: parse + validate request, call a service, shape the response. No business logic. No SQL.
  • services/: orchestrate. Open transactions, read/write DB, call game/ functions, return plain objects.
  • game/: pure functions. No require of db/express. Deterministic given inputs (RNG is passed in).
  • db/: connection, migrations, seeds. Only services import db.
  • middleware/: auth guard, rate limiting, error handler, request validation helper.
  • utils/: tiny helpers (rng, time, ids, logger).

Dependencies only point downward. game/ never imports from services/.

Folder structure

emberfall/
├── NOTES.md  STATUS.md  README.md
├── package.json  .env  .env.example  .gitignore
├── docs/                      # specs (this folder)
├── data/                      # SQLite file lives here (gitignored)
├── scripts/
│   ├── migrate.js             # runs src/db/migrations/*.sql in order
│   ├── seed.js                # runs src/db/seeds/*.js (idempotent)
│   └── seed-dev.js            # demo account + hero
├── src/
│   ├── server.js              # listen()
│   ├── app.js                 # builds express app (exported for tests)
│   ├── config/index.js
│   ├── db/
│   │   ├── index.js           # better-sqlite3 handle
│   │   ├── migrations/        # 001_users.sql, 002_characters.sql ...
│   │   └── seeds/             # zones.js, monsters.js, items.js, expeditions.js
│   ├── middleware/
│   │   ├── requireAuth.js
│   │   ├── rateLimiters.js
│   │   ├── validate.js        # zod wrapper
│   │   └── errorHandler.js
│   ├── routes/
│   │   ├── index.js           # mounts all routers under /api
│   │   ├── auth.js  character.js  hunt.js  inventory.js
│   │   ├── shop.js  expeditions.js  leaderboard.js  heroes.js
│   ├── services/
│   │   ├── authService.js  characterService.js  huntService.js
│   │   ├── inventoryService.js  shopService.js
│   │   ├── expeditionService.js  leaderboardService.js
│   ├── game/
│   │   ├── constants.js       # ALL tunable numbers live here
│   │   ├── classes.js  stats.js  energy.js  combat.js
│   │   ├── rewards.js  leveling.js  loot.js
│   └── utils/
│       ├── rng.js  time.js  errors.js  logger.js
├── public/
│   ├── index.html  login.html  register.html
│   ├── create-hero.html  game.html  shop.html  leaderboard.html
│   ├── css/  (base.css, theme.css, layout.css, components.css)
│   ├── js/   (api.js, auth.js, game.js, ui.js, ...)
│   └── img/
└── tests/
    ├── unit/          # src/game/* and utils
    └── integration/   # HTTP-level tests using a temp DB

Module format

Use CommonJS (require / module.exports) everywhere on the server, to keep tooling simple. Frontend JS uses plain <script> tags or ES modules (type="module") — pick one and stay consistent (log in DECISIONS.md).

Request lifecycle

  1. helmet → JSON parser → session → static files
  2. /api/* routers; protected routers use requireAuth
  3. Route validates body/query with zod
  4. Service runs (transaction for anything that writes more than one row)
  5. Route sends { ok: true, data: ... }
  6. Any thrown AppError → errorHandler → { ok: false, error: {code, message} }

Transactions

Any action that spends a resource AND grants a result (hunt, buy, claim) is ONE db.transaction(() => { ... })(). better-sqlite3 is synchronous, so this also prevents double-spend races from parallel requests.

Time

All timestamps are integer milliseconds since epoch (UTC), stored as INTEGER. src/utils/time.js exports now() so tests can stub it. Never call Date.now() directly in services or game code.

Config

Gameplay numbers → src/game/constants.js. Environment/infra → src/config/index.js. Never hardcode magic numbers inline.