268ea31bb8
Coins were always 1:10 of XP. Now they have their own event log + a
helper that dedups by reason within a configurable window.
Backend:
• migration 032 creates coin_log (user_id, amount, reason, created_at)
with indices for the 'fired today?' check
• awardCoins now records into coin_log on every call (reason defaults
to 'xp_bonus' for the legacy XP-proportional path)
• awardCoinsOnce(userId, amount, reason, window) — fires the bonus
only if no row matches in the window:
'day' → DATE(created_at) = today
'week' → ISO week match
'forever' → never twice
Wired events (Phase 4 subset of the plan):
• Daily login — 10 coins, once/day. Hooked in updateStreak so the
bonus rides on the existing 'daily_activity' XP trigger.
• Daily goal completion — 15/25/40 coins (easy/medium/hard), once/day.
Sits next to the existing tier XP bonus in updateDailyGoal.
• Variant clear — 30 coins, once per (user, variant) forever. Fires
from the exam-prep attempts endpoint when the user's final correct
answer fills out a math9 variant.
Deferred (need invasive trigger hooks): weekly goal, paragraph close,
boss defeated, referral.
Verified end-to-end: awardCoinsOnce returns true→false on repeated
calls, coin_log records the first, coins balance moves once.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Versioned migrations
Each schema change is a separate .sql file, applied in alphabetical order.
Applied files are tracked in the _migrations table.
Applying migrations
npm run migrate # apply pending migrations (safe to re-run)
npm run migrate:bootstrap # mark 000_baseline.sql as applied on existing DB (run ONCE per env)
npm run migrate:legacy # run legacy migrate.js (kept for reference, do not use)
Naming convention
NNN_short_description.sql
Examples: 001_add_user_avatar.sql, 002_drop_unused_columns.sql
To find the next number:
ls backend/src/db/migrations/*.sql | sort -r | head -1
Rules
- Never edit a migration file after it has been committed and deployed.
- To revert: write a new migration that undoes the change.
- Each file must be valid SQLite SQL (not PostgreSQL — no SERIAL, no EXTENSION).
- Use
IF NOT EXISTS/IF EXISTSwhere possible for safety. - Test on a copy of the prod DB before deploying.
Deploy order (first time on a new environment)
npm run migrate:legacy # initialize full schema (existing init script)
npm run seed:permissions # seed default permissions and achievements
npm run migrate:bootstrap # mark 000_baseline.sql as applied
npm run migrate # apply any newer migrations (should say "nothing to apply")
npm start
Adding a new migration
# 1. Create the file
echo "ALTER TABLE users ADD COLUMN avatar_url TEXT;" > backend/src/db/migrations/001_add_avatar_url.sql
# 2. Apply and verify
npm run migrate
# 3. Commit
git add backend/src/db/migrations/001_add_avatar_url.sql
git commit -m "db: add avatar_url column to users"
Files
| File | Description |
|---|---|
000_baseline.sql |
Snapshot of full schema as of 2026-05-06. Never runs on existing DBs. |