dubs-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/.claude/settings.local.json +280 -0
  2. package/CLAUDE.md +46 -0
  3. package/CONNECT4_PRODUCTION_DEPLOY.md +155 -0
  4. package/CURRENT_SESSION.md +171 -0
  5. package/CURRENT_SESSION_DRAW.md +516 -0
  6. package/MARCH_MADNESS_SURVIVOR.md +254 -0
  7. package/PANDA.md +166 -0
  8. package/Procfile +4 -0
  9. package/README.md +476 -0
  10. package/controllers/livescoresController.js +376 -0
  11. package/controllers/pickemController.js +554 -0
  12. package/controllers/survivorAdminController.js +887 -0
  13. package/controllers/survivorController.js +623 -0
  14. package/cron/oracleMonitor.js +77 -0
  15. package/cron/pickemOracleMonitor.js +73 -0
  16. package/data/jackpot-history.json +952 -0
  17. package/data/ncaaTeams.js +406 -0
  18. package/documentation/API_SECURITY_GUIDE.md +327 -0
  19. package/documentation/ARCADE_API.md +593 -0
  20. package/documentation/ARCADE_IMPLEMENTATION_SUMMARY.md +399 -0
  21. package/documentation/ARCADE_QUICKSTART.md +242 -0
  22. package/documentation/AUTOMATIC_MODE_ORACLE.md +321 -0
  23. package/documentation/BUG_FIX_COHORT_DATE_DISPLAY.md +171 -0
  24. package/documentation/CLAIM_MIGRATION_INSTRUCTIONS.md +52 -0
  25. package/documentation/CLAIM_STATUS_FIX.md +67 -0
  26. package/documentation/CLI_TOOL_GUIDE.md +372 -0
  27. package/documentation/COHORT_RETENTION_ANALYSIS.md +295 -0
  28. package/documentation/COHORT_RETENTION_IMPLEMENTATION_COMPLETE.md +461 -0
  29. package/documentation/COHORT_RETENTION_SUMMARY.md +204 -0
  30. package/documentation/COMPLETE_PROJECT_SUMMARY.md +490 -0
  31. package/documentation/DATABASE_QUERIES.md +269 -0
  32. package/documentation/DATABASE_RETENTION_POLICY.md +390 -0
  33. package/documentation/DATABASE_SETUP_GUIDE.md +361 -0
  34. package/documentation/DATABASE_SETUP_SUMMARY.md +247 -0
  35. package/documentation/DEMO_API_CURL_COMMANDS.md +656 -0
  36. package/documentation/DEPLOYMENT_SUMMARY.txt +100 -0
  37. package/documentation/DUPLICATE_NOTIFICATIONS_FIXED.md +201 -0
  38. package/documentation/EXCHANGE_RATES_INTEGRATION.md +371 -0
  39. package/documentation/FINAL_API_PROTECTION_TABLE.md +175 -0
  40. package/documentation/GAME_START_NOTIFICATIONS_DEPLOYMENT.md +256 -0
  41. package/documentation/GAME_START_NOTIFICATIONS_INTEGRATION.md +275 -0
  42. package/documentation/HEROKU_DEPLOYMENT.md +134 -0
  43. package/documentation/HEROKU_SCHEDULER_SETUP.md +271 -0
  44. package/documentation/JACKPOT_API.md +521 -0
  45. package/documentation/JACKPOT_DEPLOYMENT_GUIDE.md +362 -0
  46. package/documentation/JWT_IMPLEMENTATION_SUMMARY.md +373 -0
  47. package/documentation/JWT_QUICK_SETUP.md +268 -0
  48. package/documentation/JWT_TESTING_GUIDE.md +404 -0
  49. package/documentation/KEEPER_RECOVERY_GUIDE.md +381 -0
  50. package/documentation/KEEPER_SETUP.md +206 -0
  51. package/documentation/KEEPER_STATE_MACHINE.md +423 -0
  52. package/documentation/LATEST_PRODUCTION_SETUP.md +387 -0
  53. package/documentation/LOCAL_VOTING_TEST.md +279 -0
  54. package/documentation/ORACLE_FIXES_SUMMARY.md +188 -0
  55. package/documentation/ORACLE_POSTGRESQL_UPDATE.md +202 -0
  56. package/documentation/PAYMENT_DEPLOYMENT.md +209 -0
  57. package/documentation/PNL_TRACKING_SETUP.md +189 -0
  58. package/documentation/PREVENTING_LOCKUP_ERRORS.md +472 -0
  59. package/documentation/PRODUCTION_READY_SUMMARY.md +227 -0
  60. package/documentation/PUBLIC_VS_PRIVATE_ENDPOINTS.md +278 -0
  61. package/documentation/QUICK_AUTH_SETUP.md +99 -0
  62. package/documentation/QUICK_DEPLOY.md +224 -0
  63. package/documentation/QUICK_FIX.md +114 -0
  64. package/documentation/QUICK_START.md +152 -0
  65. package/documentation/REFEREE_MODE_GUIDE.md +392 -0
  66. package/documentation/RETENTION_CORE_ACTION_UPDATE.md +313 -0
  67. package/documentation/RETENTION_UPDATE_SUMMARY.md +108 -0
  68. package/documentation/RUN_MIGRATION_NOW.md +39 -0
  69. package/documentation/SCRIPTS_UPDATE_SUMMARY.md +251 -0
  70. package/documentation/SETUP_GUIDE.md +184 -0
  71. package/documentation/STATE_MACHINE_IMPLEMENTATION.md +250 -0
  72. package/documentation/TELEGRAM_NOTIFICATIONS_DIAGNOSIS.md +361 -0
  73. package/documentation/UNIFIED_ARCHITECTURE.md +231 -0
  74. package/documentation/VOTING_DEPLOYMENT_SUMMARY.md +392 -0
  75. package/documentation/WEBSOCKET_ARCHITECTURE.md +881 -0
  76. package/documentation/WHAT_WE_BUILT_TODAY.md +369 -0
  77. package/documentation/latest/LATEST_PRODUCTION_SETUP.md +865 -0
  78. package/ecosystem.config.js +65 -0
  79. package/env.template +125 -0
  80. package/middleware/apiKeyAuth.js +136 -0
  81. package/middleware/authenticate.js +214 -0
  82. package/middleware/developerUserAuth.js +76 -0
  83. package/middleware/socketAuth.js +69 -0
  84. package/package.json +49 -0
  85. package/postman/Dubs-API-v1-With-Voting.postman_collection.json +555 -0
  86. package/postman/Dubs-API-v1.postman_collection.json +205 -0
  87. package/postman/Dubs_Developer_API.postman_collection.json +662 -0
  88. package/postman/QUICKSTART.md +118 -0
  89. package/postman/QUICK_REFERENCE.md +246 -0
  90. package/postman/README.md +71 -0
  91. package/postman/VOTING_API_GUIDE.md +426 -0
  92. package/refactor/Animations.md +148 -0
  93. package/refactor/Chat.md +252 -0
  94. package/routes/actionsRoutes.js +699 -0
  95. package/routes/adminRoutes.js +370 -0
  96. package/routes/analyticsRoutes.js +1262 -0
  97. package/routes/arcadeRoutes.js +557 -0
  98. package/routes/authRoutes.js +2310 -0
  99. package/routes/avatarRoutes.js +85 -0
  100. package/routes/botRoutes.js +211 -0
  101. package/routes/chatRoutes.js +377 -0
  102. package/routes/cryptoPriceRoutes.js +105 -0
  103. package/routes/developerRoutes.js +4201 -0
  104. package/routes/deviceRoutes.js +214 -0
  105. package/routes/dmRoutes.js +167 -0
  106. package/routes/esportsRoutes.js +806 -0
  107. package/routes/exchangeRateRoutes.js +233 -0
  108. package/routes/gamesRoutes.js +3028 -0
  109. package/routes/jackpotRoutes.js +754 -0
  110. package/routes/keeperMonitoringRoutes.js +156 -0
  111. package/routes/keeperWebhookRoutes.js +466 -0
  112. package/routes/livescoresRoutes.js +31 -0
  113. package/routes/pickemAdminRoutes.js +199 -0
  114. package/routes/pickemRoutes.js +231 -0
  115. package/routes/playerStatsRoutes.js +147 -0
  116. package/routes/portfolioRoutes.js +217 -0
  117. package/routes/promoRoutes.js +418 -0
  118. package/routes/referralEarningsRoutes.js +392 -0
  119. package/routes/socialRoutes.js +459 -0
  120. package/routes/sportsRoutes.js +1271 -0
  121. package/routes/survivorAdminRoutes.js +345 -0
  122. package/routes/survivorRoutes.js +756 -0
  123. package/routes/uploadRoutes.js +256 -0
  124. package/routes/userProfileRoutes.js +244 -0
  125. package/routes/whatsNewRoutes.js +331 -0
  126. package/scripts/.claude/settings.local.json +15 -0
  127. package/scripts/README.md +170 -0
  128. package/scripts/RESTART_EVERYTHING.sh +104 -0
  129. package/scripts/add-claim-columns.sql +48 -0
  130. package/scripts/add-crypto-prices-cache.sql +27 -0
  131. package/scripts/add-exchange-rates-cache.sql +40 -0
  132. package/scripts/add-game-invite-column.sql +23 -0
  133. package/scripts/add-game-invite-notification.sql +33 -0
  134. package/scripts/add-game-invite-telegram-pref.sql +16 -0
  135. package/scripts/add-game-joined-notification.sql +16 -0
  136. package/scripts/add-game-joined-pref.js +40 -0
  137. package/scripts/add-game-joined-preference.sql +6 -0
  138. package/scripts/add-game-start-notifications.sql +41 -0
  139. package/scripts/add-notification-flags-to-games.sql +55 -0
  140. package/scripts/add-pending-game-dismissals.sql +19 -0
  141. package/scripts/add-preferred-currency.sql +34 -0
  142. package/scripts/add-winner-columns.js +61 -0
  143. package/scripts/add_mention_system.sql +53 -0
  144. package/scripts/add_payment_system.sql +96 -0
  145. package/scripts/add_sports_event_id_column.sql +22 -0
  146. package/scripts/analyze-cohort-data-heroku.js +276 -0
  147. package/scripts/analyze-cohort-data.js +295 -0
  148. package/scripts/analyze-prod-cohorts.sh +10 -0
  149. package/scripts/backfill-matchup-images.js +245 -0
  150. package/scripts/backfill-missing-signatures.js +175 -0
  151. package/scripts/backfill-referral-earnings.js +202 -0
  152. package/scripts/check-chat-schema.js +130 -0
  153. package/scripts/check-db.sh +14 -0
  154. package/scripts/check_oracle_in_game.js +54 -0
  155. package/scripts/cleanup-database.js +193 -0
  156. package/scripts/clear-notification-cache.js +85 -0
  157. package/scripts/convert-mnemonic.js +50 -0
  158. package/scripts/create-users-table.sql +44 -0
  159. package/scripts/debug-cohort-counts.js +248 -0
  160. package/scripts/debug-winner-calc.js +84 -0
  161. package/scripts/deploy-payment-system.sh +118 -0
  162. package/scripts/deploy-to-heroku.sh +63 -0
  163. package/scripts/diagnose-locked-round.js +143 -0
  164. package/scripts/dubs-cli.js +720 -0
  165. package/scripts/dump-account.js +65 -0
  166. package/scripts/find-vrf-offset.js +48 -0
  167. package/scripts/fix-chat-notifications-constraint.sql +122 -0
  168. package/scripts/fix-claim-columns.js +124 -0
  169. package/scripts/fix-constraint-now.js +44 -0
  170. package/scripts/fix-lock-timestamps.js +96 -0
  171. package/scripts/fix-locked-round.sh +126 -0
  172. package/scripts/fix-missing-badges.sql +91 -0
  173. package/scripts/fix-payment-notifications.sql +41 -0
  174. package/scripts/force-new-round.js +55 -0
  175. package/scripts/force-resolve-and-claim.js +278 -0
  176. package/scripts/important/README.md +115 -0
  177. package/scripts/important/authority-force-lock.js +197 -0
  178. package/scripts/important/authority-resolve-game.js +267 -0
  179. package/scripts/important/check-game-status.js +373 -0
  180. package/scripts/important/list-pending-games-by-version.js +270 -0
  181. package/scripts/important/reconcile-v1-v2-payouts.js +270 -0
  182. package/scripts/initialize-jackpot.js +111 -0
  183. package/scripts/jackpot/.claude/settings.local.json +10 -0
  184. package/scripts/jackpot/force-reset.js +84 -0
  185. package/scripts/jackpot/initialize-mainnet.js +100 -0
  186. package/scripts/jackpot/keeper.js +742 -0
  187. package/scripts/jackpot/status.js +107 -0
  188. package/scripts/jackpot/update-round-duration.js +143 -0
  189. package/scripts/keeper-bot.js +112 -0
  190. package/scripts/list-pending-games.js +131 -0
  191. package/scripts/migrate-chat-v2.js +127 -0
  192. package/scripts/migrate-chat-winners.js +84 -0
  193. package/scripts/migrate-chat.sh +17 -0
  194. package/scripts/migrate-game-invite.js +83 -0
  195. package/scripts/migrate-heroku-game-notifications.sh +159 -0
  196. package/scripts/migrations/001_analytics_tables.sql +422 -0
  197. package/scripts/migrations/002_add_matchup_image_url.sql +14 -0
  198. package/scripts/migrations/003_referral_earnings.sql +208 -0
  199. package/scripts/migrations/004_add_whats_new_notification_type.sql +62 -0
  200. package/scripts/migrations/005_add_connect4_your_turn_notification.sql +61 -0
  201. package/scripts/migrations/005_push_notifications.sql +55 -0
  202. package/scripts/migrations/006_add_draw_team_players.sql +28 -0
  203. package/scripts/migrations/006_add_game_cancelled_notification.sql +62 -0
  204. package/scripts/migrations/007_add_gif_url.sql +8 -0
  205. package/scripts/migrations/008_add_connect4_columns.sql +139 -0
  206. package/scripts/migrations/008_add_pool_tracking.sql +22 -0
  207. package/scripts/migrations/009_create_survivor_pool_tables.sql +174 -0
  208. package/scripts/migrations/010_add_survivor_pool_outcome.sql +28 -0
  209. package/scripts/migrations/011_create_developer_tables.sql +67 -0
  210. package/scripts/migrations/011_fix_keeper_tables.sql +85 -0
  211. package/scripts/migrations/012_create_developer_webhooks.sql +31 -0
  212. package/scripts/migrations/013_add_network_mode.sql +18 -0
  213. package/scripts/migrations/014_create_developer_app_users.sql +19 -0
  214. package/scripts/migrations/015_add_ui_config.sql +4 -0
  215. package/scripts/migrations/016_add_resolution_secret.sql +4 -0
  216. package/scripts/migrations/017_add_external_game_id.sql +3 -0
  217. package/scripts/migrations/018_create_pickem_tables.sql +115 -0
  218. package/scripts/migrations/019_expo_push_tokens.sql +19 -0
  219. package/scripts/migrations/create_whats_new_tables.sql +88 -0
  220. package/scripts/migrations/drop_live_games_tables.sql +34 -0
  221. package/scripts/open-jackpot-round.js +85 -0
  222. package/scripts/purge-all-data.sh +329 -0
  223. package/scripts/purge-all-data.sql +142 -0
  224. package/scripts/purge-heroku-data.sh +149 -0
  225. package/scripts/purge-heroku-data.sql +62 -0
  226. package/scripts/rebuild-heroku-database.sh +113 -0
  227. package/scripts/recover-funds.js +357 -0
  228. package/scripts/regenerate-epl-images.js +278 -0
  229. package/scripts/resize-s3-matchup-images.js +374 -0
  230. package/scripts/resolve-direct.js +88 -0
  231. package/scripts/resolve-mock-game.js +124 -0
  232. package/scripts/resolve-pickem-game.js +55 -0
  233. package/scripts/resolve-round-manual.js +83 -0
  234. package/scripts/resolve-stuck-game.js +382 -0
  235. package/scripts/resolve-stuck-round.js +42 -0
  236. package/scripts/run-connect4-migration.sh +16 -0
  237. package/scripts/run-mention-migration.sh +32 -0
  238. package/scripts/run-payment-migration.sh +51 -0
  239. package/scripts/run-preferred-currency-migration.sh +31 -0
  240. package/scripts/run-referral-earnings-migration.sh +32 -0
  241. package/scripts/run-survivor-outcome-migration.sh +16 -0
  242. package/scripts/seed-test-users.js +346 -0
  243. package/scripts/setup-auth-tables.js +78 -0
  244. package/scripts/setup-complete-database.sql +992 -0
  245. package/scripts/setup-database-fresh.sh +359 -0
  246. package/scripts/setup-heroku-keeper.sh +48 -0
  247. package/scripts/setup-keeper-database.js +83 -0
  248. package/scripts/setup-keeper-state-db.sql +110 -0
  249. package/scripts/setup-oracle.sh +39 -0
  250. package/scripts/setup-pnl-tracking.js +111 -0
  251. package/scripts/start-devnet.sh +14 -0
  252. package/scripts/test-arcade-devnet.sh +160 -0
  253. package/scripts/test-arcade-match.sh +109 -0
  254. package/scripts/test-automatic-mode.sh +239 -0
  255. package/scripts/test-connect4-cancel-claim.js +370 -0
  256. package/scripts/test-connect4-e2e.js +369 -0
  257. package/scripts/test-connect4-resolve.js +369 -0
  258. package/scripts/test-game-state-endpoint.js +136 -0
  259. package/scripts/test-invite-notification.js +86 -0
  260. package/scripts/test-jackpot-api.sh +71 -0
  261. package/scripts/test-poll-confirmation.js +267 -0
  262. package/scripts/test-resolve-game.js +271 -0
  263. package/scripts/test-resolve-signature.js +223 -0
  264. package/scripts/test-signature-preservation.js +124 -0
  265. package/scripts/test-state-machine.js +291 -0
  266. package/scripts/test-webhook-receiver.js +60 -0
  267. package/scripts/update-notification-constraint.js +52 -0
  268. package/scripts/verify-account-layout.js +145 -0
  269. package/scripts/verify-winner-algorithm.js +278 -0
  270. package/server.js +5259 -0
  271. package/services/arcadeMatchService.js +763 -0
  272. package/services/automaticGameOracle.js +1596 -0
  273. package/services/chatService.js +1612 -0
  274. package/services/connect4GameService.js +1049 -0
  275. package/services/connect4NotificationService.js +374 -0
  276. package/services/cryptoPriceService.js +223 -0
  277. package/services/customGameResolver.js +260 -0
  278. package/services/db.js +79 -0
  279. package/services/directMessageService.js +389 -0
  280. package/services/discordNotifications.js +160 -0
  281. package/services/exchangeRateService.js +289 -0
  282. package/services/expoPushService.js +314 -0
  283. package/services/gamesCacheService.js +539 -0
  284. package/services/jackpotHistory.js +331 -0
  285. package/services/jackpotService.js +856 -0
  286. package/services/keeperStateService.js +355 -0
  287. package/services/matchupImageService.js +591 -0
  288. package/services/notificationCacheService.js +407 -0
  289. package/services/pickemOracle.js +440 -0
  290. package/services/playerStatsService.js +389 -0
  291. package/services/portfolioService.js +555 -0
  292. package/services/promoService.js +757 -0
  293. package/services/promoTreasuryService.js +239 -0
  294. package/services/pushNotifications.js +353 -0
  295. package/services/redisService.js +422 -0
  296. package/services/referralEarningsService.js +728 -0
  297. package/services/s3Service.js +396 -0
  298. package/services/socialService.js +1202 -0
  299. package/services/survivorOracle.js +469 -0
  300. package/services/survivorSimulator.js +475 -0
  301. package/services/telegramNotifications.js +461 -0
  302. package/services/userProfileStatsService.js +1185 -0
  303. package/services/whatsNewService.js +388 -0
  304. package/utils/urlHelper.js +95 -0
@@ -0,0 +1,992 @@
1
+ -- ============================================
2
+ -- 🎮 DUBS SERVER - COMPLETE DATABASE SCHEMA
3
+ -- ============================================
4
+ -- This script creates ALL tables needed for the DUBS server
5
+ -- Safe to run multiple times (uses IF NOT EXISTS)
6
+ -- Run with: psql -d dubs_db -f scripts/setup-complete-database.sql
7
+
8
+ -- ============================================
9
+ -- 1. AUTHENTICATION & USERS
10
+ -- ============================================
11
+
12
+ -- Users table (main user profiles)
13
+ CREATE TABLE IF NOT EXISTS users (
14
+ id SERIAL PRIMARY KEY,
15
+ wallet_address VARCHAR(44) UNIQUE NOT NULL,
16
+ email VARCHAR(255),
17
+ username VARCHAR(50) NOT NULL,
18
+ avatar TEXT,
19
+ referral_code VARCHAR(50),
20
+ my_referral_code VARCHAR(50),
21
+ signature TEXT,
22
+ onboarding_complete BOOLEAN DEFAULT false,
23
+ telegram_user_id BIGINT UNIQUE,
24
+ telegram_username VARCHAR(255),
25
+ telegram_first_name VARCHAR(255),
26
+ telegram_last_name VARCHAR(255),
27
+ telegram_photo_url TEXT,
28
+ telegram_connected_at TIMESTAMP,
29
+ preferred_currency VARCHAR(3) DEFAULT 'USD',
30
+ created_at TIMESTAMP DEFAULT NOW(),
31
+ updated_at TIMESTAMP DEFAULT NOW()
32
+ );
33
+
34
+ -- Auth nonces (for signature verification)
35
+ CREATE TABLE IF NOT EXISTS auth_nonces (
36
+ wallet_address VARCHAR(44) PRIMARY KEY,
37
+ nonce VARCHAR(64) NOT NULL,
38
+ expires_at TIMESTAMP NOT NULL,
39
+ used BOOLEAN DEFAULT false,
40
+ created_at TIMESTAMP DEFAULT NOW()
41
+ );
42
+
43
+ -- User sessions (JWT token management)
44
+ CREATE TABLE IF NOT EXISTS user_sessions (
45
+ id SERIAL PRIMARY KEY,
46
+ wallet_address VARCHAR(100) NOT NULL,
47
+ user_id INTEGER,
48
+ token_hash VARCHAR(64) NOT NULL,
49
+ expires_at TIMESTAMP NOT NULL,
50
+ created_at TIMESTAMP DEFAULT NOW(),
51
+ last_activity TIMESTAMP DEFAULT NOW()
52
+ );
53
+
54
+ -- User badges (achievements)
55
+ CREATE TABLE IF NOT EXISTS user_badges (
56
+ id SERIAL PRIMARY KEY,
57
+ user_id INTEGER NOT NULL,
58
+ badge_type VARCHAR(50) NOT NULL,
59
+ badge_name VARCHAR(100) NOT NULL,
60
+ badge_description TEXT,
61
+ badge_icon TEXT,
62
+ referral_count INTEGER,
63
+ earned_at TIMESTAMP DEFAULT NOW(),
64
+ created_at TIMESTAMP DEFAULT NOW(),
65
+ UNIQUE(user_id, badge_type)
66
+ );
67
+
68
+ -- Telegram notification preferences
69
+ CREATE TABLE IF NOT EXISTS telegram_notification_preferences (
70
+ user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
71
+ notify_reply BOOLEAN DEFAULT true,
72
+ notify_reaction BOOLEAN DEFAULT true,
73
+ notify_friend_request BOOLEAN DEFAULT true,
74
+ notify_friend_request_accepted BOOLEAN DEFAULT true,
75
+ notify_friend_request_declined BOOLEAN DEFAULT true,
76
+ notify_referral BOOLEAN DEFAULT true,
77
+ notify_mention BOOLEAN DEFAULT true,
78
+ notify_friend_message BOOLEAN DEFAULT true,
79
+ notify_game_joined BOOLEAN DEFAULT true,
80
+ notify_game_invite BOOLEAN DEFAULT true,
81
+ created_at TIMESTAMP DEFAULT NOW(),
82
+ updated_at TIMESTAMP DEFAULT NOW()
83
+ );
84
+
85
+ -- Push notification subscriptions (for PWA/seeker mode)
86
+ CREATE TABLE IF NOT EXISTS push_subscriptions (
87
+ id SERIAL PRIMARY KEY,
88
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
89
+ endpoint TEXT NOT NULL,
90
+ p256dh TEXT NOT NULL,
91
+ auth TEXT NOT NULL,
92
+ device_type VARCHAR(50) DEFAULT 'android_pwa',
93
+ created_at TIMESTAMP DEFAULT NOW(),
94
+ updated_at TIMESTAMP DEFAULT NOW(),
95
+ UNIQUE(user_id, endpoint)
96
+ );
97
+
98
+ -- Push notification preferences (mirrors telegram_notification_preferences)
99
+ CREATE TABLE IF NOT EXISTS push_notification_preferences (
100
+ user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
101
+ notify_reply BOOLEAN DEFAULT true,
102
+ notify_reaction BOOLEAN DEFAULT true,
103
+ notify_friend_request BOOLEAN DEFAULT true,
104
+ notify_friend_request_accepted BOOLEAN DEFAULT true,
105
+ notify_friend_request_declined BOOLEAN DEFAULT true,
106
+ notify_referral BOOLEAN DEFAULT true,
107
+ notify_mention BOOLEAN DEFAULT true,
108
+ notify_friend_message BOOLEAN DEFAULT true,
109
+ notify_game_joined BOOLEAN DEFAULT true,
110
+ notify_game_invite BOOLEAN DEFAULT true,
111
+ created_at TIMESTAMP DEFAULT NOW(),
112
+ updated_at TIMESTAMP DEFAULT NOW()
113
+ );
114
+
115
+ -- ============================================
116
+ -- 2. CHAT SYSTEM
117
+ -- ============================================
118
+
119
+ -- Chat messages (main chat table)
120
+ CREATE TABLE IF NOT EXISTS chat_messages (
121
+ id SERIAL PRIMARY KEY,
122
+ user_id INTEGER,
123
+ wallet_address VARCHAR(100) NOT NULL,
124
+ username VARCHAR(100),
125
+ avatar TEXT,
126
+ message TEXT NOT NULL,
127
+ reply_to_id INTEGER,
128
+ is_winner_announcement BOOLEAN DEFAULT FALSE,
129
+ win_amount NUMERIC(20, 9),
130
+ round_id INTEGER,
131
+ game_invite_metadata JSONB,
132
+ edited BOOLEAN DEFAULT FALSE,
133
+ edited_at TIMESTAMP,
134
+ deleted BOOLEAN DEFAULT FALSE,
135
+ deleted_at TIMESTAMP,
136
+ timestamp TIMESTAMP DEFAULT NOW(),
137
+ created_at TIMESTAMP DEFAULT NOW()
138
+ );
139
+
140
+ -- Chat reactions (emojis on messages)
141
+ CREATE TABLE IF NOT EXISTS chat_reactions (
142
+ id SERIAL PRIMARY KEY,
143
+ message_id INTEGER,
144
+ user_id INTEGER,
145
+ wallet_address VARCHAR(100) NOT NULL,
146
+ reaction VARCHAR(20) NOT NULL,
147
+ created_at TIMESTAMP DEFAULT NOW(),
148
+ UNIQUE(message_id, user_id, reaction)
149
+ );
150
+
151
+ -- User relationships (friends/blocks)
152
+ CREATE TABLE IF NOT EXISTS user_relationships (
153
+ id SERIAL PRIMARY KEY,
154
+ user_id INTEGER,
155
+ target_user_id INTEGER,
156
+ relationship_type VARCHAR(20) NOT NULL CHECK (relationship_type IN ('friend', 'block')),
157
+ created_at TIMESTAMP DEFAULT NOW(),
158
+ UNIQUE(user_id, target_user_id)
159
+ );
160
+
161
+ -- Chat notifications
162
+ CREATE TABLE IF NOT EXISTS chat_notifications (
163
+ id SERIAL PRIMARY KEY,
164
+ user_id INTEGER,
165
+ message_id INTEGER,
166
+ sender_user_id INTEGER,
167
+ notification_type VARCHAR(30) NOT NULL CHECK (notification_type IN ('reply', 'mention', 'friend_message', 'reaction', 'friend_request', 'friend_request_accepted', 'friend_request_declined', 'referral', 'game_joined', 'game_invite', 'game_starting_soon', 'game_starting_now', 'game_won', 'game_lost', 'payment_received', 'payment_sent', 'dm', 'dm_message', 'whats_new')),
168
+ notification_data JSONB,
169
+ read BOOLEAN DEFAULT FALSE,
170
+ created_at TIMESTAMP DEFAULT NOW()
171
+ );
172
+
173
+ -- Message mentions (@mention system)
174
+ CREATE TABLE IF NOT EXISTS message_mentions (
175
+ id SERIAL PRIMARY KEY,
176
+ message_id INTEGER NOT NULL REFERENCES chat_messages(id) ON DELETE CASCADE,
177
+ mentioned_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
178
+ created_at TIMESTAMP DEFAULT NOW(),
179
+ UNIQUE(message_id, mentioned_user_id)
180
+ );
181
+
182
+ -- Chat payments (SOL payment tracking)
183
+ CREATE TABLE IF NOT EXISTS chat_payments (
184
+ id SERIAL PRIMARY KEY,
185
+ message_id INTEGER REFERENCES chat_messages(id) ON DELETE CASCADE,
186
+ sender_user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
187
+ sender_wallet VARCHAR(100) NOT NULL,
188
+ recipient_user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
189
+ recipient_wallet VARCHAR(100) NOT NULL,
190
+ amount_sol NUMERIC(20, 9) NOT NULL,
191
+ transaction_signature VARCHAR(200) NOT NULL UNIQUE,
192
+ status VARCHAR(20) NOT NULL CHECK (status IN ('pending', 'confirmed', 'failed')),
193
+ error_message TEXT,
194
+ created_at TIMESTAMP DEFAULT NOW(),
195
+ updated_at TIMESTAMP DEFAULT NOW()
196
+ );
197
+
198
+ -- ============================================
199
+ -- 3. GAMES SYSTEM (Sports Betting)
200
+ -- ============================================
201
+
202
+ -- Games table (shared game data)
203
+ CREATE TABLE IF NOT EXISTS games (
204
+ id SERIAL PRIMARY KEY,
205
+ game_id VARCHAR(255) UNIQUE NOT NULL,
206
+ game_address VARCHAR(255) NOT NULL,
207
+ title VARCHAR(500),
208
+ image_url TEXT,
209
+ matchup_image_url TEXT, -- Pre-generated S3 matchup image URL
210
+ game_type VARCHAR(50),
211
+ buy_in NUMERIC(20, 9),
212
+ max_players INTEGER DEFAULT 0,
213
+ game_mode INTEGER,
214
+ created_by VARCHAR(255) NOT NULL,
215
+ sports_event JSONB,
216
+ home_team_players TEXT[] DEFAULT '{}',
217
+ away_team_players TEXT[] DEFAULT '{}',
218
+ draw_team_players TEXT[] DEFAULT '{}', -- For EPL/soccer draw betting
219
+ lock_timestamp BIGINT,
220
+ is_locked BOOLEAN DEFAULT false,
221
+ is_resolved BOOLEAN DEFAULT false,
222
+ automatic_status VARCHAR(50),
223
+ lock_notification_sent_10min BOOLEAN DEFAULT false,
224
+ lock_notification_sent_now BOOLEAN DEFAULT false,
225
+ -- Connect4 specific columns
226
+ game_status VARCHAR(50), -- waiting, playing, completed, cancelled
227
+ connect4_board JSONB, -- 6x7 2D array board state
228
+ connect4_current_turn VARCHAR(10), -- home or away
229
+ connect4_winner VARCHAR(10), -- home, away, draw, or NULL
230
+ connect4_winning_cells JSONB, -- Array of {row, col} winning cells
231
+ claim_signature TEXT, -- On-chain claim transaction signature
232
+ completed_at TIMESTAMP, -- When game ended
233
+ invited_player VARCHAR(255), -- Wallet for private game invites
234
+ created_at TIMESTAMP DEFAULT NOW(),
235
+ updated_at TIMESTAMP DEFAULT NOW()
236
+ );
237
+
238
+ -- User game refs table (user-specific game data)
239
+ CREATE TABLE IF NOT EXISTS user_game_refs (
240
+ id SERIAL PRIMARY KEY,
241
+ wallet_address VARCHAR(255) NOT NULL,
242
+ game_id VARCHAR(255) NOT NULL,
243
+ role VARCHAR(50),
244
+ joined_at TIMESTAMP,
245
+ team_choice VARCHAR(10),
246
+ my_signature VARCHAR(255),
247
+ my_explorer_url TEXT,
248
+ status VARCHAR(50),
249
+ wallet_type VARCHAR(50),
250
+ claimed_at TIMESTAMP,
251
+ claim_signature TEXT,
252
+ claim_explorer_url TEXT,
253
+ amount_claimed NUMERIC(20, 9),
254
+ created_at TIMESTAMP DEFAULT NOW(),
255
+ updated_at TIMESTAMP DEFAULT NOW(),
256
+ UNIQUE(wallet_address, game_id)
257
+ );
258
+
259
+ -- Audit logs table
260
+ CREATE TABLE IF NOT EXISTS audit_logs (
261
+ id SERIAL PRIMARY KEY,
262
+ log_type VARCHAR(100),
263
+ method VARCHAR(100),
264
+ user_id VARCHAR(255),
265
+ metadata JSONB,
266
+ created_at TIMESTAMP DEFAULT NOW()
267
+ );
268
+
269
+ -- ============================================
270
+ -- 4. SOCIAL FEATURES
271
+ -- ============================================
272
+
273
+ -- Friends table
274
+ CREATE TABLE IF NOT EXISTS friends (
275
+ id SERIAL PRIMARY KEY,
276
+ user_wallet VARCHAR(100) NOT NULL,
277
+ friend_wallet VARCHAR(100) NOT NULL,
278
+ status VARCHAR(20) DEFAULT 'pending',
279
+ created_at TIMESTAMP DEFAULT NOW(),
280
+ updated_at TIMESTAMP DEFAULT NOW(),
281
+ UNIQUE(user_wallet, friend_wallet)
282
+ );
283
+
284
+ -- Friend requests
285
+ CREATE TABLE IF NOT EXISTS friend_requests (
286
+ id SERIAL PRIMARY KEY,
287
+ from_user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
288
+ to_user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
289
+ status VARCHAR(20) NOT NULL CHECK (status IN ('pending', 'accepted', 'rejected')) DEFAULT 'pending',
290
+ created_at TIMESTAMP DEFAULT NOW(),
291
+ updated_at TIMESTAMP DEFAULT NOW()
292
+ );
293
+
294
+ -- Add unique constraint for friend_requests if it doesn't exist
295
+ DO $$
296
+ BEGIN
297
+ IF NOT EXISTS (
298
+ SELECT 1 FROM pg_constraint
299
+ WHERE conname = 'friend_requests_from_user_id_to_user_id_key'
300
+ ) THEN
301
+ ALTER TABLE friend_requests
302
+ ADD CONSTRAINT friend_requests_from_user_id_to_user_id_key
303
+ UNIQUE (from_user_id, to_user_id);
304
+ END IF;
305
+ END $$;
306
+
307
+ -- Groups (group chats)
308
+ CREATE TABLE IF NOT EXISTS groups (
309
+ id SERIAL PRIMARY KEY,
310
+ name VARCHAR(100) NOT NULL,
311
+ description TEXT,
312
+ creator_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
313
+ created_at TIMESTAMP DEFAULT NOW()
314
+ );
315
+
316
+ -- Group members
317
+ CREATE TABLE IF NOT EXISTS group_members (
318
+ id SERIAL PRIMARY KEY,
319
+ group_id INTEGER REFERENCES groups(id) ON DELETE CASCADE,
320
+ user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
321
+ role VARCHAR(20) DEFAULT 'member',
322
+ joined_at TIMESTAMP DEFAULT NOW()
323
+ );
324
+
325
+ -- Add unique constraint for group_members if it doesn't exist
326
+ DO $$
327
+ BEGIN
328
+ IF NOT EXISTS (
329
+ SELECT 1 FROM pg_constraint
330
+ WHERE conname = 'group_members_group_id_user_id_key'
331
+ ) THEN
332
+ ALTER TABLE group_members
333
+ ADD CONSTRAINT group_members_group_id_user_id_key
334
+ UNIQUE (group_id, user_id);
335
+ END IF;
336
+ END $$;
337
+
338
+ -- ============================================
339
+ -- 5. PLAYER STATS & TRACKING
340
+ -- ============================================
341
+
342
+ -- Player stats (overall performance)
343
+ CREATE TABLE IF NOT EXISTS player_stats (
344
+ wallet_address VARCHAR(100) PRIMARY KEY,
345
+ total_wagered NUMERIC(20, 9) DEFAULT 0,
346
+ total_won NUMERIC(20, 9) DEFAULT 0,
347
+ net_pnl NUMERIC(20, 9) DEFAULT 0,
348
+ rounds_played INTEGER DEFAULT 0,
349
+ rounds_won INTEGER DEFAULT 0,
350
+ biggest_win NUMERIC(20, 9) DEFAULT 0,
351
+ biggest_win_round INTEGER,
352
+ last_played TIMESTAMP,
353
+ created_at TIMESTAMP DEFAULT NOW()
354
+ );
355
+
356
+ -- Player history (individual transactions)
357
+ CREATE TABLE IF NOT EXISTS player_history (
358
+ id SERIAL PRIMARY KEY,
359
+ wallet_address VARCHAR(100) NOT NULL,
360
+ round_id INTEGER NOT NULL,
361
+ action VARCHAR(20) NOT NULL,
362
+ amount NUMERIC(20, 9) NOT NULL,
363
+ cumulative_pnl NUMERIC(20, 9) NOT NULL,
364
+ timestamp TIMESTAMP DEFAULT NOW()
365
+ );
366
+
367
+ -- ============================================
368
+ -- 6. JACKPOT SYSTEM
369
+ -- ============================================
370
+
371
+ -- Jackpot rounds history
372
+ CREATE TABLE IF NOT EXISTS jackpot_rounds (
373
+ id SERIAL PRIMARY KEY,
374
+ round_id VARCHAR(50) UNIQUE NOT NULL,
375
+ winner VARCHAR(100) NOT NULL,
376
+ win_amount VARCHAR(50) NOT NULL,
377
+ total_pot VARCHAR(50) NOT NULL,
378
+ entry_count INTEGER NOT NULL,
379
+ locked_at TIMESTAMP,
380
+ resolved_at TIMESTAMP,
381
+ created_at TIMESTAMP DEFAULT NOW()
382
+ );
383
+
384
+ -- Keeper actions (keeper bot logs)
385
+ CREATE TABLE IF NOT EXISTS keeper_actions (
386
+ id SERIAL PRIMARY KEY,
387
+ round_id INTEGER,
388
+ action_type VARCHAR(50) NOT NULL,
389
+ status VARCHAR(20) NOT NULL,
390
+ details TEXT,
391
+ error_message TEXT,
392
+ created_at TIMESTAMP DEFAULT NOW()
393
+ );
394
+
395
+ -- Keeper health (monitoring)
396
+ CREATE TABLE IF NOT EXISTS keeper_health (
397
+ id SERIAL PRIMARY KEY,
398
+ round_id INTEGER,
399
+ check_type VARCHAR(50) NOT NULL,
400
+ status VARCHAR(20) NOT NULL,
401
+ message TEXT,
402
+ created_at TIMESTAMP DEFAULT NOW()
403
+ );
404
+
405
+ -- Keeper rounds (keeper state machine)
406
+ CREATE TABLE IF NOT EXISTS keeper_rounds (
407
+ id SERIAL PRIMARY KEY,
408
+ round_id INTEGER UNIQUE NOT NULL,
409
+ state VARCHAR(50) NOT NULL,
410
+ entries_count INTEGER DEFAULT 0,
411
+ total_pot NUMERIC(20, 9) DEFAULT 0,
412
+ locked_at TIMESTAMP,
413
+ resolved_at TIMESTAMP,
414
+ winner VARCHAR(100),
415
+ last_action TIMESTAMP DEFAULT NOW(),
416
+ created_at TIMESTAMP DEFAULT NOW()
417
+ );
418
+
419
+ -- ============================================
420
+ -- 7. REFERRAL EARNINGS SYSTEM
421
+ -- ============================================
422
+ -- Commission goes to the GAME CREATOR's referrer ONLY!
423
+ -- The person who referred the GAME CREATOR earns 1% of the ENTIRE POT.
424
+ -- Fee Structure: 4% Operator + 1% Referrer + 1% Oracle = 6% Total
425
+ -- If game creator has no referrer, operator keeps full 5%.
426
+
427
+ -- Main earnings table - tracks each individual earning event
428
+ CREATE TABLE IF NOT EXISTS referral_earnings (
429
+ id SERIAL PRIMARY KEY,
430
+
431
+ -- Relationship: Referrer (who earns) and Referee (who played)
432
+ referrer_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
433
+ referrer_wallet VARCHAR(44) NOT NULL,
434
+ referee_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
435
+ referee_wallet VARCHAR(44) NOT NULL,
436
+
437
+ -- Game details
438
+ game_id VARCHAR(255) NOT NULL,
439
+ game_type VARCHAR(50) NOT NULL, -- 'sports', 'billiards', 'jackpot'
440
+
441
+ -- Financial details (all in lamports)
442
+ pot_size BIGINT NOT NULL, -- Total pot of the game
443
+ referee_buy_in BIGINT NOT NULL, -- Amount referee wagered
444
+ referee_won BOOLEAN NOT NULL, -- Did referee win?
445
+ referee_payout BIGINT DEFAULT 0, -- Amount referee won (0 if lost)
446
+
447
+ -- Commission details
448
+ commission_rate DECIMAL(6,4) NOT NULL DEFAULT 0.0100, -- 1% = 0.0100
449
+ commission_amount BIGINT NOT NULL, -- Actual commission in lamports
450
+
451
+ -- Status and payout tracking
452
+ status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'paid', 'cancelled')),
453
+ notes TEXT,
454
+ created_at TIMESTAMP DEFAULT NOW(),
455
+ paid_at TIMESTAMP,
456
+ payout_tx_signature VARCHAR(128),
457
+ payout_batch_id INTEGER,
458
+ notes TEXT, -- Optional notes (e.g., "Paid on-chain during game resolution")
459
+
460
+ -- Prevent duplicate entries for same referee in same game
461
+ UNIQUE(referee_wallet, game_id)
462
+ );
463
+
464
+ -- Payout batches table - tracks batch payouts to referrers
465
+ CREATE TABLE IF NOT EXISTS referral_payout_batches (
466
+ id SERIAL PRIMARY KEY,
467
+
468
+ -- Batch details
469
+ total_amount BIGINT NOT NULL, -- Total lamports in this batch
470
+ num_referrers INTEGER NOT NULL, -- Number of unique referrers paid
471
+ num_earnings INTEGER NOT NULL, -- Number of individual earning records
472
+
473
+ -- Transaction details
474
+ tx_signature VARCHAR(128),
475
+ status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'completed', 'failed')),
476
+
477
+ -- Timing
478
+ created_at TIMESTAMP DEFAULT NOW(),
479
+ processed_at TIMESTAMP,
480
+ completed_at TIMESTAMP,
481
+
482
+ -- Error tracking
483
+ error_message TEXT,
484
+ retry_count INTEGER DEFAULT 0
485
+ );
486
+
487
+ -- Referral earnings indexes
488
+ CREATE INDEX IF NOT EXISTS idx_referral_earnings_referrer ON referral_earnings(referrer_user_id, status);
489
+ CREATE INDEX IF NOT EXISTS idx_referral_earnings_referrer_wallet ON referral_earnings(referrer_wallet);
490
+ CREATE INDEX IF NOT EXISTS idx_referral_earnings_referee ON referral_earnings(referee_user_id);
491
+ CREATE INDEX IF NOT EXISTS idx_referral_earnings_game ON referral_earnings(game_id);
492
+ CREATE INDEX IF NOT EXISTS idx_referral_earnings_status ON referral_earnings(status);
493
+ CREATE INDEX IF NOT EXISTS idx_referral_earnings_created ON referral_earnings(created_at DESC);
494
+ CREATE INDEX IF NOT EXISTS idx_referral_earnings_pending ON referral_earnings(referrer_wallet) WHERE status = 'pending';
495
+ CREATE INDEX IF NOT EXISTS idx_payout_batches_status ON referral_payout_batches(status);
496
+
497
+ -- Aggregated summary view for quick dashboard stats
498
+ CREATE OR REPLACE VIEW referral_earnings_summary AS
499
+ SELECT
500
+ u.id as user_id,
501
+ u.wallet_address,
502
+ u.username,
503
+ u.my_referral_code,
504
+ COALESCE(COUNT(re.id), 0) as total_referral_games,
505
+ COALESCE(SUM(re.commission_amount), 0) as total_earned_lamports,
506
+ COALESCE(SUM(CASE WHEN re.status = 'pending' THEN re.commission_amount ELSE 0 END), 0) as pending_lamports,
507
+ COALESCE(SUM(CASE WHEN re.status = 'paid' THEN re.commission_amount ELSE 0 END), 0) as paid_lamports,
508
+ MAX(re.created_at) as last_earning_at
509
+ FROM users u
510
+ LEFT JOIN referral_earnings re ON u.wallet_address = re.referrer_wallet
511
+ WHERE u.my_referral_code IS NOT NULL
512
+ GROUP BY u.id, u.wallet_address, u.username, u.my_referral_code;
513
+
514
+ -- ============================================
515
+ -- 8. WHAT'S NEW SYSTEM (Feature Announcements)
516
+ -- ============================================
517
+
518
+ -- What's New posts (admin announcements with GIFs)
519
+ CREATE TABLE IF NOT EXISTS whats_new_posts (
520
+ id SERIAL PRIMARY KEY,
521
+
522
+ -- Post content
523
+ title VARCHAR(200) NOT NULL,
524
+ content TEXT NOT NULL, -- Markdown-style content
525
+ gif_url TEXT, -- Optional GIF URL to showcase feature
526
+
527
+ -- Categorization
528
+ category VARCHAR(50) DEFAULT 'feature', -- feature, improvement, bugfix, announcement
529
+
530
+ -- Versioning
531
+ version VARCHAR(20), -- Optional app version (e.g., "1.2.0")
532
+
533
+ -- Targeting (future use - could target specific user segments)
534
+ target_all BOOLEAN DEFAULT TRUE,
535
+
536
+ -- Status
537
+ is_published BOOLEAN DEFAULT TRUE,
538
+ is_pinned BOOLEAN DEFAULT FALSE, -- Pinned posts show at top
539
+
540
+ -- Audit
541
+ created_by VARCHAR(64) NOT NULL, -- Admin wallet address
542
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
543
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
544
+ );
545
+
546
+ -- User What's New reads (tracking which users have seen which posts)
547
+ CREATE TABLE IF NOT EXISTS user_whats_new_reads (
548
+ id SERIAL PRIMARY KEY,
549
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
550
+ post_id INTEGER NOT NULL REFERENCES whats_new_posts(id) ON DELETE CASCADE,
551
+ read_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
552
+
553
+ -- Prevent duplicates
554
+ UNIQUE(user_id, post_id)
555
+ );
556
+
557
+ -- What's New indexes
558
+ CREATE INDEX IF NOT EXISTS idx_whats_new_posts_published
559
+ ON whats_new_posts(is_published, created_at DESC);
560
+ CREATE INDEX IF NOT EXISTS idx_whats_new_posts_pinned
561
+ ON whats_new_posts(is_pinned, created_at DESC);
562
+ CREATE INDEX IF NOT EXISTS idx_user_whats_new_reads_user
563
+ ON user_whats_new_reads(user_id);
564
+ CREATE INDEX IF NOT EXISTS idx_user_whats_new_reads_post
565
+ ON user_whats_new_reads(post_id);
566
+
567
+ -- Function to update updated_at timestamp for whats_new_posts
568
+ CREATE OR REPLACE FUNCTION update_whats_new_updated_at()
569
+ RETURNS TRIGGER AS $$
570
+ BEGIN
571
+ NEW.updated_at = NOW();
572
+ RETURN NEW;
573
+ END;
574
+ $$ LANGUAGE plpgsql;
575
+
576
+ -- Trigger to auto-update updated_at
577
+ DROP TRIGGER IF EXISTS whats_new_posts_updated_at ON whats_new_posts;
578
+ CREATE TRIGGER whats_new_posts_updated_at
579
+ BEFORE UPDATE ON whats_new_posts
580
+ FOR EACH ROW
581
+ EXECUTE FUNCTION update_whats_new_updated_at();
582
+
583
+ -- ============================================
584
+ -- 9. DEVELOPER API PLATFORM
585
+ -- ============================================
586
+
587
+ -- Developer accounts (linked to Solana wallet)
588
+ CREATE TABLE IF NOT EXISTS developer_accounts (
589
+ id SERIAL PRIMARY KEY,
590
+ wallet_address VARCHAR(44) UNIQUE NOT NULL,
591
+ display_name VARCHAR(100),
592
+ email VARCHAR(255),
593
+ commission_wallet VARCHAR(44) NOT NULL,
594
+ created_at TIMESTAMPTZ DEFAULT NOW(),
595
+ updated_at TIMESTAMPTZ DEFAULT NOW()
596
+ );
597
+
598
+ -- Developer apps (each gets API keys)
599
+ CREATE TABLE IF NOT EXISTS developer_apps (
600
+ id SERIAL PRIMARY KEY,
601
+ developer_id INTEGER REFERENCES developer_accounts(id),
602
+ app_name VARCHAR(100) NOT NULL,
603
+ description TEXT,
604
+ website_url VARCHAR(500),
605
+ environment VARCHAR(10) DEFAULT 'sandbox',
606
+ status VARCHAR(20) DEFAULT 'active',
607
+ network_mode VARCHAR(10) DEFAULT 'open' CHECK (network_mode IN ('open', 'private')),
608
+ ui_config JSONB DEFAULT '{}',
609
+ resolution_secret VARCHAR(64),
610
+ created_at TIMESTAMPTZ DEFAULT NOW(),
611
+ updated_at TIMESTAMPTZ DEFAULT NOW()
612
+ );
613
+
614
+ -- API keys (sandbox + production per app)
615
+ CREATE TABLE IF NOT EXISTS developer_api_keys (
616
+ id SERIAL PRIMARY KEY,
617
+ app_id INTEGER REFERENCES developer_apps(id),
618
+ key_prefix VARCHAR(20) NOT NULL,
619
+ key_hash VARCHAR(64) NOT NULL,
620
+ key_hint VARCHAR(8) NOT NULL,
621
+ environment VARCHAR(10) NOT NULL,
622
+ is_active BOOLEAN DEFAULT TRUE,
623
+ created_at TIMESTAMPTZ DEFAULT NOW(),
624
+ last_used_at TIMESTAMPTZ
625
+ );
626
+
627
+ -- Track which games came from which developer app
628
+ CREATE TABLE IF NOT EXISTS developer_game_attributions (
629
+ id SERIAL PRIMARY KEY,
630
+ game_id VARCHAR(100) NOT NULL,
631
+ app_id INTEGER REFERENCES developer_apps(id),
632
+ developer_id INTEGER REFERENCES developer_accounts(id),
633
+ commission_wallet VARCHAR(44) NOT NULL,
634
+ created_at TIMESTAMPTZ DEFAULT NOW()
635
+ );
636
+
637
+ -- API usage logs for rate limiting and analytics
638
+ CREATE TABLE IF NOT EXISTS developer_api_logs (
639
+ id SERIAL PRIMARY KEY,
640
+ app_id INTEGER REFERENCES developer_apps(id),
641
+ endpoint VARCHAR(200) NOT NULL,
642
+ method VARCHAR(10) NOT NULL,
643
+ status_code INTEGER,
644
+ response_time_ms INTEGER,
645
+ created_at TIMESTAMPTZ DEFAULT NOW()
646
+ );
647
+
648
+ -- Developer webhooks (outbound notifications)
649
+ CREATE TABLE IF NOT EXISTS developer_webhooks (
650
+ id SERIAL PRIMARY KEY,
651
+ app_id INTEGER NOT NULL REFERENCES developer_apps(id) ON DELETE CASCADE,
652
+ url VARCHAR(500) NOT NULL,
653
+ secret VARCHAR(64) NOT NULL,
654
+ events TEXT[] NOT NULL DEFAULT '{}',
655
+ is_active BOOLEAN DEFAULT TRUE,
656
+ description VARCHAR(200),
657
+ created_at TIMESTAMPTZ DEFAULT NOW(),
658
+ updated_at TIMESTAMPTZ DEFAULT NOW()
659
+ );
660
+
661
+ -- Developer webhook delivery logs
662
+ CREATE TABLE IF NOT EXISTS developer_webhook_logs (
663
+ id SERIAL PRIMARY KEY,
664
+ webhook_id INTEGER NOT NULL REFERENCES developer_webhooks(id) ON DELETE CASCADE,
665
+ event VARCHAR(50) NOT NULL,
666
+ payload JSONB NOT NULL,
667
+ status_code INTEGER,
668
+ response_body TEXT,
669
+ attempts INTEGER DEFAULT 1,
670
+ success BOOLEAN DEFAULT FALSE,
671
+ error TEXT,
672
+ created_at TIMESTAMPTZ DEFAULT NOW()
673
+ );
674
+
675
+ -- Developer app users (tracks which users authenticated through which app)
676
+ CREATE TABLE IF NOT EXISTS developer_app_users (
677
+ developer_app_id INTEGER NOT NULL REFERENCES developer_apps(id) ON DELETE CASCADE,
678
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
679
+ first_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
680
+ last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
681
+ PRIMARY KEY (developer_app_id, user_id)
682
+ );
683
+
684
+ -- Developer platform indexes
685
+ CREATE INDEX IF NOT EXISTS idx_developer_accounts_wallet ON developer_accounts(wallet_address);
686
+ CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON developer_api_keys(key_hash);
687
+ CREATE INDEX IF NOT EXISTS idx_game_attributions_game ON developer_game_attributions(game_id);
688
+ CREATE INDEX IF NOT EXISTS idx_game_attributions_app ON developer_game_attributions(app_id);
689
+ CREATE INDEX IF NOT EXISTS idx_api_logs_app ON developer_api_logs(app_id, created_at);
690
+ CREATE INDEX IF NOT EXISTS idx_webhooks_app ON developer_webhooks(app_id);
691
+ CREATE INDEX IF NOT EXISTS idx_webhook_logs_webhook ON developer_webhook_logs(webhook_id, created_at DESC);
692
+ CREATE INDEX IF NOT EXISTS idx_developer_app_users_app ON developer_app_users(developer_app_id);
693
+ CREATE INDEX IF NOT EXISTS idx_developer_app_users_user ON developer_app_users(user_id);
694
+ CREATE INDEX IF NOT EXISTS idx_developer_app_users_last_seen ON developer_app_users(developer_app_id, last_seen_at DESC);
695
+
696
+ -- ============================================
697
+ -- 10. MIGRATIONS (Add missing columns to existing tables)
698
+ -- ============================================
699
+
700
+ -- Add buy_in column to user_game_refs if it doesn't exist
701
+ DO $$
702
+ BEGIN
703
+ IF NOT EXISTS (
704
+ SELECT 1 FROM information_schema.columns
705
+ WHERE table_name = 'user_game_refs' AND column_name = 'buy_in'
706
+ ) THEN
707
+ ALTER TABLE user_game_refs ADD COLUMN buy_in NUMERIC(20, 9);
708
+ END IF;
709
+
710
+ IF NOT EXISTS (
711
+ SELECT 1 FROM information_schema.columns
712
+ WHERE table_name = 'user_game_refs' AND column_name = 'won_game'
713
+ ) THEN
714
+ ALTER TABLE user_game_refs ADD COLUMN won_game BOOLEAN DEFAULT false;
715
+ END IF;
716
+ END $$;
717
+
718
+ -- Add check constraint for preferred_currency if it doesn't exist
719
+ DO $$
720
+ BEGIN
721
+ IF NOT EXISTS (
722
+ SELECT 1 FROM information_schema.table_constraints
723
+ WHERE constraint_name = 'check_preferred_currency'
724
+ ) THEN
725
+ ALTER TABLE users
726
+ ADD CONSTRAINT check_preferred_currency
727
+ CHECK (preferred_currency IN ('USD', 'EUR', 'CAD', 'GBP', 'JPY', 'AUD', 'CHF', 'CNY', 'SEK', 'NZD'));
728
+ END IF;
729
+ END $$;
730
+
731
+ -- Add comment for preferred_currency column
732
+ COMMENT ON COLUMN users.preferred_currency IS 'User preferred display currency (3-letter code)';
733
+
734
+ -- Add notification tracking columns to games table if they don't exist
735
+ DO $$
736
+ BEGIN
737
+ IF NOT EXISTS (
738
+ SELECT 1 FROM information_schema.columns
739
+ WHERE table_name = 'games' AND column_name = 'lock_notification_sent_10min'
740
+ ) THEN
741
+ ALTER TABLE games ADD COLUMN lock_notification_sent_10min BOOLEAN DEFAULT false;
742
+ END IF;
743
+
744
+ IF NOT EXISTS (
745
+ SELECT 1 FROM information_schema.columns
746
+ WHERE table_name = 'games' AND column_name = 'lock_notification_sent_now'
747
+ ) THEN
748
+ ALTER TABLE games ADD COLUMN lock_notification_sent_now BOOLEAN DEFAULT false;
749
+ END IF;
750
+
751
+ -- Add matchup_image_url column for pre-generated S3 matchup images
752
+ IF NOT EXISTS (
753
+ SELECT 1 FROM information_schema.columns
754
+ WHERE table_name = 'games' AND column_name = 'matchup_image_url'
755
+ ) THEN
756
+ ALTER TABLE games ADD COLUMN matchup_image_url TEXT;
757
+ END IF;
758
+
759
+ -- Add draw_team_players column for EPL/soccer draw betting
760
+ IF NOT EXISTS (
761
+ SELECT 1 FROM information_schema.columns
762
+ WHERE table_name = 'games' AND column_name = 'draw_team_players'
763
+ ) THEN
764
+ ALTER TABLE games ADD COLUMN draw_team_players TEXT[] DEFAULT '{}';
765
+ END IF;
766
+
767
+ -- Add Connect4 specific columns
768
+ IF NOT EXISTS (
769
+ SELECT 1 FROM information_schema.columns
770
+ WHERE table_name = 'games' AND column_name = 'game_status'
771
+ ) THEN
772
+ ALTER TABLE games ADD COLUMN game_status VARCHAR(50);
773
+ END IF;
774
+
775
+ IF NOT EXISTS (
776
+ SELECT 1 FROM information_schema.columns
777
+ WHERE table_name = 'games' AND column_name = 'connect4_board'
778
+ ) THEN
779
+ ALTER TABLE games ADD COLUMN connect4_board JSONB;
780
+ END IF;
781
+
782
+ IF NOT EXISTS (
783
+ SELECT 1 FROM information_schema.columns
784
+ WHERE table_name = 'games' AND column_name = 'connect4_current_turn'
785
+ ) THEN
786
+ ALTER TABLE games ADD COLUMN connect4_current_turn VARCHAR(10);
787
+ END IF;
788
+
789
+ IF NOT EXISTS (
790
+ SELECT 1 FROM information_schema.columns
791
+ WHERE table_name = 'games' AND column_name = 'connect4_winner'
792
+ ) THEN
793
+ ALTER TABLE games ADD COLUMN connect4_winner VARCHAR(10);
794
+ END IF;
795
+
796
+ IF NOT EXISTS (
797
+ SELECT 1 FROM information_schema.columns
798
+ WHERE table_name = 'games' AND column_name = 'connect4_winning_cells'
799
+ ) THEN
800
+ ALTER TABLE games ADD COLUMN connect4_winning_cells JSONB;
801
+ END IF;
802
+
803
+ IF NOT EXISTS (
804
+ SELECT 1 FROM information_schema.columns
805
+ WHERE table_name = 'games' AND column_name = 'claim_signature'
806
+ ) THEN
807
+ ALTER TABLE games ADD COLUMN claim_signature TEXT;
808
+ END IF;
809
+
810
+ IF NOT EXISTS (
811
+ SELECT 1 FROM information_schema.columns
812
+ WHERE table_name = 'games' AND column_name = 'completed_at'
813
+ ) THEN
814
+ ALTER TABLE games ADD COLUMN completed_at TIMESTAMP;
815
+ END IF;
816
+
817
+ IF NOT EXISTS (
818
+ SELECT 1 FROM information_schema.columns
819
+ WHERE table_name = 'games' AND column_name = 'invited_player'
820
+ ) THEN
821
+ ALTER TABLE games ADD COLUMN invited_player VARCHAR(255);
822
+ END IF;
823
+ END $$;
824
+
825
+ -- Add indexes for Connect4 columns
826
+ CREATE INDEX IF NOT EXISTS idx_games_game_status ON games(game_status);
827
+ CREATE INDEX IF NOT EXISTS idx_games_type_status ON games(game_type, game_status);
828
+ CREATE INDEX IF NOT EXISTS idx_games_invited_player ON games(invited_player) WHERE invited_player IS NOT NULL;
829
+
830
+ -- ============================================
831
+ -- 11. FOREIGN KEY CONSTRAINTS
832
+ -- ============================================
833
+
834
+ -- Add foreign keys if they don't exist
835
+ DO $$
836
+ BEGIN
837
+ -- Chat messages reply_to_id
838
+ IF NOT EXISTS (
839
+ SELECT 1 FROM information_schema.table_constraints
840
+ WHERE constraint_name = 'chat_messages_reply_to_id_fkey'
841
+ ) THEN
842
+ ALTER TABLE chat_messages
843
+ ADD CONSTRAINT chat_messages_reply_to_id_fkey
844
+ FOREIGN KEY (reply_to_id) REFERENCES chat_messages(id) ON DELETE SET NULL;
845
+ END IF;
846
+
847
+ -- Chat reactions message_id
848
+ IF NOT EXISTS (
849
+ SELECT 1 FROM information_schema.table_constraints
850
+ WHERE constraint_name = 'chat_reactions_message_id_fkey'
851
+ ) THEN
852
+ ALTER TABLE chat_reactions
853
+ ADD CONSTRAINT chat_reactions_message_id_fkey
854
+ FOREIGN KEY (message_id) REFERENCES chat_messages(id) ON DELETE CASCADE;
855
+ END IF;
856
+
857
+ -- Chat notifications message_id
858
+ IF NOT EXISTS (
859
+ SELECT 1 FROM information_schema.table_constraints
860
+ WHERE constraint_name = 'chat_notifications_message_id_fkey'
861
+ ) THEN
862
+ ALTER TABLE chat_notifications
863
+ ADD CONSTRAINT chat_notifications_message_id_fkey
864
+ FOREIGN KEY (message_id) REFERENCES chat_messages(id) ON DELETE CASCADE;
865
+ END IF;
866
+ END $$;
867
+
868
+ -- ============================================
869
+ -- 12. INDEXES FOR PERFORMANCE
870
+ -- ============================================
871
+
872
+ -- Users indexes
873
+ CREATE INDEX IF NOT EXISTS idx_users_wallet ON users(wallet_address);
874
+ CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
875
+ CREATE INDEX IF NOT EXISTS idx_users_referral_code ON users(referral_code);
876
+ CREATE INDEX IF NOT EXISTS idx_users_telegram_user_id ON users(telegram_user_id) WHERE telegram_user_id IS NOT NULL;
877
+ CREATE INDEX IF NOT EXISTS idx_users_preferred_currency ON users(preferred_currency);
878
+
879
+ -- Push subscriptions indexes
880
+ CREATE INDEX IF NOT EXISTS idx_push_subscriptions_user_id ON push_subscriptions(user_id);
881
+
882
+ -- Auth indexes
883
+ CREATE INDEX IF NOT EXISTS idx_nonces_expires ON auth_nonces(expires_at);
884
+ CREATE INDEX IF NOT EXISTS idx_user_sessions_wallet ON user_sessions(wallet_address);
885
+ CREATE INDEX IF NOT EXISTS idx_user_sessions_token_hash ON user_sessions(token_hash);
886
+ CREATE INDEX IF NOT EXISTS idx_user_sessions_expires ON user_sessions(expires_at);
887
+
888
+ -- User badges indexes
889
+ CREATE INDEX IF NOT EXISTS idx_user_badges_user_id ON user_badges(user_id);
890
+ CREATE INDEX IF NOT EXISTS idx_user_badges_type ON user_badges(badge_type);
891
+ CREATE INDEX IF NOT EXISTS idx_user_badges_earned ON user_badges(earned_at DESC);
892
+
893
+ -- Chat indexes
894
+ CREATE INDEX IF NOT EXISTS idx_chat_timestamp ON chat_messages(timestamp DESC);
895
+ CREATE INDEX IF NOT EXISTS idx_chat_wallet ON chat_messages(wallet_address);
896
+ CREATE INDEX IF NOT EXISTS idx_chat_user_id ON chat_messages(user_id);
897
+ CREATE INDEX IF NOT EXISTS idx_chat_reply_to ON chat_messages(reply_to_id);
898
+ CREATE INDEX IF NOT EXISTS idx_chat_winner ON chat_messages(is_winner_announcement);
899
+ CREATE INDEX IF NOT EXISTS idx_chat_game_invite ON chat_messages((game_invite_metadata->>'gameId')) WHERE game_invite_metadata IS NOT NULL;
900
+ CREATE INDEX IF NOT EXISTS idx_reactions_message ON chat_reactions(message_id);
901
+ CREATE INDEX IF NOT EXISTS idx_relationships_user ON user_relationships(user_id);
902
+ CREATE INDEX IF NOT EXISTS idx_relationships_type ON user_relationships(relationship_type);
903
+ CREATE INDEX IF NOT EXISTS idx_notifications_user ON chat_notifications(user_id);
904
+ CREATE INDEX IF NOT EXISTS idx_message_mentions_user ON message_mentions(mentioned_user_id);
905
+ CREATE INDEX IF NOT EXISTS idx_message_mentions_message ON message_mentions(message_id);
906
+ CREATE INDEX IF NOT EXISTS idx_payments_message ON chat_payments(message_id);
907
+ CREATE INDEX IF NOT EXISTS idx_payments_sender ON chat_payments(sender_user_id);
908
+ CREATE INDEX IF NOT EXISTS idx_payments_recipient ON chat_payments(recipient_user_id);
909
+ CREATE INDEX IF NOT EXISTS idx_payments_signature ON chat_payments(transaction_signature);
910
+
911
+ -- Games indexes
912
+ CREATE INDEX IF NOT EXISTS idx_games_game_id ON games(game_id);
913
+ CREATE INDEX IF NOT EXISTS idx_games_creator ON games(created_by);
914
+ CREATE INDEX IF NOT EXISTS idx_games_status ON games(automatic_status);
915
+ CREATE INDEX IF NOT EXISTS idx_user_game_refs_wallet ON user_game_refs(wallet_address);
916
+ CREATE INDEX IF NOT EXISTS idx_user_game_refs_game ON user_game_refs(game_id);
917
+ CREATE INDEX IF NOT EXISTS idx_audit_logs_user ON audit_logs(user_id);
918
+ CREATE INDEX IF NOT EXISTS idx_audit_logs_type ON audit_logs(log_type);
919
+
920
+ -- Social indexes
921
+ CREATE INDEX IF NOT EXISTS idx_friends_user ON friends(user_wallet);
922
+ CREATE INDEX IF NOT EXISTS idx_friends_status ON friends(status);
923
+ CREATE INDEX IF NOT EXISTS idx_friend_requests_from ON friend_requests(from_user_id);
924
+ CREATE INDEX IF NOT EXISTS idx_friend_requests_to ON friend_requests(to_user_id);
925
+ CREATE INDEX IF NOT EXISTS idx_friend_requests_status ON friend_requests(status);
926
+ CREATE INDEX IF NOT EXISTS idx_group_members_group ON group_members(group_id);
927
+ CREATE INDEX IF NOT EXISTS idx_group_members_user ON group_members(user_id);
928
+
929
+ -- Player stats indexes
930
+ CREATE INDEX IF NOT EXISTS idx_player_stats_pnl ON player_stats(net_pnl DESC);
931
+ CREATE INDEX IF NOT EXISTS idx_player_stats_wagered ON player_stats(total_wagered DESC);
932
+ CREATE INDEX IF NOT EXISTS idx_player_history_wallet ON player_history(wallet_address, timestamp DESC);
933
+ CREATE INDEX IF NOT EXISTS idx_player_history_round ON player_history(round_id);
934
+
935
+ -- Keeper indexes
936
+ CREATE INDEX IF NOT EXISTS idx_keeper_actions_round ON keeper_actions(round_id);
937
+ CREATE INDEX IF NOT EXISTS idx_keeper_health_round ON keeper_health(round_id);
938
+ CREATE INDEX IF NOT EXISTS idx_keeper_rounds_state ON keeper_rounds(state);
939
+
940
+ -- ============================================
941
+ -- 13. VIEWS FOR MONITORING
942
+ -- ============================================
943
+
944
+ -- Stuck rounds view (for keeper monitoring)
945
+ CREATE OR REPLACE VIEW stuck_rounds AS
946
+ SELECT
947
+ round_id,
948
+ state,
949
+ entries_count,
950
+ last_action,
951
+ (NOW() - last_action) as time_stuck
952
+ FROM keeper_rounds
953
+ WHERE state IN ('open', 'locked')
954
+ AND (NOW() - last_action) > INTERVAL '10 minutes'
955
+ ORDER BY last_action ASC;
956
+
957
+ -- Keeper health summary
958
+ CREATE OR REPLACE VIEW keeper_health_summary AS
959
+ SELECT
960
+ DATE(created_at) as date,
961
+ check_type,
962
+ status,
963
+ COUNT(*) as count
964
+ FROM keeper_health
965
+ WHERE created_at > NOW() - INTERVAL '7 days'
966
+ GROUP BY DATE(created_at), check_type, status
967
+ ORDER BY date DESC, check_type;
968
+
969
+ -- ============================================
970
+ -- 14. SUMMARY
971
+ -- ============================================
972
+
973
+ -- Show all tables
974
+ \echo '✅ Database schema setup complete!'
975
+ \echo ''
976
+ \echo 'Tables created:'
977
+ SELECT table_name
978
+ FROM information_schema.tables
979
+ WHERE table_schema = 'public'
980
+ AND table_type = 'BASE TABLE'
981
+ ORDER BY table_name;
982
+
983
+ \echo ''
984
+ \echo 'Views created:'
985
+ SELECT table_name
986
+ FROM information_schema.views
987
+ WHERE table_schema = 'public'
988
+ ORDER BY table_name;
989
+
990
+ \echo ''
991
+ \echo '🎉 All done! Your database is ready for the DUBS server.'
992
+