Brainrush
Multiplayer quiz game: a Flutter app for iOS and Android, a Next.js website where people play in the browser, an Admin panel and the API, plus a background worker. You add your own keys; everything else is ready.
01Welcome
Thank you for buying Brainrush. This guide is written for beginners: follow the chapters in order and you will have the server, the website and the app running under your own name and keys.
What is in the download
| Folder | What it is |
|---|---|
brainrush_mobile_flutter/ | The mobile app (Flutter, iOS and Android): levels, the daily quiz, live battles, contests, leagues, exams, player-made quizzes, coin store and ads. |
brainrush_web_nextjs/ | One Next.js app that is the website (play in the browser), the Admin panel (/admin), the API the app uses (/api/v1) and the background worker (pnpm worker: battle clock, contest payouts, weekly leagues, daily quizzes, pushes). |
deploy/ | The Docker stack: website + worker + PostgreSQL database + MinIO file storage (+ optional automatic HTTPS), started with one command. |
tools/rename.mjs | Rebrands the whole kit (name, app id, colour, font, icon) in one command. |
docs/ | This documentation (HTML and PDF). |
How the parts fit
The mobile app and the website both talk to the same server (the Next.js app). All data lives in your PostgreSQL database: players, questions, games, coins, leagues. Question pictures, sounds and avatars live in S3-compatible storage (MinIO in the Docker stack, or Amazon S3, Cloudflare R2…). Firebase is used only for sign-in and push notifications.
DEMO_PURCHASES="true": they then run in demo mode (coins are granted without payment and labelled DEMO). Keep it false on a live server. Ads show Google's test ads until you add your AdMob ids (rewarded ads pay coins only after AdMob server-side verification, chapter 12). AI and email each show a clear "add your key" state until set.Screenshots
The main screens, so you know what you are setting up. The names, photos and numbers are demo data; yours replace them.
Mobile app (iOS and Android)
Website
Admin panel (/admin)
02Requirements
To run the server
- A Linux server (VPS) with at least 2 CPU cores, 4 GB RAM and 20 GB disk. Ubuntu 24.04 is used in this guide.
- Docker with the Compose plugin (install guide).
- A domain name, for example
your-domain.com, with a DNS record pointing at the server.
To build the mobile app
- Flutter 3.44 or newer (Dart 3.12; install), Android Studio for Android, a Mac with Xcode for iOS.
- Node.js 22 or newer for the rename tool, and pnpm (
npm i -g pnpm) if you run the website without Docker.
Third-party services (accounts you will create)
| Service | Needed for | What it costs (check their pricing page) |
|---|---|---|
| Firebase | Sign-in and push (required) | The free Spark plan covers sign-in and push for most apps; phone sign-in and very large user counts are paid. |
| RevenueCat | In-app purchases (optional) | Free up to a monthly revenue limit, then a share of revenue. |
| Google AdMob | Ads (optional) | Free; Google pays you a share of ad revenue. |
| Anthropic, OpenAI or Google Gemini | AI question generator (optional) | Pay per use (per token); Gemini has a free tier with limits. |
| Any SMTP provider | Staff invites and password emails (optional) | Many have a free monthly allowance, then a fee per email. |
| Apple Developer Program / Google Play Console | Publishing the app | Apple: yearly fee. Google: one-time fee. |
| A VPS and a domain | The server | Monthly hosting and a yearly domain fee from your provider. |
- Firebase (required: sign-in and push).
- Optional, when you want them: RevenueCat (in-app purchases), Google AdMob (ads), an AI key from Anthropic, OpenAI or Google Gemini (question generator), any SMTP email provider.
03Quick start (Docker)
This gets the whole server running on your VPS in about 15 minutes. Sign-in needs the Firebase keys from chapter 4; you can do this chapter first and add them after.
- Copy the kit to the server, for example with
scp brainrush-1.0.0.zip root@YOUR_SERVER_IP:, then on the server:apt install -y unzip unzip brainrush-1.0.0.zip cd brainrush/deploy cp .env.example .env nano .env - In
.env, fill at least these values:Key What to put POSTGRES_PASSWORDA long random password, letters and digits only (run openssl rand -hex 24to make one).S3_SECRET_ACCESS_KEYAnother random value (at least 8 characters) for the built-in file storage. APP_URLYour website address, e.g. https://your-domain.com(for a first test:http://YOUR_SERVER_IP:3000).NEXT_PUBLIC_FIREBASE_*,FIREBASE_*From chapter 4. - Start everything:
The first build takes 5–10 minutes. Each start creates or updates the database tables, then loads the badges and coin packs.docker compose up -d - Open
APP_URLin a browser. The first visit opens the install wizard: your admin account, the app name and the sample quizzes (chapter 7).APP_URL/api/v1/healthshows{"ok":true,…}. - Put it on your domain with HTTPS (chapter 5).
WEB_PORT in .env..env? Run docker compose up -d again. Changed code? Run docker compose up -d --build.04Firebase setup
Firebase handles sign-in (guest, Google, Apple, email) and push notifications. Your data stays in your own database.
- Go to the Firebase console → Add project. Give it your app name.
- Build → Authentication → Get started → Sign-in method. Turn on: Anonymous (guest play), Google, Email/Password (players and staff) and Apple (see chapter 15).
- Authentication → Settings → Authorized domains: add
your-domain.com. - Project settings (gear) → General → Your apps → Add app → Web (name it "Website"). Copy the values from the
firebaseConfigshown intodeploy/.env:
The website reads these when it starts, so a restart (NEXT_PUBLIC_FIREBASE_API_KEY="…apiKey…" NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN="your-project.firebaseapp.com" NEXT_PUBLIC_FIREBASE_PROJECT_ID="your-project" NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET="your-project.firebasestorage.app" NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID="…" NEXT_PUBLIC_FIREBASE_APP_ID="1:…:web:…"docker compose up -d) is enough after a change. - Project settings → Service accounts → Generate new private key. A JSON file downloads. Copy three values from it into
deploy/.env:
Keep theFIREBASE_PROJECT_ID="your-project" FIREBASE_CLIENT_EMAIL="firebase-adminsdk-xxxx@your-project.iam.gserviceaccount.com" FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIE…\n-----END PRIVATE KEY-----\n"\nas they are in the file. Keep this file secret. - Add the mobile apps: Project settings → Your apps → Add app, once for Android and once for iOS, with your app id (for example
com.yourcompany.brainrush; see chapter 16 to change it first). Downloadgoogle-services.jsonandGoogleService-Info.plist: you only need to copy values out of them (the app does not use the files themselves). Or runflutterfire configure, which does the same and prints the values. - Put the values in
brainrush_mobile_flutter/.env:Key in the app's .env Where to find it FIREBASE_PROJECT_ID,FIREBASE_MESSAGING_SENDER_ID,FIREBASE_STORAGE_BUCKETProject settings → General (project id, project number, storage bucket) FIREBASE_ANDROID_API_KEY,FIREBASE_ANDROID_APP_IDgoogle-services.json:api_key.current_keyandmobilesdk_app_idFIREBASE_IOS_API_KEY,FIREBASE_IOS_APP_ID,FIREBASE_IOS_BUNDLE_ID,FIREBASE_IOS_CLIENT_IDGoogleService-Info.plist:API_KEY,GOOGLE_APP_ID,BUNDLE_ID,CLIENT_IDGOOGLE_SERVER_CLIENT_IDAuthentication → Sign-in method → Google → Web SDK configuration → Web client ID (Google sign-in on Android) - iOS: copy
ios/Flutter/Keys.xcconfig.exampletoios/Flutter/Keys.xcconfig(the download already has one) and setGOOGLE_REVERSED_CLIENT_ID(theREVERSED_CLIENT_IDfromGoogleService-Info.plist) andFIREBASE_ENCODED_APP_ID(yourGOOGLE_APP_IDwith the colons turned into dashes, e.g.1:123:ios:abc→app-1-123-ios-abc). - Android Google sign-in needs your signing key fingerprints: run
cd brainrush_mobile_flutter/android && ./gradlew signingReportand add the SHA-1 and SHA-256 under Project settings → Your apps → Android → Add fingerprint. Add the fingerprints of your upload key and of Google Play's app signing key too when you publish.
05Your domain and HTTPS
Easiest: the stack can get a free certificate for you with Caddy. Point your domain's DNS at the server, open ports 80 and 443, then in deploy/.env set DOMAIN="your-domain.com" and APP_URL="https://your-domain.com", and start with the https profile:
docker compose --profile https up -d
Already use Nginx, Traefik or Caddy on the server? Point your-domain.com at port 3000 (WEB_PORT), set APP_URL, run docker compose up -d, and leave the https profile off. Close port 3000 in your firewall once the proxy works (ufw allow 22,80,443/tcp && ufw enable).
Backups
cd brainrush/deploy
docker compose exec postgres pg_dump -U brainrush brainrush | gzip > backup-$(date +%F).sql.gz
Run it daily with cron and copy the files off the server. Uploaded pictures and sounds are in the minio-data Docker volume; back it up too, or use a cloud bucket (next chapter).
06Other hosting
Website on Vercel (or any Node host)
Costs: Vercel and managed database providers are third-party services with their own pricing; you are responsible for any hosting costs.
- Create a PostgreSQL database (Neon, Supabase, Railway, or your own) and copy its connection string.
- Import
brainrush_web_nextjsinto Vercel (Root directory:brainrush_web_nextjs). Add the keys ofbrainrush_web_nextjs/.env.exampleunder Settings → Environment Variables, withDATABASE_URLset to your database and theS3_*keys set to a cloud bucket. - Create the tables once from your computer: in
brainrush_web_nextjs, put the sameDATABASE_URLin.env, then runpnpm install,pnpm prisma:migrate:deployandpnpm prisma:seed --base. Then open your site: the install wizard does the rest. - The worker (battle clock, contest payouts, leagues, daily quizzes, pushes) runs all the time, so it cannot run on Vercel. Run it on any small server:
cd brainrush_web_nextjs && pnpm install && pnpm workerwith the same.env(use a process manager such as pm2 or systemd), or with Docker usingdeploy/docker-compose.yml'sworkerservice.
Cloud storage instead of MinIO
Any S3-compatible bucket works. Set S3_ENDPOINT (empty for Amazon S3; for Cloudflare R2 https://<account>.r2.cloudflarestorage.com), S3_REGION, S3_BUCKET, S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY. Keep the bucket private: files are served through your API. Without any S3_* key (outside Docker), uploads go to a local uploads/ folder.
07First admin and sample quizzes
The first visit to your site opens the install wizard (/install). It asks for:
- the app name (set your support email later in Admin → Settings),
- your name, email and password: this makes your super admin account (it creates the Firebase email account for you),
- whether to load the sample quizzes: 14 categories, about 830 questions in all six question types, daily quizzes, articles and badges. Keep them while you test; you can delete or edit any of it later.
Then sign in at https://your-domain.com/admin. Invite more staff from Admin → Roles (roles: super admin, content editor, moderator, finance). The wizard only runs while no super admin exists.
docker compose down -v deletes the database and files (everything), then docker compose up -d and open the site again for the wizard.08Setup check
Open Admin → Setup check: it lists every key, says which are missing, and tests each connection (database, storage, Firebase sign-in, push, purchases, email, AI, worker) with a clear message. The same check runs in a terminal:
docker compose run --rm migrate pnpm run doctor # Docker
cd brainrush_web_nextjs && pnpm run doctor # without Docker
09Run the mobile app
cd brainrush_mobile_flutter. The download has a.envwith empty values (a copy of.env.example). Set:API_BASE_URL=https://your-domain.com APP_NAME=Brainrush FIREBASE_… (from chapter 4)- Get the packages and run on a phone or emulator:
flutter pub get flutter run - Build for testing:
flutter build apk --release(Android) or openios/Runner.xcworkspacein Xcode (iOS). - iOS signing: the Xcode project ships with Automatic signing and no team or provisioning profile set. Open
ios/Runner.xcworkspace→ select Runner → Signing & Capabilities → keep Automatically manage signing on and choose your own Team. Xcode then creates the certificates and profiles for your bundle id. Do this on the All tab so Debug, Release and Profile use the same team.
android/app/build.gradle.kts sets compileSdk = 36, targetSdk = 36 and minSdk = 24 explicitly. Google Play requires a recent target SDK; when Google raises it, change both numbers there.http://10.0.2.2:3000 and the iOS simulator at http://localhost:3000. Release builds need an https address.The app reads every setting from .env: no keys live in Dart code. After changing .env, stop the app and run it again (hot reload does not reload .env). Without the Firebase values the app shows a setup screen that says what is missing.
10In-app purchases (RevenueCat)
Costs: RevenueCat is a third-party service with its own pricing plans; you are responsible for any RevenueCat costs.
The app sells coin packs (consumables) and "Remove ads" (non-consumable) through the App Store and Google Play, with RevenueCat in between. The server is told about every purchase and adds the coins; the app never decides a balance itself.
- Create the products in App Store Connect and Google Play Console with these ids (or your own; the id of each pack is set in Admin → Coin Packs & IAP):
- Coins (consumable):
br.coins.500,br.coins.1200,br.coins.3000,br.coins.6500 - Remove ads (non-consumable):
br.removeads
- Coins (consumable):
- In RevenueCat: create a project, add your iOS and Android apps and import the products. The app buys products by id, so no offering or entitlement is needed.
- App keys: RevenueCat → Project → API keys → the public SDK keys. In
brainrush_mobile_flutter/.env:REVENUECAT_APPLE_KEYandREVENUECAT_GOOGLE_KEY. - Server keys in
deploy/.env:REVENUECAT_SECRET_KEY(a secret API key) andREVENUECAT_WEBHOOK_AUTH(any long random value). - RevenueCat → Integrations → Webhooks → add
https://your-domain.com/api/v1/webhooks/revenuecatwith the Authorization header value equal toREVENUECAT_WEBHOOK_AUTH.
DEMO_PURCHASES="true" and no REVENUECAT_SECRET_KEY, purchases are granted without payment and labelled DEMO, so you can try every flow. It is off by default, so a live server never gives coins away; setting the key switches to the stores.The website does not sell coins: players buy them in the app, and the same balance shows on the website.
11Push notifications
Costs: Firebase Cloud Messaging is free; Apple needs your paid Apple Developer Program membership for the APNs key.
- Android works once Firebase is set up (chapter 4).
- iOS: in the Apple Developer site create an APNs key (Keys → +, Apple Push Notifications service), then upload it in Firebase → Project settings → Cloud Messaging → Apple app configuration.
- Every app joins the topic
brainrush_all, and the server also sends to each player's own devices (battle invites, contest results, streak reminders, league results). Send your own messages from Admin → Notifications, now or scheduled.
12Ads (Google AdMob)
Costs: AdMob is a third-party Google service with its own terms and revenue share; you are responsible for any costs of using it.
The app has three ad types: rewarded (watch an ad to earn coins), interstitial (between games) and banner. Players who bought "Remove ads" see no interstitials or banners.
- In AdMob, add your Android and iOS apps and create one ad unit of each type you want.
- In
brainrush_mobile_flutter/.envsetADMOB_ANDROID_APP_IDand the unit idsADMOB_ANDROID_REWARDED_ID,ADMOB_ANDROID_INTERSTITIAL_ID,ADMOB_ANDROID_BANNER_ID,ADMOB_IOS_REWARDED_ID,ADMOB_IOS_INTERSTITIAL_ID,ADMOB_IOS_BANNER_ID. - The iOS app id goes in
ios/Flutter/Keys.xcconfig:ADMOB_IOS_APP_ID=ca-app-pub-…~…. - Server-side verification (required for rewarded ads): in AdMob open each rewarded ad unit → Server-side verification → turn it on and set the callback URL to
https://your-domain.com/api/v1/ads/ssv(use Verify URL to test it). On the server keepAD_REWARDS_MODE="ssv"(the default indeploy/.env). - Control ads in Admin → Ads: on or off, coins per rewarded ad, rewarded ads per day, an interstitial every N games, banners on or off.
/api/v1/ads/ssv with a signed message; the server checks Google's signature (with Google's published keys), stores the confirmed view, and only then pays the coins — once per confirmed view, within the daily limit you set. A request without Google's confirmation gets nothing. Admin → Setup (and pnpm run doctor) shows the callback URL and how many confirmed views arrived.Google's test ad ids never send verification callbacks. So without your own rewarded ad ids, "Watch an ad" shows "No ad available right now", while interstitials and banners keep showing Google's test ads. For local testing only, AD_REWARDS_MODE="demo" on the server accepts rewarded ads without Google's callback — never use it on a live server, because anyone could claim the coins.
Other rewards are checked on the server too. Referral codes work only for signed-in players (not guests) in their first 7 days, only with the code of an older active player, never in a circle, and both sides are paid only after the new player's first real quiz (5 correct answers), with at most 20 paid invites per player and 5 per day. A guest's progress can be merged into an account only with the guest's own sign-in token, and each account takes in one guest only. Quiz coins are scored by the server from its own answer key.
When Google's confirmation is late or never comes
- Late (usually a second or two, sometimes longer): the app waits up to 10 seconds. If Google's callback arrives after that, the view is kept as confirmed-but-unpaid, and the worker pays it within a minute, once, inside the daily limit, with an inbox message "Your ad reward arrived".
- The app went offline right after the ad: the same worker step pays it when Google's callback reaches your server.
- Never (for example the callback URL is wrong, or test ad ids are used): nothing is paid, and the player sees "Google hasn't confirmed this ad yet". Check Admin → Setup check and the ad unit's Verify URL button in AdMob. Confirmed views older than a day are not paid.
Other networks join through AdMob mediation, set up in your AdMob account.
13AI question generator
Costs: Anthropic, OpenAI and Google Gemini charge per use (per token) on their own pricing pages; Gemini has a free tier with limits. The monthly quota in Admin → Settings → AI caps how many questions the generator writes.
Admin → AI Generator writes new questions for any topic, level and question type; you review them before they go live. Set one of ANTHROPIC_API_KEY, OPENAI_API_KEY or GEMINI_API_KEY in deploy/.env, or paste a key in Admin → Settings → AI. Without a key the page says "add your key", and you can still write questions by hand or import a CSV.
14Email
Costs: your SMTP provider's pricing applies; most have a free monthly allowance, then a fee per email.
Staff invites and password resets are sent by email. Set SMTP_HOST, SMTP_PORT (587, or 465 with SMTP_SECURE=true), SMTP_USER, SMTP_PASS from any provider (Amazon SES, Postmark, Mailgun, Brevo, your host). Without SMTP_HOST, emails are written to the server log instead (docker compose logs web), which is handy while testing. Player password resets are sent by Firebase.
15Sign-in methods
- Guest: Firebase Anonymous. Turn guest play on or off in Admin → Settings. A guest who signs in later keeps their coins and progress. The website's shared-quiz links use guest play for "Play in browser — no sign-up".
- Google: on in Firebase; Android needs the SHA fingerprints and
GOOGLE_SERVER_CLIENT_ID, iOS needsGOOGLE_REVERSED_CLIENT_ID(chapter 4). - Apple (required by Apple when you offer Google sign-in on iOS): Apple Developer → your app id → enable Sign in with Apple. In Firebase → Authentication → Apple, turn it on (for iOS only, the bundle id is enough).
- Email and password: players can create an account with email on the app and website; staff sign in at
/adminwith the account the wizard or an invite made.
DEMO_MODE empty on your real site. DEMO_MODE=true is for a public demo only: it adds one-click demo sign-in buttons and resets the data every night.16Rebrand: name, app id, colour
One command changes the app name everywhere (app, website, admin, emails, push topic), the Android application id and iOS bundle id, the brand colour (with its lighter and darker shades), the website font and the icons:
cd brainrush
node tools/rename.mjs --name "QuizNova" --id com.mycompany.quiznova --color "#2563EB" --font "DM Sans" --icon my-icon-1024.png
Every option is optional, so you can run it again later for one change. Add --dry to see what would change. Afterwards:
cd brainrush_mobile_flutter
flutter pub get
dart run flutter_launcher_icons
dart run flutter_native_splash:create
then add the Firebase apps for the new app id (chapter 4) and rebuild the website (docker compose up -d --build). The folder names (brainrush_mobile_flutter and so on) stay as they are; people never see them. The app name shown inside the running app and website comes from Admin → Settings → App name too.
ink text on those keys in br_colors.dart.By hand: the app's colours are in brainrush_mobile_flutter/lib/theme/br_colors.dart (a light and a dark set) and the website's in brainrush_web_nextjs/src/app/globals.css (--br-primary and friends).
17Logo, icon and fonts
- Icons inside the app: Phosphor Icons (bold), bundled as a font in
assets/fonts/Phosphor-Bold.ttf. Every icon the app uses is listed once inlib/theme/app_icons.dart, so you can swap one there. - App icon: replace the 1024×1024 PNGs in
brainrush_mobile_flutter/assets/icon/:icon.png(full square, no transparency), and for Android's round and squircle iconsicon_foreground.png(your symbol on a transparent background, inside the middle two thirds) andicon_background.png. Then rundart run flutter_launcher_icons. - Logo in the app and launch screen:
assets/images/logo.png(512×512). The launch screen colour is underflutter_native_splash:inpubspec.yaml; rundart run flutter_native_splash:createafter a change. - Website icons: replace
brainrush_web_nextjs/src/app/icon.png(256×256) andapple-icon.png(180×180). - App fonts: Bricolage Grotesque (words) and Space Mono (numbers, timers and labels). Put your
.ttffiles inassets/fonts/, list them underfonts:inpubspec.yaml, and change the family names inlib/theme/app_theme.dart. Check the font's licence allows apps. - Website fonts:
--fontin the rename tool swaps Bricolage Grotesque for another Google Font; Space Mono for numbers is set inbrainrush_web_nextjs/src/app/layout.tsx(next/font/google).
18Basic edits
| I want to change… | Where |
|---|---|
| Coins per right answer, level rewards, lifeline prices, streak freeze price, welcome and referral coins, questions per level, seconds per question, stars rules | Admin → Coin Economy |
| Coin packs, prices shown, store product ids | Admin → Coin Packs & IAP |
| Categories, subcategories, premium (coin-locked) categories | Admin → Categories |
| Questions | Admin → Questions (editor, CSV import), Admin → AI Generator |
| Today's and upcoming daily quizzes | Admin → Daily Quiz (the worker fills the next 7 days on its own) |
| Contests, entry fees and prize split | Admin → Contests |
| Exams for a class or a company | Admin → Exams |
| Battle rules (entry fees, bot wait, room size) | Admin → Battles |
| League tiers, players per league, how many move up and down, board rewards | Admin → Leagues |
| Badges | Admin → Badges |
| Fun & Learn articles and their quizzes | Admin → Fun & Learn |
| App name, support email, store links, FAQ, guest play, minimum app version (force update), maintenance mode | Admin → Settings |
| Terms and privacy pages | Admin → Settings (or point the links at your own pages) |
| App texts | The page files in brainrush_mobile_flutter/lib/pages/ |
| Website texts | The page files in brainrush_web_nextjs/src/app/(site)/ and src/components/site/ |
Permission texts (iOS)
iOS shows these when the app first asks. Change them in brainrush_mobile_flutter/ios/Runner/Info.plist; keep them honest and specific, or App Review rejects the build.
| Key | When iOS shows it | Text in the kit |
|---|---|---|
NSPhotoLibraryUsageDescription | Picking a profile photo or a picture for a quiz question | Choose a photo for your profile picture or for a question in a quiz you make. |
NSCameraUsageDescription | Taking that photo with the camera | Take a photo for your profile picture or for a question in a quiz you make. |
NSUserTrackingUsageDescription | Before personalised ads (App Tracking Transparency) | Your data is used to show ads that are more relevant to you. |
19Question bank: categories, CSV import, AI and reports
Each question belongs to a subcategory and a level. A level is played with questions per level picked at random from its pool, so put more questions in a level than are played (for example 15–20 for 10 played) for variety.
- Types: multiple choice (2–4 options), true/false, image (a picture plus options), audio (a sound plus options), maths (a formula plus options) and guess the word (the player builds the answer from letter tiles).
- CSV import (Admin → Questions → Import, up to 5,000 rows): download the template from that page, fill one row per question, and upload. The page checks every row and shows the problems before anything is saved.
- Pictures and sounds: upload them in the question editor (stored in your S3 storage).
- AI generator (Admin → AI Generator): pick a subcategory, level, difficulty and question types; drafts wait for a person to approve, edit or reject them (AI chapter).
- Bulk tools (Admin → Questions): select many questions to publish, unpublish or delete at once; drag categories into the order players see in Admin → Categories → Reorder.
- Players can report a wrong question; reports land in Admin → Reports with the reason and how often the question is answered right, so you fix the worst first.
20Game modes, step by step
Every mode is played against the server's clock and answer key: the app shows questions, the server decides what is right and what it pays. The table says how each mode plays and where you change it.
| Mode | How it plays | Change it in |
|---|---|---|
| Quiz Zone (levels) | Category → subcategory → level. A level asks questions per level picked at random from its pool. 6 right (default) passes with 2 stars and unlocks the next level; 70% and up gives 3 stars. Lifelines: 50/50, audience poll, skip, extra time, each for coins. | Admin → Categories, Questions, Coin Economy |
| Daily quiz | One set for everyone per day, one try. 8 or more right pays a bonus and keeps the streak. The worker fills the next 7 days on its own; you can pick the questions for any day. | Admin → Daily Quiz |
| 1v1 battle | Quick match with a random player (a bot joins after 20 seconds when nobody is free), or a code to share with a friend. 5 questions, 15 seconds each, points for right answers plus a speed bonus. Both players pay the entry fee; the winner takes the pot. | Admin → Battles |
| Group room | A host opens a room for up to 8 players, picks the number of questions, seconds and entry fee, shares the code and starts. The pot is split 60 / 25 / 15 between the top three. | Admin → Battles |
| Contests | Scheduled events with an entry fee (or free), one attempt and a coin prize pool. The top scores share the pool; the worker pays it out once when the contest ends. | Admin → Contests |
| Exams | For a class or a company: players enter the exam code, answer within the time limit and see the result when you release it. Leaving the app counts as a warning; after the set number of warnings the exam is handed in automatically. | Admin → Exams |
| Self-challenge | The player picks a category, the number of questions and the time, and tries to beat their own best. | (no settings) |
| Fun & Learn | Short articles with a quiz at the end; reading and passing pays coins once. | Admin → Fun & Learn |
| Guess the word | The player builds the answer from letter tiles; a hint costs coins. | Admin → Questions (type: guess the word) |
| Player-made quizzes | Players write their own quiz in the app, publish it and share a link or QR code. Friends can play it in the browser without signing up and see a scoreboard. | Admin → Settings (share link base) |
Progress: XP, streaks, leagues and badges
- XP and levels: every game pays XP; levels unlock avatar frames (bronze, silver, gold, diamond).
- Streaks: one finished game a day keeps the streak. A streak freeze (bought with coins, up to 2 held) covers one missed day; a lost streak can be bought back the same day.
- Weekly leagues: every Monday players join a group of 30 in their tier (bronze → diamond). XP earned that week ranks them; on Sunday night the top move up and the bottom move down, and the worker pays the tier rewards. Daily and weekly boards pay the top three when they reset.
- Badges: earned for milestones (games, streaks, battles won, perfect levels…). Edit the list and the rules in Admin → Badges.
21Coins and fair play
Coins are the game's money, so every coin change is decided on the server, inside a database transaction, and written to the coin history (coin_tx). The app only shows the balance the server sends. These rules are on by default:
| Rule | What it stops | Change it in |
|---|---|---|
| Answers are checked on the server, never in the app | A changed app sending "all correct" | (always on) |
| Fastest paid answer: a game finished faster than 700 ms per question (by the server's clock) pays no coins | Scripts that answer instantly to farm coins | Admin → Coin Economy |
| Play coins per day: levels and Fun & Learn pay at most 600 coins a day per player (the daily quiz, battles and contests are separate). Finishing many games at the same moment cannot pass it. | Replaying an easy level all day | Admin → Coin Economy |
| Rewarded ads pay only after Google confirms the view (AdMob server-side verification), at most 5 a day; parallel claims are counted under a lock | Fake "ad watched" calls and racing the daily limit | Admin → Ads (Ads chapter) |
| Each store purchase is granted once per transaction id, whether the app or RevenueCat's webhook reports it first; the webhook needs your shared secret | Double coins and forged purchase calls | deploy/.env (Purchases chapter) |
| Refunds: when Apple or Google refund a purchase, RevenueCat tells the server, which takes the coins back (down to zero, never below) and turns ads back on for a refunded "Remove ads". Refunded and demo purchases are left out of the revenue reports. | Buy, spend, refund | (always on) |
| Contest entry fees are paid once; the last free seat of a full contest goes to one player only; prizes are paid once | Double entries and overfull contests | (always on) |
| Battle rounds are timed by the server; an answer after the clock does not count | Slow-network or changed-clock tricks | Admin → Battles |
| Referral coins are paid only after the new player's first real quiz, with daily and lifetime limits | Fake accounts made for referral coins | Admin → Coin Economy |
22Extend it: a new API route and a new worker job
Two small examples that follow the kit's own patterns. Make the change, run pnpm typecheck and pnpm test in brainrush_web_nextjs, then rebuild (docker compose up -d --build).
A new API route: a random fact for Home
- Create
brainrush_web_nextjs/src/lib/server/handlers/facts.ts:import { prisma } from "@/database/prisma_client/database"; import { json } from "@/lib/server/http"; import { route } from "@/lib/server/router"; /** A random explanation from a live question, for a "Did you know?" card. */ route("GET", "/facts/random", async ({ auth }) => { await auth.user(); // signed-in players only (guests count) const where = { status: "live" as const, explanation: { not: "" } }; const count = await prisma.question.count({ where }); const [q] = await prisma.question.findMany({ where, skip: Math.floor(Math.random() * Math.max(count, 1)), take: 1 }); return json({ fact: q?.explanation ?? null }); }); - Register it with the others in
src/lib/server/api.ts: addimport "@/lib/server/handlers/facts";. - Call it from the app:
ref.watch(apiGet('/facts/random'))gives you{fact: …}with loading and error states (seelib/pages/home/home_page.dartfor the same pattern with/me/home).
A new worker job: a Sunday reminder
The worker (src/worker/index.ts) runs a list of timed tasks; only one worker runs them even if you start two. Add an entry to tasks:
{
name: "sunday-reminder",
everyMs: 3_600_000, // checks once an hour
last: 0,
run: async () => {
const now = new Date();
if (now.getUTCDay() !== 0 || now.getUTCHours() !== 17) return;
// a scheduled campaign, as if made in Admin → Notifications; the "campaigns" task sends it within a minute
await prisma.pushCampaign.create({ data: { title: "Leagues close tonight", body: "One more game could move you up a tier.", audience: "all", scheduledAt: now } });
}
}
PushCampaign in schema.prisma before you copy this; the Database chapter lists every table. A task that sends something should remember that it ran (for example in the settings table, as the heartbeat task does) so a worker restart in the same hour does not send twice.23Every key, explained
"Where" says which .env file it goes in: Docker = deploy/.env (website and worker in the Docker stack), website = brainrush_web_nextjs/.env (without Docker), app = brainrush_mobile_flutter/.env. Admin → Setup check shows the same list with the live status of each key.
| Key | Required | Where | What it does |
|---|---|---|---|
| Database | |||
DATABASE_URL | Yes | website | PostgreSQL connection string |
POSTGRES_DB | Yes | Docker | Database the Docker stack creates |
POSTGRES_USER | Yes | Docker | Database user for the Docker stack |
POSTGRES_PASSWORD | Yes | Docker | Database password for the Docker stack |
| App | |||
APP_URL | Yes | website, Docker | Public URL of the website/API, e.g. https://quiz.example.com |
NEXT_PUBLIC_APP_NAME | — | website, Docker | Name shown before the admin sets one |
WEB_PORT | — | Docker | Host port the web container listens on |
DOMAIN | — | Docker | Your domain for automatic HTTPS (docker compose --profile https) |
CORS_ORIGINS | — | website, Docker | Browser apps allowed to call /api/v1 (Flutter web builds) |
DEMO_MODE | — | website, Docker | "true" only for a public demo: one-click demo sign-in |
| Firebase sign-in | |||
NEXT_PUBLIC_FIREBASE_API_KEY | Yes | website, Docker | Firebase web app config |
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN | Yes | website, Docker | Firebase web app config |
NEXT_PUBLIC_FIREBASE_PROJECT_ID | Yes | website, Docker | Firebase web app config |
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET | — | website, Docker | Firebase web app config |
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID | — | website, Docker | Firebase web app config |
NEXT_PUBLIC_FIREBASE_APP_ID | Yes | website, Docker | Firebase web app config |
| Firebase Admin + push | |||
FIREBASE_PROJECT_ID | Yes | website, Docker | Firebase project id |
FIREBASE_CLIENT_EMAIL | Yes | website, Docker | Service account e-mail |
FIREBASE_PRIVATE_KEY | Yes | website, Docker | Service account private key |
| In-app purchases | |||
REVENUECAT_SECRET_KEY | — | website, Docker | RevenueCat secret API key — verifies store purchases (empty = demo purchases) |
REVENUECAT_WEBHOOK_AUTH | — | website, Docker | Authorization header RevenueCat sends to /api/v1/webhooks/revenuecat |
| Ads | |||
AD_REWARDS_MODE | — | website, Docker | "ssv" (default): rewarded ads pay only after AdMob server-side verification; "demo": testing only |
| In-app purchases | |||
DEMO_PURCHASES | — | website, Docker | "true" grants purchases without payment while RevenueCat is not set (testing only) |
| AI question generator | |||
ANTHROPIC_API_KEY | — | website, Docker | Claude API key (or set it in Admin → Settings → AI) |
OPENAI_API_KEY | — | website, Docker | OpenAI API key |
GEMINI_API_KEY | — | website, Docker | Google Gemini API key |
| Storage | |||
S3_ENDPOINT | — | website, Docker | S3-compatible endpoint (MinIO, R2); empty for AWS |
S3_REGION | — | website, Docker | Bucket region |
S3_BUCKET | — | website, Docker | Bucket for photos and documents (empty = local uploads folder) |
S3_ACCESS_KEY_ID | — | website, Docker | Storage access key |
S3_SECRET_ACCESS_KEY | — | website, Docker | Storage secret key |
SMTP_HOST | — | website, Docker | SMTP server; empty = emails are printed to the log |
SMTP_PORT | — | website, Docker | 587 (STARTTLS) or 465 (TLS) |
SMTP_USER | — | website, Docker | SMTP user |
SMTP_PASS | — | website, Docker | SMTP password |
SMTP_SECURE | — | website, Docker | "true" for port 465 (TLS); empty for 587 |
| Storage | |||
UPLOAD_DIR | — | website | Folder for uploads when no bucket is set (default ./uploads) |
| Mobile apps | |||
API_BASE_URL | Yes | app | Your server; the app calls <url>/api/v1 |
APP_NAME | — | app | App name in the UI |
FIREBASE_PROJECT_ID | Yes | app | Firebase project id |
FIREBASE_MESSAGING_SENDER_ID | Yes | app | Firebase config |
FIREBASE_STORAGE_BUCKET | — | app | Firebase config |
FIREBASE_ANDROID_API_KEY | Yes | app | Firebase Android config |
FIREBASE_ANDROID_APP_ID | Yes | app | Firebase Android config |
FIREBASE_IOS_API_KEY | Yes | app | Firebase iOS config |
FIREBASE_IOS_APP_ID | Yes | app | Firebase iOS config |
FIREBASE_IOS_CLIENT_ID | — | app | Google sign-in on iOS |
FIREBASE_IOS_BUNDLE_ID | — | app | iOS bundle id |
GOOGLE_SERVER_CLIENT_ID | — | app | Web OAuth client id — Google sign-in on Android |
SUPPORT_EMAIL | — | app | Fallback support e-mail until the server config loads |
REVENUECAT_APPLE_KEY | — | app | RevenueCat public iOS key (empty = demo purchases) |
REVENUECAT_GOOGLE_KEY | — | app | RevenueCat public Android key (empty = demo purchases) |
ADMOB_ANDROID_APP_ID | — | app | AdMob Android app id (empty = Google test ads) |
ADMOB_ANDROID_REWARDED_ID | — | app | AdMob Android rewarded ad unit id (empty = Google test ads, which pay only with AD_REWARDS_MODE=demo) |
ADMOB_IOS_REWARDED_ID | — | app | AdMob iOS rewarded ad unit id (empty = Google test ads, which pay only with AD_REWARDS_MODE=demo) |
ADMOB_ANDROID_INTERSTITIAL_ID | — | app | AdMob Android interstitial ad unit id (empty = Google test ads) |
ADMOB_IOS_INTERSTITIAL_ID | — | app | AdMob iOS interstitial ad unit id (empty = Google test ads) |
ADMOB_ANDROID_BANNER_ID | — | app | AdMob Android banner ad unit id (empty = Google test ads) |
ADMOB_IOS_BANNER_ID | — | app | AdMob iOS banner ad unit id (empty = Google test ads) |
DEMO_SIGN_IN | — | app | "true" shows one-tap demo sign-in (your public demo only) |
24File structure
Mobile app (brainrush_mobile_flutter/lib)
main.dart loads .env, starts Firebase, runs the app (short on purpose)
app.dart theme, router and app-wide listeners
router.dart every route (go_router) and the sign-in / setup gates
config/ .env values (app_env.dart) and Firebase options
api/ the typed client for your server (/api/v1)
models/ data models parsed from the API
providers/ app-wide state (Riverpod): session, config, wallet, home…
services/ push, purchases (RevenueCat or demo), ads (AdMob), sounds, sign-in
theme/ colours (br_colors.dart), fonts and text styles
components/ shared widgets, names start with cc_ (buttons, cards, sheets, avatars…)
pages/ one folder per area: launch, auth, home, play, quiz, results, battle, compete,
leagues, exam, learn, create, store, profile, settings…
utils/ formatting and small helpers
Lists and grids use the lazy .builder constructors, and state flows through Riverpod providers rather than long chains of widget parameters.
Server (brainrush_web_nextjs/src)
app/(site)/ the public website (landing, play, leaderboard, contests, shared quizzes…)
app/(game)/quiz/ the full-screen quiz player
app/admin/ Admin panel pages
app/install/ the first-run wizard
app/api/v1/[...path]/ the single API entry point; routes live in lib/server/handlers/
components/site|play|panel|ui website, quiz player, admin and base UI (shadcn) components
lib/server/handlers/ every API route by area: play, compete, social, me, admin…
lib/server/ games and scoring, battles, contests, leagues, exams, economy, purchases,
storage, email, AI, push, settings, setup check
database/prisma_client/ database schema (schema.prisma) and migrations
database/seed/ the sample quizzes, badges and coin packs
worker/ background jobs (pnpm worker)
scripts/doctor.ts the setup check in a terminal
tests/ API tests (Vitest); e2e/ has browser smoke tests (Playwright)
25API reference (181 routes)
The app, the website and the Admin panel all use the same JSON API under https://your-domain.com/api/v1. Signed-in calls send the Firebase ID token as Authorization: Bearer <token>; the server checks it and finds the player. Errors come back as {"error": {"code": "…", "message": "…"}} with a matching HTTP status. This table is generated from the route files when the download is built, so it lists exactly the routes the server has.
| Method | Path | Sign-in | What it does |
|---|---|---|---|
| System, files and webhooks | |||
GET | /api/v1/health | No | Health check for Docker and uptime monitors |
GET | /api/v1/firebase-config.js | No | The Firebase web config, read when the page loads rather than baked in at build time, so one Docker image works with any Firebase project |
GET | /api/v1/ads/ssv | Signed by the provider | AdMob server-side verification callback (set it on every rewarded ad unit in AdMob) |
GET | /api/v1/config | No | Everything a client needs before the first screen: names, links, version gate, maintenance, economy, ads, purchases |
GET | /api/v1/legal/:doc | No | Terms of use or privacy policy text |
GET | /api/v1/files/* | No | Serve an uploaded file |
POST | /api/v1/uploads | Staff: content editor or super admin | Multipart upload (field `file`, optional `folder`: avatars · quizzes for players; questions · articles · categories · badges for staff) |
POST | /api/v1/auth/demo | No | One-click demo sign-in (DEMO_MODE only): a custom token for the seeded demo player or the read-only demo admin |
POST | /api/v1/webhooks/revenuecat | Signed by the provider | RevenueCat purchase, renewal and refund events (shared secret) |
GET | /api/v1/install/status | No | Whether the first admin still needs to be created |
POST | /api/v1/install | No | Only while no super admin exists: creates the owner's Firebase email account and staff row, names the app, loads sample content |
| Account, inbox and devices | |||
GET | /api/v1/me | Signed in | The player's profile, coins, level and stats |
PATCH | /api/v1/me | Signed in | Update name, username, avatar, language or settings |
GET | /api/v1/me/username | Signed in | Check whether a username is free |
GET | /api/v1/me/home | Signed in | Home: the player, streak (saved / lost dialogs), league card, daily quiz card and pending celebrations in one call |
POST | /api/v1/me/celebrations/:id/seen | Signed in | Mark a celebration (contest win, level up) as seen |
POST | /api/v1/me/streak/freeze | Signed in | Buy a streak freeze (max 2 equipped) |
POST | /api/v1/me/streak/restore | Signed in | Restore a streak that broke today |
GET | /api/v1/me/stats | Signed in | Accuracy by category from finished games |
GET | /api/v1/me/coins | Signed in | The player's coin history |
GET | /api/v1/me/referrals | Signed in | The player's invite code and friends who joined |
POST | /api/v1/me/referral | Signed in (not a guest) | A friend's code, checked on the server: members only (no guests), within 7 days of signing up, from an older active member, never the player's own invitee, once per account |
POST | /api/v1/me/devices | Signed in | Register a device for push messages |
DELETE | /api/v1/me/devices | Signed in | Unregister a device from push messages |
GET | /api/v1/notifications | Signed in | The player's inbox |
POST | /api/v1/notifications/read-all | Signed in | Mark every notification read |
POST | /api/v1/notifications/:id/read | Signed in | Mark one notification read |
POST | /api/v1/feedback | Optional | Send feedback to the team |
DELETE | /api/v1/me | Signed in | Guests are deleted at once; accounts are scheduled for 30 days (signing in again cancels it) |
POST | /api/v1/me/export | Signed in (not a guest) | Download my data: a copy of the player's data, sent to their email |
POST | /api/v1/me/merge | Signed in (not a guest) | A guest signs in to an account that already has progress |
GET | /api/v1/me/notification-defaults | No | Which notifications are on by default |
| Categories, solo games, bookmarks and Fun & Learn | |||
GET | /api/v1/categories | Optional | Categories: every live category with the player's progress, lock state and "new" flag |
GET | /api/v1/categories/:id | Optional | A category with its subcategories and the player's progress |
POST | /api/v1/categories/:id/unlock | Signed in | Unlock a premium category forever |
GET | /api/v1/subcategories/:id/levels | Optional | Levels: stars per level and which levels are open |
GET | /api/v1/subcategories/:id/pack | Signed in | Offline pack: the full question pool of one open level with answers, for play without a connection |
GET | /api/v1/search | Optional | Search: subcategories and categories by name (the app also filters its loaded list on device) |
GET | /api/v1/interests | No | Pick Interests: the category list without progress |
POST | /api/v1/games | Signed in | Start a game (level, self-challenge, guess the word, shared quiz, article quiz) |
GET | /api/v1/games/:id | Signed in | A game in progress |
POST | /api/v1/games/:id/lifeline | Signed in | Use a lifeline (50/50, poll, skip, extra time) for coins |
POST | /api/v1/games/:id/answer | Signed in | Answer a question; the server checks it |
POST | /api/v1/games/:id/finish | Signed in | Finish a game and pay coins and XP |
POST | /api/v1/games/:id/quit | Signed in | Quit: the game ends without rewards (coins spent on lifelines stay spent) |
GET | /api/v1/games/:id/result | Signed in | A finished game's result |
GET | /api/v1/games/:id/review | Signed in | A finished game's questions with the right answers |
POST | /api/v1/games/sync | Signed in | Upload games played offline |
GET | /api/v1/self/best | Signed in | The player's best with the same self-challenge settings |
GET | /api/v1/bookmarks | Signed in | The player's bookmarked questions |
POST | /api/v1/questions/:id/bookmark | Signed in | Bookmark a question |
DELETE | /api/v1/questions/:id/bookmark | Signed in | Remove a bookmark |
POST | /api/v1/questions/:id/report | Signed in | Report: question bank questions only; three or more open reports flag the question for editors |
GET | /api/v1/report-reasons | No | Reasons a player can pick when reporting a question |
GET | /api/v1/articles | Optional | Fun & Learn articles |
GET | /api/v1/articles/:id | Optional | One article with its short quiz |
POST | /api/v1/articles/:id/read | Signed in | Marks an article as read ("In progress" on the articles list) and counts the read for editors |
| Daily quiz, leaderboards, leagues, contests, battles and exams | |||
GET | /api/v1/daily | Optional | Today's daily quiz and whether the player has played it |
POST | /api/v1/daily/start | Signed in | Start today's daily quiz (one try per day) |
GET | /api/v1/leaderboards/:period | Optional | Daily, weekly or all-time leaderboard |
GET | /api/v1/leagues/current | Signed in (not a guest) | The player's league group this week |
GET | /api/v1/leagues/tiers | No | League tiers and their rules |
GET | /api/v1/badges | Signed in | Every badge and the player's progress |
GET | /api/v1/contests | Optional | Live, upcoming and past contests |
GET | /api/v1/contests/:id | Optional | One contest with the player's entry |
POST | /api/v1/contests/:id/join | Signed in (not a guest) | Join a contest (pays the entry fee once) |
POST | /api/v1/contests/:id/remind | Signed in (not a guest) | Remind the player when a contest starts |
GET | /api/v1/contests/:id/leaderboard | Optional | A contest's live ranking |
GET | /api/v1/battles/hub | Signed in | Battle tab: players online, entry fee and recent battles |
POST | /api/v1/battles/quick | Signed in (not a guest) | Find a random opponent for a 1v1 battle |
POST | /api/v1/battles/friend | Signed in (not a guest) | Create a 1v1 battle with a code for a friend |
POST | /api/v1/battles/join | Signed in (not a guest) | Join a battle or room by code |
POST | /api/v1/rooms | Signed in (not a guest) | Create a group battle room for up to 8 players |
GET | /api/v1/battles/:id | Signed in | Battle state: players, question, scores |
POST | /api/v1/battles/:id/bot | Signed in | Fill an empty seat with a bot when nobody joins |
POST | /api/v1/battles/:id/start | Signed in | Start a room (host only) |
POST | /api/v1/battles/:id/kick | Signed in | Remove a player from a room (host only) |
POST | /api/v1/battles/:id/leave | Signed in | Leave a battle or room |
POST | /api/v1/battles/:id/answer | Signed in | Answer the current battle question |
POST | /api/v1/battles/:id/rematch | Signed in | Ask the same players for a rematch |
POST | /api/v1/exams/lookup | Signed in (not a guest) | Find an exam by its code |
POST | /api/v1/exams/:id/start | Signed in (not a guest) | Start an exam attempt |
PATCH | /api/v1/exam-attempts/:id | Signed in (not a guest) | Save the answers of an exam in progress |
POST | /api/v1/exam-attempts/:id/leave | Signed in (not a guest) | Record that the player left the exam screen |
POST | /api/v1/exam-attempts/:id/submit | Signed in (not a guest) | Submit an exam |
GET | /api/v1/exam-attempts/:id | Signed in (not a guest) | An exam attempt and its result once released |
| Player-made quizzes, store, purchases and rewarded ads | |||
GET | /api/v1/my-quizzes | Signed in | Quizzes the player made |
POST | /api/v1/my-quizzes | Signed in (not a guest) | Create a quiz |
GET | /api/v1/my-quizzes/:id | Signed in | One of the player's quizzes |
PATCH | /api/v1/my-quizzes/:id | Signed in (not a guest) | Update one of the player's quizzes |
POST | /api/v1/my-quizzes/:id/publish | Signed in (not a guest) | Publish a quiz and get its share link |
DELETE | /api/v1/my-quizzes/:id | Signed in | Delete one of the player's quizzes |
GET | /api/v1/q/:slug | Optional | The public page of a shared quiz with the scores to beat |
GET | /api/v1/quizzes/featured | No | Player-made quizzes picked for the website |
GET | /api/v1/store | Signed in | Coin packs, Remove ads and the player's purchases |
POST | /api/v1/purchases/demo | Signed in | Demo only: grant a store product without paying (refused unless purchases run in demo mode) |
POST | /api/v1/purchases/sync | Signed in | Grant purchases the app store confirmed (checked with RevenueCat) |
POST | /api/v1/ads/rewarded | Signed in | The app shows a rewarded ad with a one-time nonce as AdMob server-side verification data, then claims it here |
| Admin: dashboard, content, import, AI and daily quiz | |||
GET | /api/v1/admin/dashboard | Staff with dashboard access | Dashboard numbers: players, games, revenue and what needs attention |
GET | /api/v1/admin/search | Staff with dashboard access | Search players, questions and contests from the top bar |
GET | /api/v1/admin/categories | Staff with content access | List categories with their question counts |
POST | /api/v1/admin/categories | Staff: content editor or super admin | Create a category |
PATCH | /api/v1/admin/categories/:id | Staff: content editor or super admin | Update a category |
DELETE | /api/v1/admin/categories/:id | Staff: content editor or super admin | Delete an empty category |
POST | /api/v1/admin/categories/reorder | Staff: content editor or super admin | Save the order of categories |
GET | /api/v1/admin/categories/:id/subcategories | Staff with content access | List a category's subcategories |
GET | /api/v1/admin/subcategories | Staff with content access | Every subcategory with its category, for the question editor's picker and filters |
POST | /api/v1/admin/subcategories | Staff: content editor or super admin | Create a subcategory |
PATCH | /api/v1/admin/subcategories/:id | Staff: content editor or super admin | Update a subcategory |
DELETE | /api/v1/admin/subcategories/:id | Staff: content editor or super admin | Delete an empty subcategory |
GET | /api/v1/admin/questions | Staff with content access | List questions with search and filters |
GET | /api/v1/admin/questions/:id | Staff with content access | One question with its stats |
POST | /api/v1/admin/questions | Staff: content editor or super admin | Create a question |
PATCH | /api/v1/admin/questions/:id | Staff: content editor or super admin | Update a question |
DELETE | /api/v1/admin/questions/:id | Staff: content editor or super admin | Delete a question |
POST | /api/v1/admin/questions/bulk | Staff: content editor or super admin | Publish, unpublish or delete many questions at once |
GET | /api/v1/admin/import/template | Staff with content access | Download the CSV import template |
POST | /api/v1/admin/import/check | Staff: content editor or super admin | Upload a CSV and check every row before import |
POST | /api/v1/admin/import/:id/commit | Staff: content editor or super admin | Import the checked rows |
GET | /api/v1/admin/import/:id/skipped | Staff with content access | Download the rows an import skipped, with the reason |
GET | /api/v1/admin/ai/status | Staff with content access | AI generator status: provider, model and this month's quota |
POST | /api/v1/admin/ai/generate | Staff: content editor or super admin | Generates drafts and keeps them as `ai_draft` questions, so they survive a closed tab and count on the dashboard |
GET | /api/v1/admin/ai/drafts | Staff with content access | List AI question drafts waiting for review |
POST | /api/v1/admin/ai/review | Staff: content editor or super admin | Approve, edit or reject AI drafts |
GET | /api/v1/admin/daily | Staff with daily access | Daily quiz calendar for one month |
GET | /api/v1/admin/daily/:day | Staff with daily access | One day's daily quiz with its results |
PUT | /api/v1/admin/daily/:day | Staff: content editor or super admin | Schedule or change a day's daily quiz |
DELETE | /api/v1/admin/daily/:day | Staff: content editor or super admin | Remove a scheduled daily quiz |
POST | /api/v1/admin/daily/autofill | Staff: content editor or super admin | Auto-fill gaps: a mixed quiz for every empty day of the month from today on |
GET | /api/v1/admin/articles | Staff with articles access | List Fun & Learn articles |
GET | /api/v1/admin/articles/:id | Staff with articles access | One article with its quiz |
POST | /api/v1/admin/articles | Staff: content editor or super admin | Create an article |
PATCH | /api/v1/admin/articles/:id | Staff: content editor or super admin | Update an article |
DELETE | /api/v1/admin/articles/:id | Staff: content editor or super admin | Delete an article |
GET | /api/v1/admin/badges | Staff with content access | List badges |
POST | /api/v1/admin/badges | Staff: content editor or super admin | Create a badge |
PATCH | /api/v1/admin/badges/:id | Staff: content editor or super admin | Update a badge |
DELETE | /api/v1/admin/badges/:id | Staff: content editor or super admin | Delete a badge |
| Admin: contests, exams, settings and store products | |||
GET | /api/v1/admin/contests | Staff with contests access | List contests |
POST | /api/v1/admin/contests | Staff: finance, content editor or super admin | Create a contest |
PATCH | /api/v1/admin/contests/:id | Staff: finance, content editor or super admin | Update a contest |
DELETE | /api/v1/admin/contests/:id | Staff: finance, content editor or super admin | Delete a contest nobody joined yet |
GET | /api/v1/admin/contests/:id/results | Staff with contests access | A contest's ranking |
GET | /api/v1/admin/contests/:id/export | Staff with contests access | Download a contest's ranking as CSV |
POST | /api/v1/admin/contests/:id/payout | Staff: finance or super admin | Pay a finished contest's prize pool once |
GET | /api/v1/admin/exams | Staff with exams access | List exams |
POST | /api/v1/admin/exams | Staff: content editor or super admin | Create an exam |
PATCH | /api/v1/admin/exams/:id | Staff: content editor or super admin | Update an exam |
DELETE | /api/v1/admin/exams/:id | Staff: content editor or super admin | Delete an exam |
GET | /api/v1/admin/exams/:id/results | Staff with exams access | An exam's attempts and scores |
POST | /api/v1/admin/exams/:id/release | Staff: content editor or super admin | Release an exam's results to its players |
GET | /api/v1/admin/exams/:id/export | Staff with exams access | Download an exam's results as CSV |
GET | /api/v1/admin/leagues | Staff with settings access | This week's leagues — how many groups, and players per tier |
GET | /api/v1/admin/settings | Staff with dashboard access | Every settings group (secrets are masked) |
PUT | /api/v1/admin/settings/:group | Staff: the group's writer role or super admin | Save one settings group |
GET | /api/v1/admin/products | Staff with money access | List coin packs and the Remove ads product |
POST | /api/v1/admin/products | Staff: finance or super admin | Create a store product |
PATCH | /api/v1/admin/products/:id | Staff: finance or super admin | Update a store product |
DELETE | /api/v1/admin/products/:id | Staff: finance or super admin | Delete a store product |
GET | /api/v1/admin/leaderboards/:period | Staff with leaderboards access | Leaderboard for a period, with players' details |
POST | /api/v1/admin/leaderboards/reset | Staff: moderator or super admin | Reset the weekly board (league standings are separate and keep their XP) |
| Admin: players, reports, push, staff and audit log | |||
GET | /api/v1/admin/me | Staff (any role) | The signed-in staff member and the pages their role may open |
GET | /api/v1/admin/users | Staff with users access | List players with search and filters |
GET | /api/v1/admin/users/export | Staff with users access | Download the player list as CSV |
GET | /api/v1/admin/users/:id | Staff with users access | One player: profile, coins, games and devices |
POST | /api/v1/admin/users/:id/coins | Staff: moderator, finance or super admin | Give or take coins from a player (logged) |
POST | /api/v1/admin/users/:id/ban | Staff: moderator or super admin | Ban — signed out (the API refuses them), off the boards and leagues; optionally take back contest prizes |
POST | /api/v1/admin/users/:id/unban | Staff: moderator or super admin | Lift a player's ban |
GET | /api/v1/admin/reports | Staff with reports access | List reported questions |
GET | /api/v1/admin/reports/question/:id | Staff with reports access | Every report on one question |
POST | /api/v1/admin/reports/question/:id/resolve | Staff: moderator, content editor or super admin | Mark a question's reports fixed or dismissed |
GET | /api/v1/admin/feedback | Staff with reports access | List feedback sent from the app |
PATCH | /api/v1/admin/feedback/:id | Staff: moderator or super admin | Mark feedback read or done |
GET | /api/v1/admin/push | Staff with push access | List sent and scheduled push messages |
GET | /api/v1/admin/push/audience | Staff with push access | Count the players a push audience reaches |
POST | /api/v1/admin/push | Staff: content editor or super admin | Send or schedule a push message |
GET | /api/v1/admin/staff | Staff with roles access | List staff members and their roles |
POST | /api/v1/admin/staff/invite | Staff: super admin | Invite a staff member by email |
PATCH | /api/v1/admin/staff/:id | Staff: super admin | Change a staff member's role or remove their access |
GET | /api/v1/admin/setup | Staff with setup access | Setup check: which keys and services are ready |
GET | /api/v1/admin/audit | Staff with settings access | The audit log of staff actions |
26Database (PostgreSQL)
All data lives in one PostgreSQL database, managed by Prisma. The schema is brainrush_web_nextjs/src/database/prisma_client/schema.prisma and every change ships as a migration in …/prisma_client/migrations/, applied by pnpm prisma:migrate:deploy (the Docker stack runs it on start). Never edit the tables by hand on a live server: add a field in schema.prisma, run pnpm prisma:migrate:dev --name your_change on your computer, and ship the new migration folder.
The 39 tables, generated from the schema:
| Table | What it holds | Keys | Points to |
|---|---|---|---|
| People | |||
users | A player or staff member: profile, coins, XP, streak, league tier, ad and ban status. | PK id unique firebaseUid unique username unique referralCode | referredById → users |
devices | A phone or browser that receives push messages (FCM token). | PK id unique token | userId → users |
| Content: categories, questions, articles | |||
categories | A top-level quiz category (Science, History…) with its icon, colour, order and premium price. | PK id unique slug | — |
subcategories | A topic inside a category; its questions are split into levels. | PK id unique slug | categoryId → categories |
questions | One question: type (multiple choice, true/false, guess the word), options, answer, level, difficulty and status. | PK id unique number | subcategoryId → subcategories |
category_unlocks | A premium category a player unlocked with coins. | PK (userId, categoryId) | userId → users categoryId → categories |
level_progress | A player's best stars on one level of a subcategory. | PK (userId, subcategoryId, level) | userId → users subcategoryId → subcategories |
bookmarks | A question a player saved for later. | PK (userId, questionId) | userId → users questionId → questions |
reports | A player's report on a wrong or unclear question. | PK id | questionId → questions userId → users resolvedById → users |
articles | A Fun & Learn article with a short quiz at the end. | PK id | — |
article_reads | A player who read an article (pays its read bonus once). | PK (userId, articleId) | userId → users articleId → articles |
| Solo games (levels, daily, self-challenge, article, shared quiz, guess the word, contest plays) | |||
game_sessions | One solo game: mode, questions, answers, timing, coins and XP paid. | PK id | userId → users |
daily_quizzes | The question set for one day's daily quiz. | PK day | — |
| Battles (1v1 quick match, friend code, group rooms) — server-timed rounds | |||
battles | A 1v1 battle or group room: code, host, state, questions and server-timed rounds. | PK id unique code | hostId → users |
battle_players | A player (or bot) in a battle with their answers and score. | PK id unique (battleId, userId) | battleId → battles userId → users |
| Contests | |||
contests | A scheduled contest: questions, entry fee, prize pool and split. | PK id | categoryId → categories |
contest_entries | A player's entry and score in a contest. | PK id unique (contestId, userId) | contestId → contests userId → users |
contest_reminders | A player who asked to be told when a contest starts. | PK (contestId, userId) | contestId → contests userId → users |
| Weekly leagues | |||
league_groups | One weekly league group of up to 30 players in a tier. | PK id | — |
league_members | A player's place and XP in a weekly league group. | PK (groupId, userId) | groupId → league_groups userId → users |
xp_days | XP earned per player per day (daily and weekly boards, the dashboard chart). | PK (userId, day) | — |
| Exams | |||
exams | A timed exam with a code, questions and result release. | PK id unique code | — |
exam_attempts | A player's attempt at an exam: answers, score and how often they left the app. | PK id unique (examId, userId) | examId → exams userId → users |
| Player-made quizzes (create, share by link + QR) | |||
user_quizzes | A quiz a player made and shares by link or QR code. | PK id unique slug | ownerId → users categoryId → categories |
user_quiz_plays | One play of a player-made quiz. | PK id | quizId → user_quizzes userId → users |
| Badges | |||
badges | A badge and the rule that earns it. | PK id unique slug | — |
user_badges | A badge a player earned. | PK (userId, badgeId) | userId → users badgeId → badges |
| Coins, purchases, products | |||
coin_txs | Every coin change with its reason (game, ad, purchase, refund, contest, admin), so balances can be audited. | PK id | userId → users |
ad_rewards | A rewarded ad Google confirmed through AdMob server-side verification (GET /api/v1/ads/ssv). The app claims it once, with the nonce it passed as SSV custom data; only then are coins paid. | PK id unique transactionId | — |
products | A store product: a coin pack or Remove ads, with its App Store and Play product id. | PK id unique productId | — |
purchases | A store purchase granted once per transaction id; refunds take it back. | PK id unique transactionId | userId → users |
| System | |||
notifications | A message in a player's inbox. | PK id | userId → users |
celebrations | One-time dialogs the app shows on Home: streak saved / lost, contest prize, league result. | PK id | userId → users |
feedback | Feedback a player sent from the app. | PK id | userId → users |
push_campaigns | A push message staff sent or scheduled, with its audience. | PK id | — |
import_batches | A CSV question import: checked rows, skipped rows and status. | PK id | — |
ai_runs | One AI question-generator run and the drafts it produced. | PK id | — |
settings | One settings group saved from Admin → Settings (overrides the defaults key by key). | PK (group, key) | — |
audit_logs | A staff action: who did what to which record, and when. | PK id | actorId → users |
27Publish to the stores
You publish the app under your own developer accounts. Before you start: set your app id (chapter 16), your Firebase values (chapter 4) and an https API_BASE_URL. The stores ask for a privacy policy link: https://your-domain.com/privacy; terms are at /terms. Players delete their account in the app (Profile → Settings → Account → Delete account).
Google Play
- Create an upload key inside
android/app:cd brainrush_mobile_flutter/android/app && keytool -genkey -v -keystore upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload. Back this file up and never share it. - Create
android/key.properties:
Without this file, release builds are signed with the debug key (fine for testing, refused by Play).storePassword=… keyPassword=… keyAlias=upload storeFile=upload-keystore.jks - Set the version in
pubspec.yaml(version: 1.0.0+1; raise the number after+for every upload), thenflutter build appbundle. - In Play Console: create the app, fill the store listing, content rating, data safety (the app collects account info, game progress, purchase history, device ids for ads and push) and upload
build/app/outputs/bundle/release/app-release.aabto a testing track first. - Add the SHA-1/SHA-256 of Play's app signing key (Play Console → Test and release → App integrity) to Firebase.
App Store
- In the Apple Developer site, register your bundle id with Push Notifications, Sign in with Apple and In-App Purchase.
- Open
ios/Runner.xcworkspacein Xcode → Runner → Signing & Capabilities → choose your team (Automatic signing is already on; chapter 9). - In App Store Connect create the app, fill the listing and App Privacy, and create the in-app purchases (chapter 10).
flutter build ipa, then uploadbuild/ios/ipa/*.ipawith the Transporter app, and send it to TestFlight first.
28Updating
When a new version comes out, read the changelog, back up your database (chapter 5), then copy the new files over your copy, keeping your .env files and any changes you made. Run docker compose up -d --build: database changes are applied automatically on start. Using git for your copy makes this much easier: commit before you copy the update in, and review the differences.
29FAQ and troubleshooting
The site opens the install wizard again
The wizard shows only while no super admin exists. If you see it on a site you already set up, the site is connected to an empty database: check DATABASE_URL / the POSTGRES_* values.
Sign-in fails on the website
Add your domain under Firebase → Authentication → Settings → Authorized domains, check the NEXT_PUBLIC_FIREBASE_* values, and restart (docker compose up -d).
Google sign-in fails on Android
Add the SHA-1 and SHA-256 of the key that signed the build to Firebase, and check GOOGLE_SERVER_CLIENT_ID is the Web client id.
Battles never find an opponent
Matchmaking and the battle clock run in the worker. Check it runs: docker compose logs worker; Admin → Setup check shows when it last answered. With few players, a bot joins after the wait set in Admin → Battles.
Purchases say DEMO
No RevenueCat key is set yet. That is on purpose: add your keys (chapter 10).
Push notifications do not arrive
iOS needs the APNs key in Firebase, and the phone must allow notifications. Admin → Setup check tests the Firebase push connection.
The app shows "Can't reach the server"
API_BASE_URL in the app's .env must be your site's address (https, no /api/v1 at the end). Open https://your-domain.com/api/v1/health in the phone's browser.
Images or sounds in questions do not load
Check the S3_* keys and Admin → Setup check → File storage. With a cloud bucket, the server needs read and write access to it.
I changed .env and nothing happened
Server: docker compose up -d. App: stop it and run it again.
Where are the logs?
docker compose logs -f web and docker compose logs -f worker.
30Credits
The services in Third-party services are separate companies, not part of this item. Each has its own pricing tiers and terms, and you are responsible for any costs from them.
Brainrush is built on these open-source projects. Each keeps its own licence (mostly MIT, BSD-3 or Apache 2.0).
Mobile app
Flutter, flutter_riverpod, go_router, FlutterFire (firebase_core, firebase_auth, firebase_messaging), google_sign_in, sign_in_with_apple, purchases_flutter (RevenueCat), google_mobile_ads, audioplayers, flutter_dotenv, shared_preferences, http, intl, share_plus, url_launcher, package_info_plus, image_picker, qr_flutter, connectivity_plus, flutter_launcher_icons, flutter_native_splash.
Website, Admin, API and worker
Next.js, React, Prisma, PostgreSQL, pg, Zod, Tailwind CSS, shadcn/ui, Base UI, Phosphor Icons, Sonner, next-themes, qrcode, Nodemailer, Firebase JS SDK, Firebase Admin SDK, AWS SDK for JavaScript, MinIO, Caddy, Docker.
Fonts
Bricolage Grotesque and Space Mono, SIL Open Font License 1.1 (licence files in assets/fonts/). Phosphor Icons, MIT licence.
Content
The sample questions, explanations, articles, flag pictures, audio clips, sound effects, category names and player names are original and part of your licence.
31Changelog
1.0.0 — 2026-10-22
First release. See CHANGELOG.md in the download for the full list.
32Support
Questions or a problem? Use the Support tab on the item page on CodeCanyon, with your purchase code, what you did, and what you saw (a screenshot and the output of pnpm run doctor help a lot). Support covers questions about the item, help with bugs, and help with the third-party services it uses as far as this documentation goes. It does not cover changes or new features you want to build; for that, or to have it installed for you, ask about our installation service on the item page.