The mechanic
- Award — call
awardPoints(userId, actionKey)from wherever an action worth points happens. The engine keeps a running total plus a per-action breakdown. - Reward (optional, off by default) — the first time a user’s total crosses
GAMIFICATION_REWARD_THRESHOLDin a period, they’re granted a free subscription period (a localpromotionalentitlement — no store, no RevenueCat call). Skipped for a user who already holds the entitlement (it’s a win-back, never overwrites an active paid subscription). - Reset — a cron job zeroes everyone at the start of each period. Set the period to
noneto accumulate for life.
The rule table — the one place you edit
shipwide_api/src/config/constants.ts → GAMIFICATION_ACTIONS:
1
Add a row
e.g.
invited_friend: { points: 25, labelKey: 'gamification.actions.invited_friend' }.2
Award it
Call
awardPoints(userId, 'invited_friend') from the server code where it happens (awardOnce
for one-time milestones, awardDailyThrottled for once-a-day actions).3
Label it
Add the label to the app’s
i18n/locales/*/gamification.json under actions.Demo actions (delete or keep)
Both are best-effort: a gamification failure can never break sign-in or a profile update.
Activation — env only
The engine and stats screen are always on. The reward loop is off by default — it hands out real premium access, so you opt in.shipwide_api/.env:
The reward reuses the same
entitlements table your subscriptions use (Billing),
so a granted free period reads as an ordinary active entitlement everywhere. It works with no
RevenueCat account — the grant is local.
Scheduling the reset
Run it from cron at the start of each period — a no-op whennone, so the same cron entry can
stay in place while you turn resets off:
Server surface
awardPoints (atomic total + breakdown, fires the reward hook on threshold crossing), awardOnce,
awardDailyThrottled, GET /v1/gamification/stats, and the reset job. Operators can view a user’s
score and apply a manual adjust ({ delta }, audited, never triggers the reward hook) on the
admin user-detail page. On the client, views/pages/account/Stats.vue renders the total (count-up),
a progress bar to the threshold (only when the reward is enabled), and the breakdown table.