Brainrush · Docsv1.0.0
Documentation · version 1.0.0

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

FolderWhat 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.mjsRebrands 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.

Nothing is blocked on a key. To try coin purchases before you add RevenueCat keys, set 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)

Home
Home
Play
Play
Quiz
Quiz
Answer feedback
Answer feedback
Battles
Battles
1 v 1 battle
1 v 1 battle
Battle result
Battle result
Leagues
Leagues
Contests
Contests
Coin store
Coin store
Daily quiz (dark)
Daily quiz (dark)
Home (dark)
Home (dark)

Website

Home
Home
Web quiz
Web quiz
Result
Result
Leaderboard
Leaderboard

Admin panel (/admin)

Dashboard
Dashboard
Questions
Questions
AI question generator
AI question generator
Daily quiz
Daily quiz
Contests
Contests
Users
Users

02Requirements

To run the server

To build the mobile app

Third-party services (accounts you will create)

These services have their own pricing. Each one is run by another company, not by us, and has its own pricing tiers, usage limits and fees (for example per SMS sent, per payment taken, per map or AI request, or for storage and bandwidth). Most offer a free tier or a free test mode, but prices and limits can change at any time. You are responsible for any costs from these providers, as well as your server, domain, Apple Developer Program and Google Play Console accounts. Check each provider's pricing page before you go live.
ServiceNeeded forWhat it costs (check their pricing page)
FirebaseSign-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.
RevenueCatIn-app purchases (optional)Free up to a monthly revenue limit, then a share of revenue.
Google AdMobAds (optional)Free; Google pays you a share of ad revenue.
Anthropic, OpenAI or Google GeminiAI question generator (optional)Pay per use (per token); Gemini has a free tier with limits.
Any SMTP providerStaff invites and password emails (optional)Many have a free monthly allowance, then a fee per email.
Apple Developer Program / Google Play ConsolePublishing the appApple: yearly fee. Google: one-time fee.
A VPS and a domainThe serverMonthly hosting and a yearly domain fee from your 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.

  1. 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
  2. In .env, fill at least these values:
    KeyWhat to put
    POSTGRES_PASSWORDA long random password, letters and digits only (run openssl rand -hex 24 to 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.
  3. Start everything:
    docker compose up -d
    The first build takes 5–10 minutes. Each start creates or updates the database tables, then loads the badges and coin packs.
  4. Open APP_URL in 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/health shows {"ok":true,…}.
  5. Put it on your domain with HTTPS (chapter 5).
Port 3000 already used by something else on the server? Set WEB_PORT in .env.
Changed .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.

Firebase is a third-party service with its own pricing. Firebase is run by Google, not by us. It has its own plans and pricing structure (the free Spark plan and the pay-as-you-go Blaze plan), and some features can cost money as you grow, for example Authentication beyond the free monthly users or phone sign-in messages. Prices and free limits can change at any time. You are responsible for any Firebase costs your project incurs. Check firebase.google.com/pricing before you go live.
  1. Go to the Firebase console → Add project. Give it your app name.
  2. Build → Authentication → Get started → Sign-in method. Turn on: Anonymous (guest play), Google, Email/Password (players and staff) and Apple (see chapter 15).
  3. Authentication → Settings → Authorized domains: add your-domain.com.
  4. Project settings (gear) → General → Your apps → Add app → Web (name it "Website"). Copy the values from the firebaseConfig shown into deploy/.env:
    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:…"
    The website reads these when it starts, so a restart (docker compose up -d) is enough after a change.
  5. Project settings → Service accounts → Generate new private key. A JSON file downloads. Copy three values from it into deploy/.env:
    FIREBASE_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"
    Keep the \n as they are in the file. Keep this file secret.
  6. 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). Download google-services.json and GoogleService-Info.plist: you only need to copy values out of them (the app does not use the files themselves). Or run flutterfire configure, which does the same and prints the values.
  7. Put the values in brainrush_mobile_flutter/.env:
    Key in the app's .envWhere 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_key and mobilesdk_app_id
    FIREBASE_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_ID
    GOOGLE_SERVER_CLIENT_IDAuthentication → Sign-in method → Google → Web SDK configuration → Web client ID (Google sign-in on Android)
  8. iOS: copy ios/Flutter/Keys.xcconfig.example to ios/Flutter/Keys.xcconfig (the download already has one) and set GOOGLE_REVERSED_CLIENT_ID (the REVERSED_CLIENT_ID from GoogleService-Info.plist) and FIREBASE_ENCODED_APP_ID (your GOOGLE_APP_ID with the colons turned into dashes, e.g. 1:123:ios:abc → app-1-123-ios-abc).
  9. Android Google sign-in needs your signing key fingerprints: run cd brainrush_mobile_flutter/android && ./gradlew signingReport and 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).

Use HTTPS in production: the iOS app only talks to https servers, and Firebase sign-in on the website needs your https domain under Authorized domains.

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.

  1. Create a PostgreSQL database (Neon, Supabase, Railway, or your own) and copy its connection string.
  2. Import brainrush_web_nextjs into Vercel (Root directory: brainrush_web_nextjs). Add the keys of brainrush_web_nextjs/.env.example under Settings → Environment Variables, with DATABASE_URL set to your database and the S3_* keys set to a cloud bucket.
  3. Create the tables once from your computer: in brainrush_web_nextjs, put the same DATABASE_URL in .env, then run pnpm install, pnpm prisma:migrate:deploy and pnpm prisma:seed --base. Then open your site: the install wizard does the rest.
  4. 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 worker with the same .env (use a process manager such as pm2 or systemd), or with Docker using deploy/docker-compose.yml's worker service.

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:

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.

Need a fresh start? 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

  1. cd brainrush_mobile_flutter. The download has a .env with empty values (a copy of .env.example). Set:
    API_BASE_URL=https://your-domain.com
    APP_NAME=Brainrush
    FIREBASE_…   (from chapter 4)
  2. Get the packages and run on a phone or emulator:
    flutter pub get
    flutter run
  3. Build for testing: flutter build apk --release (Android) or open ios/Runner.xcworkspace in Xcode (iOS).
  4. 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 build settings: 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.
Running the server on your own computer? The Android emulator reaches it at 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.

  1. 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
  2. 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.
  3. App keys: RevenueCat → Project → API keys → the public SDK keys. In brainrush_mobile_flutter/.env: REVENUECAT_APPLE_KEY and REVENUECAT_GOOGLE_KEY.
  4. Server keys in deploy/.env: REVENUECAT_SECRET_KEY (a secret API key) and REVENUECAT_WEBHOOK_AUTH (any long random value).
  5. RevenueCat → Integrations → Webhooks → add https://your-domain.com/api/v1/webhooks/revenuecat with the Authorization header value equal to REVENUECAT_WEBHOOK_AUTH.
Demo mode. With 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.

  1. Android works once Firebase is set up (chapter 4).
  2. 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.
  3. 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.

  1. In AdMob, add your Android and iOS apps and create one ad unit of each type you want.
  2. In brainrush_mobile_flutter/.env set ADMOB_ANDROID_APP_ID and the unit ids ADMOB_ANDROID_REWARDED_ID, ADMOB_ANDROID_INTERSTITIAL_ID, ADMOB_ANDROID_BANNER_ID, ADMOB_IOS_REWARDED_ID, ADMOB_IOS_INTERSTITIAL_ID, ADMOB_IOS_BANNER_ID.
  3. The iOS app id goes in ios/Flutter/Keys.xcconfig: ADMOB_IOS_APP_ID=ca-app-pub-…~….
  4. 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 keep AD_REWARDS_MODE="ssv" (the default in deploy/.env).
  5. 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.
How rewarded ads are protected. The app never decides a reward on its own. Each rewarded ad carries a one-time code and the player's id as AdMob server-side verification data. When the player earns the reward, Google calls your server's /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

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

Leave 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.

Picking a colour. The brand colour sits behind dark ink text on keys, coins and the daily card, so choose a bright, light colour (yellow, lime, orange, sky). For a dark brand colour, also change 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).

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 rulesAdmin → Coin Economy
Coin packs, prices shown, store product idsAdmin → Coin Packs & IAP
Categories, subcategories, premium (coin-locked) categoriesAdmin → Categories
QuestionsAdmin → Questions (editor, CSV import), Admin → AI Generator
Today's and upcoming daily quizzesAdmin → Daily Quiz (the worker fills the next 7 days on its own)
Contests, entry fees and prize splitAdmin → Contests
Exams for a class or a companyAdmin → Exams
Battle rules (entry fees, bot wait, room size)Admin → Battles
League tiers, players per league, how many move up and down, board rewardsAdmin → Leagues
BadgesAdmin → Badges
Fun & Learn articles and their quizzesAdmin → Fun & Learn
App name, support email, store links, FAQ, guest play, minimum app version (force update), maintenance modeAdmin → Settings
Terms and privacy pagesAdmin → Settings (or point the links at your own pages)
App textsThe page files in brainrush_mobile_flutter/lib/pages/
Website textsThe 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.

KeyWhen iOS shows itText in the kit
NSPhotoLibraryUsageDescriptionPicking a profile photo or a picture for a quiz questionChoose a photo for your profile picture or for a question in a quiz you make.
NSCameraUsageDescriptionTaking that photo with the cameraTake a photo for your profile picture or for a question in a quiz you make.
NSUserTrackingUsageDescriptionBefore 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

Admin questions
Admin → Questions
CSV import
Admin → Import (CSV)

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.

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.

The game board on Home
The game board on Home
A level question
Quiz Zone level
A live 1v1 battle
A live 1v1 battle
Daily quiz
Daily quiz
ModeHow it playsChange 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 quizOne 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 battleQuick 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 roomA 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
ContestsScheduled 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
ExamsFor 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-challengeThe player picks a category, the number of questions and the time, and tries to beat their own best.(no settings)
Fun & LearnShort articles with a quiz at the end; reading and passing pays coins once.Admin → Fun & Learn
Guess the wordThe player builds the answer from letter tiles; a hint costs coins.Admin → Questions (type: guess the word)
Player-made quizzesPlayers 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

21Coins and fair play

Admin coin economy
Admin → Coin Economy
A question checked by the server
Answers are checked by the server

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:

RuleWhat it stopsChange it in
Answers are checked on the server, never in the appA 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 coinsScripts that answer instantly to farm coinsAdmin → 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 dayAdmin → 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 lockFake "ad watched" calls and racing the daily limitAdmin → 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 secretDouble coins and forged purchase callsdeploy/.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 onceDouble entries and overfull contests(always on)
Battle rounds are timed by the server; an answer after the clock does not countSlow-network or changed-clock tricksAdmin → Battles
Referral coins are paid only after the new player's first real quiz, with daily and lifetime limitsFake accounts made for referral coinsAdmin → Coin Economy
Admins can give or take coins from Admin → Users → a player → Coins; every change is written to the coin history and the audit log with the staff member's name.

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

  1. 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 });
    });
  2. Register it with the others in src/lib/server/api.ts: add import "@/lib/server/handlers/facts";.
  3. Call it from the app: ref.watch(apiGet('/facts/random')) gives you {fact: …} with loading and error states (see lib/pages/home/home_page.dart for 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 } });
  }
}
Check the fields of 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.

KeyRequiredWhereWhat it does
Database
DATABASE_URLYeswebsitePostgreSQL connection string
POSTGRES_DBYesDockerDatabase the Docker stack creates
POSTGRES_USERYesDockerDatabase user for the Docker stack
POSTGRES_PASSWORDYesDockerDatabase password for the Docker stack
App
APP_URLYeswebsite, DockerPublic URL of the website/API, e.g. https://quiz.example.com
NEXT_PUBLIC_APP_NAME—website, DockerName shown before the admin sets one
WEB_PORT—DockerHost port the web container listens on
DOMAIN—DockerYour domain for automatic HTTPS (docker compose --profile https)
CORS_ORIGINS—website, DockerBrowser 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_KEYYeswebsite, DockerFirebase web app config
NEXT_PUBLIC_FIREBASE_AUTH_DOMAINYeswebsite, DockerFirebase web app config
NEXT_PUBLIC_FIREBASE_PROJECT_IDYeswebsite, DockerFirebase web app config
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET—website, DockerFirebase web app config
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID—website, DockerFirebase web app config
NEXT_PUBLIC_FIREBASE_APP_IDYeswebsite, DockerFirebase web app config
Firebase Admin + push
FIREBASE_PROJECT_IDYeswebsite, DockerFirebase project id
FIREBASE_CLIENT_EMAILYeswebsite, DockerService account e-mail
FIREBASE_PRIVATE_KEYYeswebsite, DockerService account private key
In-app purchases
REVENUECAT_SECRET_KEY—website, DockerRevenueCat secret API key — verifies store purchases (empty = demo purchases)
REVENUECAT_WEBHOOK_AUTH—website, DockerAuthorization 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, DockerClaude API key (or set it in Admin → Settings → AI)
OPENAI_API_KEY—website, DockerOpenAI API key
GEMINI_API_KEY—website, DockerGoogle Gemini API key
Storage
S3_ENDPOINT—website, DockerS3-compatible endpoint (MinIO, R2); empty for AWS
S3_REGION—website, DockerBucket region
S3_BUCKET—website, DockerBucket for photos and documents (empty = local uploads folder)
S3_ACCESS_KEY_ID—website, DockerStorage access key
S3_SECRET_ACCESS_KEY—website, DockerStorage secret key
Email
SMTP_HOST—website, DockerSMTP server; empty = emails are printed to the log
SMTP_PORT—website, Docker587 (STARTTLS) or 465 (TLS)
SMTP_USER—website, DockerSMTP user
SMTP_PASS—website, DockerSMTP password
SMTP_SECURE—website, Docker"true" for port 465 (TLS); empty for 587
Storage
UPLOAD_DIR—websiteFolder for uploads when no bucket is set (default ./uploads)
Mobile apps
API_BASE_URLYesappYour server; the app calls <url>/api/v1
APP_NAME—appApp name in the UI
FIREBASE_PROJECT_IDYesappFirebase project id
FIREBASE_MESSAGING_SENDER_IDYesappFirebase config
FIREBASE_STORAGE_BUCKET—appFirebase config
FIREBASE_ANDROID_API_KEYYesappFirebase Android config
FIREBASE_ANDROID_APP_IDYesappFirebase Android config
FIREBASE_IOS_API_KEYYesappFirebase iOS config
FIREBASE_IOS_APP_IDYesappFirebase iOS config
FIREBASE_IOS_CLIENT_ID—appGoogle sign-in on iOS
FIREBASE_IOS_BUNDLE_ID—appiOS bundle id
GOOGLE_SERVER_CLIENT_ID—appWeb OAuth client id — Google sign-in on Android
SUPPORT_EMAIL—appFallback support e-mail until the server config loads
REVENUECAT_APPLE_KEY—appRevenueCat public iOS key (empty = demo purchases)
REVENUECAT_GOOGLE_KEY—appRevenueCat public Android key (empty = demo purchases)
ADMOB_ANDROID_APP_ID—appAdMob Android app id (empty = Google test ads)
ADMOB_ANDROID_REWARDED_ID—appAdMob Android rewarded ad unit id (empty = Google test ads, which pay only with AD_REWARDS_MODE=demo)
ADMOB_IOS_REWARDED_ID—appAdMob iOS rewarded ad unit id (empty = Google test ads, which pay only with AD_REWARDS_MODE=demo)
ADMOB_ANDROID_INTERSTITIAL_ID—appAdMob Android interstitial ad unit id (empty = Google test ads)
ADMOB_IOS_INTERSTITIAL_ID—appAdMob iOS interstitial ad unit id (empty = Google test ads)
ADMOB_ANDROID_BANNER_ID—appAdMob Android banner ad unit id (empty = Google test ads)
ADMOB_IOS_BANNER_ID—appAdMob 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.

MethodPathSign-inWhat it does
System, files and webhooks
GET/api/v1/healthNoHealth check for Docker and uptime monitors
GET/api/v1/firebase-config.jsNoThe 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/ssvSigned by the providerAdMob server-side verification callback (set it on every rewarded ad unit in AdMob)
GET/api/v1/configNoEverything a client needs before the first screen: names, links, version gate, maintenance, economy, ads, purchases
GET/api/v1/legal/:docNoTerms of use or privacy policy text
GET/api/v1/files/*NoServe an uploaded file
POST/api/v1/uploadsStaff: content editor or super adminMultipart upload (field `file`, optional `folder`: avatars · quizzes for players; questions · articles · categories · badges for staff)
POST/api/v1/auth/demoNoOne-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/revenuecatSigned by the providerRevenueCat purchase, renewal and refund events (shared secret)
GET/api/v1/install/statusNoWhether the first admin still needs to be created
POST/api/v1/installNoOnly 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/meSigned inThe player's profile, coins, level and stats
PATCH/api/v1/meSigned inUpdate name, username, avatar, language or settings
GET/api/v1/me/usernameSigned inCheck whether a username is free
GET/api/v1/me/homeSigned inHome: the player, streak (saved / lost dialogs), league card, daily quiz card and pending celebrations in one call
POST/api/v1/me/celebrations/:id/seenSigned inMark a celebration (contest win, level up) as seen
POST/api/v1/me/streak/freezeSigned inBuy a streak freeze (max 2 equipped)
POST/api/v1/me/streak/restoreSigned inRestore a streak that broke today
GET/api/v1/me/statsSigned inAccuracy by category from finished games
GET/api/v1/me/coinsSigned inThe player's coin history
GET/api/v1/me/referralsSigned inThe player's invite code and friends who joined
POST/api/v1/me/referralSigned 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/devicesSigned inRegister a device for push messages
DELETE/api/v1/me/devicesSigned inUnregister a device from push messages
GET/api/v1/notificationsSigned inThe player's inbox
POST/api/v1/notifications/read-allSigned inMark every notification read
POST/api/v1/notifications/:id/readSigned inMark one notification read
POST/api/v1/feedbackOptionalSend feedback to the team
DELETE/api/v1/meSigned inGuests are deleted at once; accounts are scheduled for 30 days (signing in again cancels it)
POST/api/v1/me/exportSigned in (not a guest)Download my data: a copy of the player's data, sent to their email
POST/api/v1/me/mergeSigned in (not a guest)A guest signs in to an account that already has progress
GET/api/v1/me/notification-defaultsNoWhich notifications are on by default
Categories, solo games, bookmarks and Fun & Learn
GET/api/v1/categoriesOptionalCategories: every live category with the player's progress, lock state and "new" flag
GET/api/v1/categories/:idOptionalA category with its subcategories and the player's progress
POST/api/v1/categories/:id/unlockSigned inUnlock a premium category forever
GET/api/v1/subcategories/:id/levelsOptionalLevels: stars per level and which levels are open
GET/api/v1/subcategories/:id/packSigned inOffline pack: the full question pool of one open level with answers, for play without a connection
GET/api/v1/searchOptionalSearch: subcategories and categories by name (the app also filters its loaded list on device)
GET/api/v1/interestsNoPick Interests: the category list without progress
POST/api/v1/gamesSigned inStart a game (level, self-challenge, guess the word, shared quiz, article quiz)
GET/api/v1/games/:idSigned inA game in progress
POST/api/v1/games/:id/lifelineSigned inUse a lifeline (50/50, poll, skip, extra time) for coins
POST/api/v1/games/:id/answerSigned inAnswer a question; the server checks it
POST/api/v1/games/:id/finishSigned inFinish a game and pay coins and XP
POST/api/v1/games/:id/quitSigned inQuit: the game ends without rewards (coins spent on lifelines stay spent)
GET/api/v1/games/:id/resultSigned inA finished game's result
GET/api/v1/games/:id/reviewSigned inA finished game's questions with the right answers
POST/api/v1/games/syncSigned inUpload games played offline
GET/api/v1/self/bestSigned inThe player's best with the same self-challenge settings
GET/api/v1/bookmarksSigned inThe player's bookmarked questions
POST/api/v1/questions/:id/bookmarkSigned inBookmark a question
DELETE/api/v1/questions/:id/bookmarkSigned inRemove a bookmark
POST/api/v1/questions/:id/reportSigned inReport: question bank questions only; three or more open reports flag the question for editors
GET/api/v1/report-reasonsNoReasons a player can pick when reporting a question
GET/api/v1/articlesOptionalFun & Learn articles
GET/api/v1/articles/:idOptionalOne article with its short quiz
POST/api/v1/articles/:id/readSigned inMarks 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/dailyOptionalToday's daily quiz and whether the player has played it
POST/api/v1/daily/startSigned inStart today's daily quiz (one try per day)
GET/api/v1/leaderboards/:periodOptionalDaily, weekly or all-time leaderboard
GET/api/v1/leagues/currentSigned in (not a guest)The player's league group this week
GET/api/v1/leagues/tiersNoLeague tiers and their rules
GET/api/v1/badgesSigned inEvery badge and the player's progress
GET/api/v1/contestsOptionalLive, upcoming and past contests
GET/api/v1/contests/:idOptionalOne contest with the player's entry
POST/api/v1/contests/:id/joinSigned in (not a guest)Join a contest (pays the entry fee once)
POST/api/v1/contests/:id/remindSigned in (not a guest)Remind the player when a contest starts
GET/api/v1/contests/:id/leaderboardOptionalA contest's live ranking
GET/api/v1/battles/hubSigned inBattle tab: players online, entry fee and recent battles
POST/api/v1/battles/quickSigned in (not a guest)Find a random opponent for a 1v1 battle
POST/api/v1/battles/friendSigned in (not a guest)Create a 1v1 battle with a code for a friend
POST/api/v1/battles/joinSigned in (not a guest)Join a battle or room by code
POST/api/v1/roomsSigned in (not a guest)Create a group battle room for up to 8 players
GET/api/v1/battles/:idSigned inBattle state: players, question, scores
POST/api/v1/battles/:id/botSigned inFill an empty seat with a bot when nobody joins
POST/api/v1/battles/:id/startSigned inStart a room (host only)
POST/api/v1/battles/:id/kickSigned inRemove a player from a room (host only)
POST/api/v1/battles/:id/leaveSigned inLeave a battle or room
POST/api/v1/battles/:id/answerSigned inAnswer the current battle question
POST/api/v1/battles/:id/rematchSigned inAsk the same players for a rematch
POST/api/v1/exams/lookupSigned in (not a guest)Find an exam by its code
POST/api/v1/exams/:id/startSigned in (not a guest)Start an exam attempt
PATCH/api/v1/exam-attempts/:idSigned in (not a guest)Save the answers of an exam in progress
POST/api/v1/exam-attempts/:id/leaveSigned in (not a guest)Record that the player left the exam screen
POST/api/v1/exam-attempts/:id/submitSigned in (not a guest)Submit an exam
GET/api/v1/exam-attempts/:idSigned in (not a guest)An exam attempt and its result once released
Player-made quizzes, store, purchases and rewarded ads
GET/api/v1/my-quizzesSigned inQuizzes the player made
POST/api/v1/my-quizzesSigned in (not a guest)Create a quiz
GET/api/v1/my-quizzes/:idSigned inOne of the player's quizzes
PATCH/api/v1/my-quizzes/:idSigned in (not a guest)Update one of the player's quizzes
POST/api/v1/my-quizzes/:id/publishSigned in (not a guest)Publish a quiz and get its share link
DELETE/api/v1/my-quizzes/:idSigned inDelete one of the player's quizzes
GET/api/v1/q/:slugOptionalThe public page of a shared quiz with the scores to beat
GET/api/v1/quizzes/featuredNoPlayer-made quizzes picked for the website
GET/api/v1/storeSigned inCoin packs, Remove ads and the player's purchases
POST/api/v1/purchases/demoSigned inDemo only: grant a store product without paying (refused unless purchases run in demo mode)
POST/api/v1/purchases/syncSigned inGrant purchases the app store confirmed (checked with RevenueCat)
POST/api/v1/ads/rewardedSigned inThe 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/dashboardStaff with dashboard accessDashboard numbers: players, games, revenue and what needs attention
GET/api/v1/admin/searchStaff with dashboard accessSearch players, questions and contests from the top bar
GET/api/v1/admin/categoriesStaff with content accessList categories with their question counts
POST/api/v1/admin/categoriesStaff: content editor or super adminCreate a category
PATCH/api/v1/admin/categories/:idStaff: content editor or super adminUpdate a category
DELETE/api/v1/admin/categories/:idStaff: content editor or super adminDelete an empty category
POST/api/v1/admin/categories/reorderStaff: content editor or super adminSave the order of categories
GET/api/v1/admin/categories/:id/subcategoriesStaff with content accessList a category's subcategories
GET/api/v1/admin/subcategoriesStaff with content accessEvery subcategory with its category, for the question editor's picker and filters
POST/api/v1/admin/subcategoriesStaff: content editor or super adminCreate a subcategory
PATCH/api/v1/admin/subcategories/:idStaff: content editor or super adminUpdate a subcategory
DELETE/api/v1/admin/subcategories/:idStaff: content editor or super adminDelete an empty subcategory
GET/api/v1/admin/questionsStaff with content accessList questions with search and filters
GET/api/v1/admin/questions/:idStaff with content accessOne question with its stats
POST/api/v1/admin/questionsStaff: content editor or super adminCreate a question
PATCH/api/v1/admin/questions/:idStaff: content editor or super adminUpdate a question
DELETE/api/v1/admin/questions/:idStaff: content editor or super adminDelete a question
POST/api/v1/admin/questions/bulkStaff: content editor or super adminPublish, unpublish or delete many questions at once
GET/api/v1/admin/import/templateStaff with content accessDownload the CSV import template
POST/api/v1/admin/import/checkStaff: content editor or super adminUpload a CSV and check every row before import
POST/api/v1/admin/import/:id/commitStaff: content editor or super adminImport the checked rows
GET/api/v1/admin/import/:id/skippedStaff with content accessDownload the rows an import skipped, with the reason
GET/api/v1/admin/ai/statusStaff with content accessAI generator status: provider, model and this month's quota
POST/api/v1/admin/ai/generateStaff: content editor or super adminGenerates drafts and keeps them as `ai_draft` questions, so they survive a closed tab and count on the dashboard
GET/api/v1/admin/ai/draftsStaff with content accessList AI question drafts waiting for review
POST/api/v1/admin/ai/reviewStaff: content editor or super adminApprove, edit or reject AI drafts
GET/api/v1/admin/dailyStaff with daily accessDaily quiz calendar for one month
GET/api/v1/admin/daily/:dayStaff with daily accessOne day's daily quiz with its results
PUT/api/v1/admin/daily/:dayStaff: content editor or super adminSchedule or change a day's daily quiz
DELETE/api/v1/admin/daily/:dayStaff: content editor or super adminRemove a scheduled daily quiz
POST/api/v1/admin/daily/autofillStaff: content editor or super adminAuto-fill gaps: a mixed quiz for every empty day of the month from today on
GET/api/v1/admin/articlesStaff with articles accessList Fun & Learn articles
GET/api/v1/admin/articles/:idStaff with articles accessOne article with its quiz
POST/api/v1/admin/articlesStaff: content editor or super adminCreate an article
PATCH/api/v1/admin/articles/:idStaff: content editor or super adminUpdate an article
DELETE/api/v1/admin/articles/:idStaff: content editor or super adminDelete an article
GET/api/v1/admin/badgesStaff with content accessList badges
POST/api/v1/admin/badgesStaff: content editor or super adminCreate a badge
PATCH/api/v1/admin/badges/:idStaff: content editor or super adminUpdate a badge
DELETE/api/v1/admin/badges/:idStaff: content editor or super adminDelete a badge
Admin: contests, exams, settings and store products
GET/api/v1/admin/contestsStaff with contests accessList contests
POST/api/v1/admin/contestsStaff: finance, content editor or super adminCreate a contest
PATCH/api/v1/admin/contests/:idStaff: finance, content editor or super adminUpdate a contest
DELETE/api/v1/admin/contests/:idStaff: finance, content editor or super adminDelete a contest nobody joined yet
GET/api/v1/admin/contests/:id/resultsStaff with contests accessA contest's ranking
GET/api/v1/admin/contests/:id/exportStaff with contests accessDownload a contest's ranking as CSV
POST/api/v1/admin/contests/:id/payoutStaff: finance or super adminPay a finished contest's prize pool once
GET/api/v1/admin/examsStaff with exams accessList exams
POST/api/v1/admin/examsStaff: content editor or super adminCreate an exam
PATCH/api/v1/admin/exams/:idStaff: content editor or super adminUpdate an exam
DELETE/api/v1/admin/exams/:idStaff: content editor or super adminDelete an exam
GET/api/v1/admin/exams/:id/resultsStaff with exams accessAn exam's attempts and scores
POST/api/v1/admin/exams/:id/releaseStaff: content editor or super adminRelease an exam's results to its players
GET/api/v1/admin/exams/:id/exportStaff with exams accessDownload an exam's results as CSV
GET/api/v1/admin/leaguesStaff with settings accessThis week's leagues — how many groups, and players per tier
GET/api/v1/admin/settingsStaff with dashboard accessEvery settings group (secrets are masked)
PUT/api/v1/admin/settings/:groupStaff: the group's writer role or super adminSave one settings group
GET/api/v1/admin/productsStaff with money accessList coin packs and the Remove ads product
POST/api/v1/admin/productsStaff: finance or super adminCreate a store product
PATCH/api/v1/admin/products/:idStaff: finance or super adminUpdate a store product
DELETE/api/v1/admin/products/:idStaff: finance or super adminDelete a store product
GET/api/v1/admin/leaderboards/:periodStaff with leaderboards accessLeaderboard for a period, with players' details
POST/api/v1/admin/leaderboards/resetStaff: moderator or super adminReset the weekly board (league standings are separate and keep their XP)
Admin: players, reports, push, staff and audit log
GET/api/v1/admin/meStaff (any role)The signed-in staff member and the pages their role may open
GET/api/v1/admin/usersStaff with users accessList players with search and filters
GET/api/v1/admin/users/exportStaff with users accessDownload the player list as CSV
GET/api/v1/admin/users/:idStaff with users accessOne player: profile, coins, games and devices
POST/api/v1/admin/users/:id/coinsStaff: moderator, finance or super adminGive or take coins from a player (logged)
POST/api/v1/admin/users/:id/banStaff: moderator or super adminBan — signed out (the API refuses them), off the boards and leagues; optionally take back contest prizes
POST/api/v1/admin/users/:id/unbanStaff: moderator or super adminLift a player's ban
GET/api/v1/admin/reportsStaff with reports accessList reported questions
GET/api/v1/admin/reports/question/:idStaff with reports accessEvery report on one question
POST/api/v1/admin/reports/question/:id/resolveStaff: moderator, content editor or super adminMark a question's reports fixed or dismissed
GET/api/v1/admin/feedbackStaff with reports accessList feedback sent from the app
PATCH/api/v1/admin/feedback/:idStaff: moderator or super adminMark feedback read or done
GET/api/v1/admin/pushStaff with push accessList sent and scheduled push messages
GET/api/v1/admin/push/audienceStaff with push accessCount the players a push audience reaches
POST/api/v1/admin/pushStaff: content editor or super adminSend or schedule a push message
GET/api/v1/admin/staffStaff with roles accessList staff members and their roles
POST/api/v1/admin/staff/inviteStaff: super adminInvite a staff member by email
PATCH/api/v1/admin/staff/:idStaff: super adminChange a staff member's role or remove their access
GET/api/v1/admin/setupStaff with setup accessSetup check: which keys and services are ready
GET/api/v1/admin/auditStaff with settings accessThe 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:

TableWhat it holdsKeysPoints to
People
usersA player or staff member: profile, coins, XP, streak, league tier, ad and ban status.PK id
unique firebaseUid
unique username
unique referralCode
referredById → users
devicesA phone or browser that receives push messages (FCM token).PK id
unique token
userId → users
Content: categories, questions, articles
categoriesA top-level quiz category (Science, History…) with its icon, colour, order and premium price.PK id
unique slug
—
subcategoriesA topic inside a category; its questions are split into levels.PK id
unique slug
categoryId → categories
questionsOne question: type (multiple choice, true/false, guess the word), options, answer, level, difficulty and status.PK id
unique number
subcategoryId → subcategories
category_unlocksA premium category a player unlocked with coins.PK (userId, categoryId)userId → users
categoryId → categories
level_progressA player's best stars on one level of a subcategory.PK (userId, subcategoryId, level)userId → users
subcategoryId → subcategories
bookmarksA question a player saved for later.PK (userId, questionId)userId → users
questionId → questions
reportsA player's report on a wrong or unclear question.PK idquestionId → questions
userId → users
resolvedById → users
articlesA Fun & Learn article with a short quiz at the end.PK id—
article_readsA 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_sessionsOne solo game: mode, questions, answers, timing, coins and XP paid.PK iduserId → users
daily_quizzesThe question set for one day's daily quiz.PK day—
Battles (1v1 quick match, friend code, group rooms) — server-timed rounds
battlesA 1v1 battle or group room: code, host, state, questions and server-timed rounds.PK id
unique code
hostId → users
battle_playersA player (or bot) in a battle with their answers and score.PK id
unique (battleId, userId)
battleId → battles
userId → users
Contests
contestsA scheduled contest: questions, entry fee, prize pool and split.PK idcategoryId → categories
contest_entriesA player's entry and score in a contest.PK id
unique (contestId, userId)
contestId → contests
userId → users
contest_remindersA player who asked to be told when a contest starts.PK (contestId, userId)contestId → contests
userId → users
Weekly leagues
league_groupsOne weekly league group of up to 30 players in a tier.PK id—
league_membersA player's place and XP in a weekly league group.PK (groupId, userId)groupId → league_groups
userId → users
xp_daysXP earned per player per day (daily and weekly boards, the dashboard chart).PK (userId, day)—
Exams
examsA timed exam with a code, questions and result release.PK id
unique code
—
exam_attemptsA 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_quizzesA quiz a player made and shares by link or QR code.PK id
unique slug
ownerId → users
categoryId → categories
user_quiz_playsOne play of a player-made quiz.PK idquizId → user_quizzes
userId → users
Badges
badgesA badge and the rule that earns it.PK id
unique slug
—
user_badgesA badge a player earned.PK (userId, badgeId)userId → users
badgeId → badges
Coins, purchases, products
coin_txsEvery coin change with its reason (game, ad, purchase, refund, contest, admin), so balances can be audited.PK iduserId → users
ad_rewardsA 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
—
productsA store product: a coin pack or Remove ads, with its App Store and Play product id.PK id
unique productId
—
purchasesA store purchase granted once per transaction id; refunds take it back.PK id
unique transactionId
userId → users
System
notificationsA message in a player's inbox.PK iduserId → users
celebrationsOne-time dialogs the app shows on Home: streak saved / lost, contest prize, league result.PK iduserId → users
feedbackFeedback a player sent from the app.PK iduserId → users
push_campaignsA push message staff sent or scheduled, with its audience.PK id—
import_batchesA CSV question import: checked rows, skipped rows and status.PK id—
ai_runsOne AI question-generator run and the drafts it produced.PK id—
settingsOne settings group saved from Admin → Settings (overrides the defaults key by key).PK (group, key)—
audit_logsA staff action: who did what to which record, and when.PK idactorId → 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

  1. 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.
  2. Create android/key.properties:
    storePassword=…
    keyPassword=…
    keyAlias=upload
    storeFile=upload-keystore.jks
    Without this file, release builds are signed with the debug key (fine for testing, refused by Play).
  3. Set the version in pubspec.yaml (version: 1.0.0+1; raise the number after + for every upload), then flutter build appbundle.
  4. 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.aab to a testing track first.
  5. Add the SHA-1/SHA-256 of Play's app signing key (Play Console → Test and release → App integrity) to Firebase.

App Store

  1. In the Apple Developer site, register your bundle id with Push Notifications, Sign in with Apple and In-App Purchase.
  2. Open ios/Runner.xcworkspace in Xcode → Runner → Signing & Capabilities → choose your team (Automatic signing is already on; chapter 9).
  3. In App Store Connect create the app, fill the listing and App Privacy, and create the in-app purchases (chapter 10).
  4. flutter build ipa, then upload build/ios/ipa/*.ipa with the Transporter app, and send it to TestFlight first.
Store rules to keep in mind: coins sold inside the app must use in-app purchases, which the app does. Apps with sign-in must offer account deletion (included) and, on iOS, Sign in with Apple when Google sign-in is offered (included). Coins have no cash value: keep contest prizes in coins.

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.