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
requireof 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
helmet→ JSON parser → session → static files/api/*routers; protected routers userequireAuth- Route validates body/query with zod
- Service runs (transaction for anything that writes more than one row)
- Route sends
{ ok: true, data: ... } - 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.