This is an application that consumes the official Helldivers 1 API, caches and rebroadcasts it to avoid high load on official servers, and stores historic data that the official API discards. It also offers account management and API keys for third parties to access the data to build their own apps.
The frontend is a live campaign dashboard with an interactive galaxy
map, a scrollytelling event log, and a real-time notification system.
All pages receive live campaign updates via client-side polling
(useLiveData hook, setInterval + fetch, 10s interval, with an
immediate refetch on visibilitychange when the tab comes back into
focus). The app runs as an installable Progressive Web App with a
Serwist-backed service worker, supports offline mode via a cached
last-poll payload in localStorage, and delivers browser and push
notifications when campaign events transition state.
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router) on Node.js 24, React 19 |
| Styling | Tailwind CSS v4 (@theme in src/app/layout.css) |
| Database | PostgreSQL via Prisma 7 (@prisma/adapter-pg) |
| Auth | BetterAuth (Discord + GitHub OAuth, optional) |
| Real-time | Client polling + BroadcastChannel leader election |
| Notifications | Sonner toasts + Web Notifications API + web-push |
| PWA | Serwist (@serwist/next) service worker |
| Observability | Sentry SDK wired to a self-hosted GlitchTip (optional) |
| Analytics | Umami v3, self-hosted, cookieless (optional) |
| Testing | Vitest (unit + jsdom) + a Playwright smoke config |
| Deployment | Docker images published to ghcr.io, deployed on a VPS |
The application is made from 2 large sections:
- the api that serves and updates the data
- the frontend that consumes the api and visualises it, alongside some user-facing features.
Detailed docs: /docs/frontend-layout (in-app) — covers the full
state machine, CSS var pipeline, and breakpoint matrix.
- Interactive galaxy map — responsive SVG galaxy covering every
campaign sector, colored by faction and campaign state. Click a
past event in the log to rewind the map to that historical state
via
computeMapStateAtEvent. - Scrollytelling event log —
useScrollEventsyncs the selected event card to the viewport's current scroll position; the map reacts in sync. - Pinned-map state machine — mobile/tablet users can pin the map
to the top of the viewport via a floating action button (FAB). On
/archivesthe FAB defaults to pinned so the map is at rest when the user reaches the event log. A class-layering system (--stickyfor the persistent pinned state,--pinningfor the 400ms slide animation) keeps the entrance animation from re-triggering on mount. - Scroll-hiding header — at md+ the fixed header scroll-hides
itself via
public/scripts/headerGPU.js. The pinned map tracks the header's offset and mirrors its live background via three CSS custom properties published on<html>(--header-offset,--header-bg,--header-glass-filter). - Live polling loop — one
useLiveDatasingleton per tab polls/api/h1/liveevery 10 seconds, withBroadcastChannelleader election so only one tab dispatches Web Notifications per session. - PWA offline shell —
src/sw.js+ Serwist precaches the app shell, and the last poll payload is cached inlocalStoragefor offline fallback.
On startup, it runs instrumentation.js (once) which will:
-
check if openapi spec exists (or generate it in dev mode)
-
check if database connection exists and is valid
-
initialize the database:
- run migrations.
- fetch remote currentStatus + currentSeason
- save normalized data in the 5-table h1_* schema (
h1_season,h1_status,h1_statistic,h1_event,h1_event_progress). Past seasons referenced by lagged event slots are skipped perqueryUpsertEvent'sevent.season !== seasonguard rather than being seeded as empty rows.
-
start a worker thread that will continiously update the database from the official Helldivers API. It simply queries the /api/h1/update endpoint every
UPDATE_INTERVALseconds using theUPDATE_KEYtoken.
Next.js route handlers under src/app/api/ expose Helldivers data in
several formats. The GET /api/h1/* endpoints are the core public
surface; the rest are internal (auth, healthcheck, analytics/error
tunnels).
GET /api/h1/update- Triggers a fresh campaign status + snapshot refresh from the
official Helldivers 1 API and persists the result to the
normalized
h1_*tables via the shared bucket-upsert pipeline. - Requires a valid
keyquery parameter matching the server'sUPDATE_KEYenvironment variable — this is the same token the background worker uses.
- Triggers a fresh campaign status + snapshot refresh from the
official Helldivers 1 API and persists the result to the
normalized
GET /api/h1/rebroadcast- Mirrors the official Helldivers 1 API behavior byte-for-byte
by reconstructing the wire format on demand from the
normalized
h1_*tables. - Request body:
action: string — one ofget_campaign_status,get_snapshotseason: integer — required whenactionisget_snapshot
- Mirrors the official Helldivers 1 API behavior byte-for-byte
by reconstructing the wire format on demand from the
normalized
GET /api/h1/campaign- Custom endpoint with an optional
seasonquery parameter. - Returns combined status + snapshot information for a specific season in one query, shaped for the frontend.
- Custom endpoint with an optional
GET /api/h1/live- Current campaign state in a lightweight shape, polled every
10 seconds by the
useLiveDataclient hook. Powers the live dashboard, event-transition toasts, and push notifications.
- Current campaign state in a lightweight shape, polled every
10 seconds by the
GET /api/h1/stats(not implemented)- Future endpoint for aggregate game stats (wins/losses/kills/ etc.) calculated by the worker.
Internal routes not listed above:
GET/POST /api/auth/[...all]— BetterAuth handlers (only whenBETTER_AUTH_SECRETis set; otherwise auth is disabled).GET /api/healthcheck— used by Docker / upstream monitoring.POST /api/notifications/subscribe— push notification subscription endpoint (only when Web Push keys are configured).POST /api/umami,POST /api/glitchtip— same-origin proxy tunnels so self-hosted analytics and error reporting aren't blocked by ad blockers (only when the corresponding env vars are set).
All user-facing features below are gated behind BetterAuth. When
BETTER_AUTH_SECRET is not set, the sign-in UI is hidden, /profile
redirects home, and the auth API returns 503 — the public
dashboard, /archives, and the /api/h1/* rebroadcast endpoints
all still work without authentication.
- Authentication via Discord or GitHub OAuth (powered by BetterAuth)
- Account management — login / logout via OAuth, account deletion
- API keys — create and revoke API keys for programmatic access
- Profile — view connected OAuth providers and Gravatar, GDPR data export (JSON download), GDPR account deletion
- Admin dashboard (admin role required) — system overview (worker health, game data stats, user metrics) and user management (role changes, bans, API key oversight)
- Reviews — create, delete, edit user-submitted reviews
The application uses nextjs running on node@24, prisma and postgres. It is deployed on a VPS in a docker container.
- provide a
.env.developmentfile based on .example.env - install dependencies with
npm install - run
npm run devto start the server locally
When using the docker container, the database you are connecting to needs to already exist. It will not create it for you.
- docker login ghcr.io
- username: your-github-username
- password: classic-key-with-correct-permissions
docker build -f ./Dockerfile.migrate -t ghcr.io/elfensky/helldiversbot-migrate:staging .
docker build -f ./Dockerfile.app -t ghcr.io/elfensky/helldiversbot:staging .- Add
--no-cache --progress=plainfor clean rebuilds - Use
docker buildx build --platform linux/amd64 -f ./Dockerfile.app -t ghcr.io/elfensky/helldiversbot:staging .to cross-compile for x86_64 - Use
docker compose upto run the containers locally
- Manually | Use
docker push ghcr.io/elfensky/helldiversbot:stagingto push the image to ghcr.io - Automatically | On every normal commit, Github Actions will generate a new
:stagingimage - Automatically | On every tagged commit, Github Actions will generate a new
:productionimage alongside and create a new Release (using Release.md)
npx prisma migrate reset
reset all data in db
npx prisma generate
reads your Prisma schema and generates the Prisma Client.
npx prisma migrate dev
npx prisma migrate dev --name init
Purpose: This command generates and applies a new migration based on your Prisma schema changes. It creates migration files that keep a history of changes.
Use Case: Use this when you want to maintain a record of database changes, which is essential for production environments or when working in teams. It allows for version control of your database schema.
Benefits: This command also includes checks for applying migrations in a controlled manner, ensuring data integrity.
npx prisma db push
Purpose: This command is used to push your current Prisma schema to the database directly. It applies any changes you've made to your schema without creating migration files.
Use Case: It’s particularly useful during the development phase when you want to quickly sync your database schema with your Prisma schema without worrying about migration history.
Caution: It can overwrite data if your schema changes affect existing tables or columns, so it’s best for early-stage development or prototyping.
cx scan create --project-name "helldivers.bot" --branch "develop" --scan-types "sast, sca, iac-security" -s . --debug
cx scan create --project-name "helldivers.bot" --branch "develop" --scan-types "container-security" --containers-local-resolution --container-images "ghcr.io/elfensky/helldiversbot:local" -s . --debug