REST API backend for the Marquinhos Discord bot ecosystem. Serves gamification (XP, levels, achievements), Wordle, maze minigame, Last.fm scrobbling, and Discord OAuth for the companion bot and web app.
- Bun 1.3.x — runtime, package manager, and test runner
- Node.js 22.x (only needed for editor tooling / type-checking with
tsc) - SQLite (bundled via
bun:sqlite, no external install required) - Docker (optional, for containerized runs)
git clone git@github.com:Devaneios/marquinhos-web-api.git
cd marquinhos-web-api
bun install
cp .env.example .env # if present, otherwise create manually — see belowRequired environment variables (.env):
| Variable | Purpose |
|---|---|
HTTP_PORT |
Port the HTTP server listens on |
HTTPS_PORT |
Port for HTTPS (if configured) |
NODE_ENV |
development / production |
SQLITE_PATH |
Path to the SQLite database file |
MARQUINHOS_API_KEY |
Shared secret for bot-to-API requests (Authorization: Bearer) |
MARQUINHOS_SECRET_KEY |
Key used to encrypt/decrypt Discord session tokens |
DISCORD_BOT_TOKEN |
Bot token, used for Discord API calls |
DISCORD_CLIENT_ID / DISCORD_CLIENT_SECRET / DISCORD_REDIRECT_URI |
Discord OAuth config |
LASTFM_API_KEY / LASTFM_SHARED_SECRET / LASTFM_REDIRECT_URI |
Last.fm scrobbling OAuth |
SPOTIFY_CLIENT_ID / SPOTIFY_CLIENT_SECRET |
Spotify metadata lookups |
CORS_ORIGINS |
Comma-separated allowlist (defaults to localhost:4200 + prod web app) |
Run the database migrations and start the dev server:
bun run devsrc/index.ts runs runMigrations() and initializes gamification defaults on boot — a failure here crashes the process intentionally rather than serving with a broken config.
docker compose up --buildThe Dockerfile is a two-stage build: dependencies and the Wordle valid-guesses word list are compiled in a builder stage, then only node_modules, src/, and the generated word lists are copied into the oven/bun:1-alpine runtime image.
Health check:
curl http://localhost:3000/api/health
# { "status": "ok" }Bot-authenticated request (server-to-server):
curl -X POST http://localhost:3000/api/gamification/xp \
-H "Authorization: Bearer $MARQUINHOS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"userId": "123", "guildId": "456", "eventType": "command"}'Web-authenticated request (from the Angular frontend, Discord token instead of the API key):
curl http://localhost:3000/api/gamification/level/123/456 \
-H "Authorization: Bearer <discord-access-token>" \
-H "marquinhos-agent: web"| Mount | Router | Notes |
|---|---|---|
/api/auth |
auth.route.ts |
Discord OAuth login/callback |
/api/user |
user.route.ts |
User profile/settings |
/api/scrobble |
scrobble.route.ts |
Last.fm scrobble ingestion |
/api/privacy-policy |
privacyPolicy.route.ts |
Static policy content |
/api/gamification |
gamification.route.ts |
XP, levels, leaderboard, game results |
/api/evolutive-achievements |
evolutiveAchievements.route.ts |
Tiered achievement progression |
/api/games/maze |
maze.route.ts |
Maze minigame sessions |
/api/wordle |
wordle.route.ts |
Wordle guesses and word list review |
- Runtime: Express app on Bun, entry point
src/index.ts. - Auth: two paths through the same middleware chain (
middlewares/botAuth.ts):- Requests with header
marquinhos-agent: webare routed toverifyDiscordToken(middlewares/userAuth.ts), which decrypts the token, checks expiry, and fetches the user's Discord identity + guild role. - All other requests are checked against
MARQUINHOS_API_KEYusing a timing-safe buffer comparison.
- Requests with header
- Persistence:
bun:sqlite, single file DB atSQLITE_PATH. Schema is created idempotently indatabase/sqlite.ts(CREATE TABLE IF NOT EXISTS), with incremental changes applied via numbered SQL files indatabase/migrations/and run throughdatabase/migrate.tsat boot. - Gamification:
services/gamification.tshandles XP awards, level-up detection, and cooldowns;services/evolutiveAchievements.tstracks per-user stat counters and auto-evolves tiered achievements when thresholds are crossed. Both are wired into the sameaddXPcall path. - Wordle: valid-guess word list is pre-generated at Docker build time (
scripts/build-valid-guesses.ts) fromwordlist.txt+ an external word frequency list, then loaded into memory once on boot (getValidationSet()) to avoid disk I/O per request. - Rate limiting:
express-rate-limitis wired intoindex.tsbut currently commented out — re-enable before exposing sensitive endpoints publicly. - Error handling: a 404 catch-all and a 4-arg Express error handler sit at the bottom of the middleware stack; error details are only returned in the response body outside of
production.
bun run typecheck # tsc --noEmit
bun run lint # eslint src/**/*.ts
bun run format:check # prettier --check
bun test tests/**/*.spec.ts- Husky +
lint-stagedrunprettier --writeandeslint --fixon staged*.tsfiles pre-commit. - Commit messages are linted with
commitlintagainst the conventional-commits config — usetype(scope): message(feat:,fix:,chore:, etc.). - Match existing patterns: controllers stay thin and delegate to
services/, routes bind controller methods with.bind(controller), and Zod schemas inschemas/validate request bodies viavalidateRequest.ts. - Open a PR against
main; CI must pass typecheck, lint, and tests before merge.
ISC