21 KiB
Engagement System - Complete Documentation
Overview
The Engagement System is a comprehensive gamification platform that rewards users for participation through XP, levels, points, achievements, and redeemable rewards.
Table of Contents
- Core Concepts
- Database Schema
- Backend API
- Frontend Integration
- Points & XP System
- Achievements
- Rewards Store
- Security & Anti-Abuse
- Admin Management
- Production Checklist
Core Concepts
Points
- Currency for redeeming rewards
- Can be manually adjusted by admins
- Awarded for user actions (commenting, voting, etc.)
- NOT deducted when spent on XP-only rewards
XP (Experience Points)
- Progression metric for leveling up
- Mirrors points by default (except admin adjustments)
- Determines user level
- Cannot be spent, only earned
Levels
- Automatically calculated from total XP
- Formula:
Total XP to Level L = 50 * (L-1) * L - Each level requires:
100 * Ladditional XP - Visual progression with colored badges
- Titles: Začátečník → Nováček → Aktivní člen → Veterán → Expert → Mistr → Legenda
Achievements
- One-time milestones that award points + XP
- Automatically checked and granted
- Examples: first comment, 10 votes, newsletter subscription
Rewards
- Items users can redeem with points
- Types: avatars, merchandise coupons, custom unlocks
- Limited or unlimited stock
- Redemption workflow with approval system
Database Schema
user_profiles
CREATE TABLE user_profiles (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE,
updated_at TIMESTAMP WITH TIME ZONE,
user_id BIGINT UNIQUE NOT NULL,
points BIGINT DEFAULT 0,
level INTEGER DEFAULT 1,
xp BIGINT DEFAULT 0,
username VARCHAR(32) UNIQUE NOT NULL,
avatar_url VARCHAR(500),
animated_avatar_url VARCHAR(500),
avatar_upload_unlocked BOOLEAN DEFAULT FALSE
);
Indexes: user_id, points DESC, level DESC, xp DESC, username
points_transactions
CREATE TABLE points_transactions (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE,
user_id BIGINT NOT NULL,
delta BIGINT NOT NULL,
xp_delta BIGINT DEFAULT 0,
reason VARCHAR(64) NOT NULL,
meta JSONB
);
Common Reasons:
comment_create- User posted a comment (5 pts/XP)comment_reacted- User reacted to a comment (1 pt/XP)poll_vote- User voted in a poll (3 pts/XP)newsletter_subscribe- User subscribed to newsletter (12 pts/XP)redeem- User redeemed a reward (negative points)redeem_refund- Redemption rejected (positive points)admin_adjust- Manual adjustment (points only, no XP)achievement:CODE- Achievement unlocked
achievements
CREATE TABLE achievements (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(64) UNIQUE NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT,
points BIGINT DEFAULT 0,
xp BIGINT DEFAULT 0,
icon VARCHAR(255),
active BOOLEAN DEFAULT TRUE
);
Default Achievements:
first_comment- První komentář (10 pts/XP)first_vote- První hlasování (8 pts/XP)newsletter_sub- Odběr novinek (12 pts/XP)comments_10- Komentátor (20 pts/XP)votes_10- Hlasující (20 pts/XP)comments_50- Aktivní člen (50 pts/XP)votes_50- Věrný fanoušek (50 pts/XP)comments_100- Veterán diskuzí (100 pts/XP)
user_achievements
Junction table tracking which achievements each user has unlocked.
reward_items
CREATE TABLE reward_items (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
type VARCHAR(32) NOT NULL,
cost_points BIGINT NOT NULL,
image_url VARCHAR(500),
stock INTEGER DEFAULT 0,
active BOOLEAN DEFAULT TRUE,
metadata JSONB
);
Types:
avatar_static- Static image avatar (auto-applied)avatar_animated- Animated GIF avatar (auto-applied)avatar_upload_unlock- Unlock custom avatar uploadmerch_coupon- Merchandise discount codemerch_physical- Physical item (requires fulfillment)merch_digital- Digital downloadcustom- Admin-defined
Stock:
-1= Unlimited0= Out of stock>0= Limited quantity
reward_redemptions
CREATE TABLE reward_redemptions (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
reward_id BIGINT NOT NULL,
status VARCHAR(24) DEFAULT 'pending'
);
Statuses:
pending- Awaiting admin approval (manual rewards)approved- Admin approved but not yet fulfilledfulfilled- Item delivered to userrejected- Admin rejected (points refunded)
Backend API
Public Endpoints
GET /api/v1/engagement/rewards
List all active rewards available for redemption.
Response:
[
{
"id": 1,
"name": "Avatar Blue #1",
"type": "avatar_static",
"cost_points": 50,
"image_url": "/uploads/avatars/blue-1.png",
"stock": 5,
"active": true
}
]
Protected Endpoints (Require Auth)
GET /api/v1/engagement/profile
Get current user's engagement profile.
Response:
{
"user_id": 123,
"points": 1250,
"level": 12,
"xp": 7800,
"username": "fan-superstar",
"avatar_url": "https://api.dicebear.com/7.x/pixel-art/svg?seed=fan-superstar",
"animated_avatar_url": null,
"avatar_upload_unlocked": true,
"achievements": 8
}
PATCH /api/v1/engagement/profile
Update username.
Request:
{
"username": "new-username"
}
Validation:
- 3-32 characters
- Only lowercase letters, numbers,
-,_,. - No consecutive special chars
- Cannot start/end with special chars
- Reserved words blocked
PATCH /api/v1/engagement/avatar
Update avatar URLs.
Request:
{
"avatar_url": "/uploads/my-avatar.png",
"animated_avatar_url": "/uploads/my-avatar.gif"
}
Note: Custom uploads require avatar_upload_unlocked = true.
POST /api/v1/engagement/redeem
Redeem a reward.
Request:
{
"reward_id": 5
}
Response:
{
"ok": true,
"status": "approved"
}
Process:
- Check user has enough points
- Check stock availability
- Deduct points atomically
- Decrement stock
- Create redemption record
- Auto-apply for avatar types
- Send confirmation email
- For manual rewards: notify admin
Rate Limit: 5 requests per hour
GET /api/v1/engagement/achievements
List all achievements with user progress.
Response:
{
"achievements": [
{
"id": 1,
"code": "first_comment",
"title": "První komentář",
"description": "Napsal/a jste první komentář.",
"points": 10,
"xp": 10,
"achieved": true,
"achieved_at": "2025-10-15T14:30:00Z"
}
],
"counters": {
"comments": 25,
"votes": 18,
"newsletter": true
}
}
GET /api/v1/engagement/leaderboard
Get top users.
Query Params:
metric:points|level|xp(default:points)limit: 1-100 (default: 20)
Response:
{
"items": [
{
"rank": 1,
"user_id": 456,
"first_name": "Jan",
"last_name": "Novák",
"username": "fan-456",
"role": "fan",
"points": 5420,
"level": 28,
"xp": 39200,
"avatar_url": "...",
"animated_avatar_url": null
}
]
}
GET /api/v1/engagement/transactions
Get user's points transaction history.
Query Params:
limit: 1-200 (default: 50)reason: filter by reason
Response:
{
"items": [
{
"id": 789,
"user_id": 123,
"delta": 5,
"xp_delta": 5,
"reason": "comment_create",
"meta": {"comment_id": 42},
"created_at": "2025-11-01T10:15:00Z"
}
]
}
Admin Endpoints
GET /admin/engagement/rewards
List all rewards (including inactive).
Query: ?active=true|false
POST /admin/engagement/rewards
Create a new reward.
PUT /admin/engagement/rewards/:id
Update reward details.
DELETE /admin/engagement/rewards/:id
Delete a reward.
GET /admin/engagement/redemptions
List all redemptions.
Query: ?status=pending|approved|rejected|fulfilled
PATCH /admin/engagement/redemptions/:id
Update redemption status.
Request:
{
"action": "approve" | "reject" | "fulfill"
}
Reject Logic:
- Refunds points to user
- Restores stock
- Logs refund transaction
- Sends notification email
GET /admin/engagement/leaderboard
Admin leaderboard (includes email, higher limits).
GET /admin/engagement/transactions
Admin transaction log.
Query: ?user_id=&reason=&limit=
POST /admin/engagement/adjust
Manually adjust user points.
Request:
{
"user_id": 123,
"delta": 100,
"reason": "admin_adjust",
"meta": {"note": "Compensation for bug"}
}
Note: Admin adjustments affect points only, not XP.
GET /admin/engagement/profile/:user_id
View any user's profile.
Frontend Integration
Services
/frontend/src/services/engagement.ts
Public API client for engagement features.
Functions:
getProfile()patchProfile(body)patchAvatar(body)getRewards()redeemReward(id)getAchievements()getLeaderboard(metric, limit)
/frontend/src/services/admin/engagement.ts
Admin API client.
Functions:
adminListRewards(params)adminCreateReward(body)adminUpdateReward(id, body)adminDeleteReward(id)adminListRedemptions(params)adminUpdateRedemptionStatus(id, action)adminGetLeaderboard(metric, limit)adminListTransactions(params)adminAdjustPoints(body)adminGetUserProfile(user_id)
Utilities
/frontend/src/utils/engagementHelpers.ts
Key Functions:
computeLevelInfo(xp, level)- Calculate level progresscomputeLevelFromXP(xp)- Determine level from XPgetLevelTitle(level)- Get level namegetLevelColor(level)- Get badge colorformatPoints(points)- Format with k/M suffixvalidateUsername(username)- Client-side validationgenerateUsernameSuggestion(first, last)- Auto-suggest username
Pages
/frontend/src/pages/SemiAdminPage.tsx
Fan Zone - User engagement profile dashboard.
Features:
- Profile stats (points, level, XP progress)
- Username editor
- Avatar management (upload, randomize)
- Level badge with colored tier
- Achievements viewer
- Leaderboard integration
- Rewards store
Access: Any authenticated user
/frontend/src/pages/admin/EngagementAdminPage.tsx
Admin Panel - Complete engagement management.
Sections:
- Leaderboards - Top users by points/level/XP
- Create Reward - Form with quick presets
- Rewards List - Edit, delete, toggle active
- Redemptions - Approve/reject/fulfill requests
- Transactions - View and filter all transactions
- Manual Adjustments - Add/remove points
Features:
- Batch reward creation (bulk avatars)
- Image upload for rewards
- Metadata editor for coupons/merch
- Real-time stock management
- Email notifications
Access: Admin only
Points & XP System
Earning Points & XP
| Action | Points | XP | Daily Cap |
|---|---|---|---|
| Comment create | 5 | 5 | 10 comments |
| Comment reaction | 1 | 1 | 20 reactions |
| Poll vote | 3 | 3 | 1 vote |
| Newsletter subscribe | 12 | 12 | Once |
| Achievement unlock | Varies | Varies | - |
Anti-Abuse:
- Daily caps per reason (tracked in
PointsTransaction) - Rate limiting on endpoints
- Spam detection for comments
- Ban system prevents abuse
Spending Points
Points are spent to redeem rewards. XP is never deducted.
Redemption Flow:
- User browses rewards store
- Clicks "Redeem" on affordable item
- System checks: points ≥ cost, stock > 0
- Atomic transaction:
- Deduct points from profile
- Decrement stock
- Create redemption record
- Log transaction
- Auto-apply for avatar types
- Email confirmation
- Admin notification if manual fulfillment needed
Refund on Rejection:
- Admin clicks "Reject" on pending redemption
- System refunds full points
- Restores stock
- Logs refund transaction
- Notifies user
Level Calculation
func ComputeLevel(xp int64) int {
lvl := 1
threshold := int64(100)
remaining := xp
for remaining >= threshold && lvl < 200 {
remaining -= threshold
lvl++
threshold += int64(100)
}
return max(1, lvl)
}
Examples:
- Level 1: 0 XP
- Level 2: 100 XP (100 more)
- Level 3: 300 XP (200 more)
- Level 4: 600 XP (300 more)
- Level 10: 4500 XP
- Level 20: 19000 XP
- Level 50: 122500 XP
Achievements
Built-in Achievements
Defined in migration 20251102000001_create_engagement_system.up.sql:
INSERT INTO achievements (code, title, description, points, xp, active) VALUES
('first_comment', 'První komentář', 'Napsal/a jste první komentář.', 10, 10, TRUE),
('first_vote', 'První hlasování', 'Poprvé jste hlasoval/a v anketě.', 8, 8, TRUE),
('newsletter_sub', 'Odběr novinek', 'Přihlášení k odběru newsletteru.', 12, 12, TRUE),
('comments_10', 'Komentátor', '10 komentářů!', 20, 20, TRUE),
('votes_10', 'Hlasující', '10 hlasování!', 20, 20, TRUE),
('comments_50', 'Aktivní člen', '50 komentářů!', 50, 50, TRUE),
('votes_50', 'Věrný fanoušek', '50 hlasování!', 50, 50, TRUE),
('comments_100', 'Veterán diskuzí', '100 komentářů!', 100, 100, TRUE);
Achievement Checking
Automatically triggered:
- After comment creation
- After poll vote
- After newsletter subscription
- On manual admin points adjustment
Service Method:
func (s *EngagementService) CheckAndAwardAchievements(userID uint) error
Process:
- Load user's completed achievements
- Count relevant actions (comments, votes, newsletter)
- Check each achievement condition
- Award if not already unlocked:
- Create
UserAchievementrecord - Add both points AND xp via
AwardPointsAndXP() - Transaction logged with reason
achievement:CODE
- Create
Adding Custom Achievements
Via SQL:
INSERT INTO achievements (code, title, description, points, xp, icon, active)
VALUES ('super_fan', 'Super Fanoušek', 'Dosáhl/a jste úrovně 50!', 500, 500, '⭐', TRUE);
Logic in Service:
// In CheckAndAwardAchievements
if up.Level >= 50 {
awardByCode("super_fan")
}
Rewards Store
Creating Rewards
Quick Presets (Admin UI):
- Avatar (static) - 50 points
- Avatar (animated) - 100 points
- Merch coupon - 200 points
Batch Creation: Useful for importing avatar packs.
Settings:
- Base URL template:
https://cdn.example.com/avatars/avatar-{i}.png - Count: 10
- Start index: 1
- Generates: avatar-1.png through avatar-10.png
Reward Types
Avatar Static/Animated
Auto-applied on redemption:
avatar_static→ UpdatesUserProfile.avatar_urlavatar_animated→ UpdatesUserProfile.animated_avatar_url- Status:
approved(instant)
Avatar Upload Unlock
Special reward type that unlocks custom upload.
- Cost: typically 100 points
- Stock: -1 (unlimited)
- Sets
UserProfile.avatar_upload_unlocked = true - One per user
Merchandise Coupons
Requires manual fulfillment.
Metadata Example:
{
"coupon_code": "SUPERFAN10",
"expires_at": "2025-12-31",
"discount": "10%",
"note": "Vyzvednout na recepci"
}
Workflow:
- User redeems → Status
pending - Admin reviews → Clicks "Approve"
- Admin delivers → Clicks "Fulfill"
Physical Merchandise
Like coupons but requires shipping.
Metadata:
{
"sku": "TSHIRT-L-RED",
"size": "L",
"color": "Červená"
}
Digital Products
E.g., e-book, wallpaper pack.
Metadata:
{
"download_url": "https://...",
"license_key": "XXXX-YYYY-ZZZZ"
}
Stock Management
Unlimited: stock = -1
Out of stock: stock = 0 (reward hidden to users)
Limited: stock > 0 (decrements on redemption, restores on rejection)
Admin can:
- Update stock inline in rewards table
- Toggle
activeto hide/show without deleting
Security & Anti-Abuse
Rate Limiting
Applied to all engagement endpoints:
- Redeem: 5 requests / hour
- Comment create: 20 / minute
- Poll vote: 60 / minute
- Reactions: 60 / minute
- Unban request: 5 / hour
Daily Caps
Implemented in EngagementService.AwardPointsCapped():
switch reason {
case "poll_vote":
return cnt < 1 // Max 1 per day
case "comment_create":
return cnt < 10 // Max 10 per day
case "comment_reacted":
return cnt < 20 // Max 20 per day
case "newsletter_subscribe":
return cnt == 0 // Once per lifetime
}
Username Validation
Backend (pkg/validation/engagement.go):
- Length: 3-32 characters
- Charset:
[a-z0-9\-_.] - No consecutive specials
- No leading/trailing specials
- Reserved word check
Frontend (utils/engagementHelpers.ts):
Pre-validation with instant feedback.
Points Atomicity
All points operations use database transactions:
tx := ec.DB.Begin()
if res := tx.Model(&models.UserProfile{}).
Where("user_id = ? AND points >= ?", userID, cost).
UpdateColumn("points", gorm.Expr("points - ?", cost));
res.RowsAffected == 0 {
tx.Rollback()
return error
}
tx.Commit()
Prevents:
- Double spending
- Race conditions
- Negative balances
Avatar Upload Security
Users must first unlock via reward redemption.
Check:
if strings.HasPrefix(url, "/uploads/") {
if !up.AvatarUploadUnlocked {
return errors.New("locked")
}
}
External URLs (Dicebear, etc.) allowed without unlock.
Admin Management
Dashboard Features
- Leaderboards - Monitor top performers
- Reward CRUD - Full management interface
- Redemption Queue - Approve/reject/fulfill
- Transaction Log - Audit all point changes
- Manual Adjustments - Add/remove points
Batch Operations
Rewards:
- Create multiple avatars from URL template
- Bulk activate/deactivate
Transactions:
- Filter by user, reason, date
- Export capability (future)
Email Notifications
To Users:
- Reward redeemed confirmation
- Redemption status updates (fulfilled/rejected)
- Achievement unlocked (future)
To Admins:
- New pending redemption alert
- Includes user info and manage link
Templates:
/templates/emails/reward_redeemed_user.html/templates/emails/reward_redeemed_admin.html
Production Checklist
Database
- Run migration
20251102000001_create_engagement_system.up.sql - Verify indexes created
- Default achievements seeded
- Avatar unlock reward created
Backend
- Engagement service implemented
- Controllers with validation
- Routes registered
- Rate limiting applied
- Email templates exist
- Helper functions created
Frontend
- User dashboard (SemiAdminPage)
- Admin panel (EngagementAdminPage)
- Services configured
- Utilities available
- Responsive design
Security
- Username validation (backend + frontend)
- Points atomicity (transactions)
- Rate limits on all endpoints
- Daily caps per action
- Avatar upload gating
- CSRF protection (cookie auth)
- Input sanitization
Testing
- Create test user profile
- Award points for comment
- Redeem avatar reward
- Test level progression
- Unlock achievement
- Admin adjust points
- Approve/reject redemption
- Test daily caps
- Verify email delivery
- Load test leaderboard
Configuration
- Set
SMTP_*environment variables - Configure canonical base URL for emails
- Review default achievement values
- Set initial reward catalog
- Configure avatar upload limits
Monitoring
- Track redemption rate
- Monitor points inflation
- Check for abuse patterns
- Review transaction logs
- Monitor email delivery
Documentation
- Complete API documentation
- User guide for Fan Zone
- Admin guide for management
- Database schema documented
- Helper functions documented
Future Enhancements
Phase 2
- Seasonal events (double XP weekends)
- Team/guild system
- Achievement categories
- Leaderboard seasons
- Profile customization (banners, badges)
Phase 3
- Referral rewards
- Daily login streaks
- Special challenges
- Limited-time rewards
- Trading system (?)
Integration Ideas
- Match prediction rewards
- Attendance check-in points
- Social media sharing bonuses
- Newsletter engagement tracking
Support
For issues or questions:
- Check admin transaction log for debugging
- Review user profile directly in database
- Check email logs for notification delivery
- Verify migration ran successfully
- Consult
/DOCS/for additional guides
Migration Files:
database/migrations/20251102000001_create_engagement_system.up.sqldatabase/migrations/20251102000001_create_engagement_system.down.sql
Key Files:
- Backend:
internal/services/engagement.go - Backend:
internal/controllers/engagement_controller.go - Frontend:
frontend/src/pages/SemiAdminPage.tsx - Frontend:
frontend/src/pages/admin/EngagementAdminPage.tsx - Utils:
frontend/src/utils/engagementHelpers.ts - Validation:
pkg/validation/engagement.go - Helpers:
internal/helpers/engagement_helpers.go
Last Updated: November 2, 2025
Status: Production Ready ✅