Make your weekly team meetings infinitely more fun with shared bingo boards in real time.
- Features
- Tech stack
- Architecture
- Project structure
- Local development
- GitHub CI / CD
- Production deployment
- Apache configuration
- Environment variables
- Database
- Security
- Useful commands
- Create a team and invite members with a unique invite code
- Join an existing team with the code
- Roles:
OWNER,ADMIN,MEMBER
- Board size is fully configurable (rows × columns, from 2×2 to 10×10)
- Optional FREE center cell (only available on odd-sized boards)
- Fill from a bank of 35 classic meeting phrases (predefined)
- Ability to add your own phrases with an emoji
- Random cell generation from the selected phrases
- The whole team shares the same board in real time via Socket.io
- Clicking a cell checks it instantly for every participant
- Automatic bingo detection: rows, columns, diagonals (on square boards)
- Animated celebration with confetti and a banner when bingo is detected
- Online members shown during the session
- Past boards are kept with their date and completion rate
- Progress is visible (checked cells / total)
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router) |
| Language | TypeScript |
| Styles | Tailwind CSS |
| Animations | Framer Motion + react-confetti |
| Authentication | NextAuth.js v4 (email/password) |
| Database | PostgreSQL 16 via Prisma ORM |
| Real time | Socket.io (custom Node.js server) |
| Deployment | Docker Compose |
| Reverse proxy | Apache 2.4 |
Internet
│
│ HTTPS :443
▼
┌─────────────┐
│ Apache 2.4 │ ← SSL termination, security headers, HSTS
│ (host) │
└──────┬──────┘
│ HTTP 127.0.0.1:3000
│ WS 127.0.0.1:3000 (Socket.io)
▼
┌─────────────────────────────┐
│ Docker network: internal │
│ │
│ ┌─────────────────────┐ │
│ │ markit_app │ │
│ │ Next.js + Socket.io│ │
│ │ port 3000 │ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ markit_db │ │
│ │ PostgreSQL 16 │ │
│ │ (not exposed) │ │
│ └─────────────────────┘ │
└─────────────────────────────┘
- The app is only reachable via
127.0.0.1:3000from the host — never directly from the Internet. - The database exposes no ports to the outside.
- Apache handles SSL, HTTP→HTTPS redirects, and security headers.
markit/
├── .github/
│ ├── workflows/ci.yml # GitHub Actions CI/CD pipeline
│ └── dependabot.yml # Automatic updates (npm, Actions, Docker)
├── apache/
│ └── markit.conf # Apache VirtualHost configuration
├── prisma/
│ ├── migrations/ # Versioned Prisma migrations
│ ├── schema.prisma # Database schema
│ └── seed.mjs # Seed data (35 default phrases)
├── src/
│ ├── app/
│ │ ├── api/
│ │ │ ├── account/ # Profile / password
│ │ │ ├── admin/ # Admin (users, invites)
│ │ │ ├── auth/ # NextAuth + email signup
│ │ │ ├── cards/ # Board API (fetch, activate, check cells)
│ │ │ └── teams/ # Team API (CRUD, join, phrases)
│ │ ├── account/ # Account settings
│ │ ├── admin/ # Admin console
│ │ ├── auth/
│ │ │ ├── signin/ # Sign-in page
│ │ │ └── signup/ # Sign-up page
│ │ ├── dashboard/
│ │ │ ├── page.tsx # Team list
│ │ │ └── teams/[teamId]/
│ │ │ ├── page.tsx # Team board list
│ │ │ └── create/ # Board creator
│ │ └── play/[cardId]/ # Real-time play page
│ ├── components/
│ │ ├── admin/
│ │ ├── account-settings-form/
│ │ ├── dashboard/
│ │ ├── landing-hero/
│ │ └── navbar/
│ ├── lib/
│ │ ├── account.ts # Profile and password validation
│ │ ├── api-auth.ts # API session guards
│ │ ├── auth.ts # NextAuth configuration
│ │ ├── bingo.ts # Bingo detection logic
│ │ ├── prisma.ts # Prisma client singleton
│ │ ├── schemas/ # Zod schemas (API inputs)
│ │ └── socket.ts # Socket.io client
│ └── types/
│ └── index.ts # Shared TypeScript types
├── .env.example # Environment variable template
├── docker-compose.yml # Container orchestration
├── Dockerfile # Application image
├── next.config.js # Next.js config + security headers
├── server.js # Custom Node.js server (Next.js + Socket.io)
└── tailwind.config.ts
- Node.js 24 (see
.nvmrc;engines:>=24 <25) - Docker + Docker Compose
npm
# 1. Clone the project
git clone <repo> markit && cd markit
# 2. Install dependencies
npm install
# 3. Copy and configure environment variables
cp .env.example .envEdit .env (defaults work for local development):
DATABASE_URL="postgresql://markit:markit_password@localhost:5432/markit"
PORT=3000
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="<generate with: openssl rand -base64 32>"If port 3000 is already in use (another local app), change PORT and NEXTAUTH_URL together, e.g. PORT=3001 and NEXTAUTH_URL="http://localhost:3001".
# 4. Start PostgreSQL
docker compose up postgres -d
# 5. Apply migrations (or db:push while prototyping)
npm run db:migrate:deploy
# 6. Load the 35 default phrases (+ admin if ADMIN_* is set)
npm run db:seed
# 7. Start the development server
npm run devThe app is available at http://localhost:3000.
Before pushing, run the local checks:
npm run ci # Node, NEXTAUTH_SECRET, Prisma generate, lint, markdownlint, typecheck, tests
npm run ci:full # same + npm audit (high+, production deps only)Husky runs npm run ci on pre-push and commitlint on commit-msg (Conventional Commits).
On first Docker start, scripts/docker-entrypoint.sh runs prisma migrate deploy, seeds the database, then starts the server. Set ADMIN_EMAIL / ADMIN_PASSWORD (≥ 12 characters) to create the initial admin account (mustChangePassword → redirect to /account).
- Admin sign-in →
/accounton first login - Dashboard → create / join a team
- Create a board → play → real-time cells
- Admin → generate an invite → signup
The .github/workflows/ci.yml workflow runs on every push and pull request to main (changes under .cursor/ are ignored).
| Job | Trigger | Role |
|---|---|---|
quality |
push / PR to main |
npm ci then npm run ci:full |
docker |
PR to main only |
Docker build (validation, no image push) |
release |
push to main (after quality) |
semantic-release, then push GHCR image if a version is published |
npm run ci:full runs:
- Node.js version check
- Presence / format of
NEXTAUTH_SECRET prisma generate- ESLint (
npm run lint) - Markdownlint (
npm run lint:md) - TypeScript (
npm run typecheck) - npm audit (
npm run audit:ci, severity ≥ high, production deps only)
Locally, npm run ci skips the network audit; npm run ci:full enables it (as in CI).
On a pull request, the Dockerfile is built with Buildx to validate the image. Nothing is pushed to GHCR at this step (production push stays in release). Build cache is shared via GHCR (buildcache) and the GitHub Actions cache.
Config: release.config.cjs. On every successful push to main, semantic-release computes the version from Conventional Commits (feat:, fix:, chore:, …) and may publish:
- a Git tag
vX.Y.Z - a GitHub Release
- updates to
CHANGELOG.md,package.json, andpackage-lock.json - a
chore(release): … [skip ci]commit
Then, if a release was published, the Docker image is pushed to ghcr.io/<org>/markit with tags vX.Y.Z, X.Y, and latest (the latter only for a stable version, not a pre-release).
Secrets (Settings → Secrets and variables → Actions):
RELEASE_APP_ID— ID of the GitHub App used to publish releasesRELEASE_APP_PRIVATE_KEY— private key for that App
The GitHub App pushes release commits / tags (useful with branch protection). GHCR images use GITHUB_TOKEN (packages: write on the docker and release jobs).
Also check Settings → Actions → General → Workflow permissions → Read and write permissions if needed for GHCR.
.github/dependabot.yml opens PRs every Monday for:
| Ecosystem | Target | Groups / ignore |
|---|---|---|
npm |
package.json / lockfile |
groups next, prisma, dev-tools; major updates for @types/node and typescript ignored |
github-actions |
workflow SHA pins | — |
docker |
Dockerfile (including digests) |
major updates for the node image ignored |
PR commits / titles use chore(deps): … / chore(deps-dev): …. With the current semantic-release releaseRules, those scopes trigger a patch release when merged to main.
Enable on GitHub (Settings → Advanced Security, or the Security tab):
- Dependency graph
- Dependabot alerts
- Dependabot security updates
- Dependabot version updates (picks up
dependabot.ymlonmain)
Then Settings → Actions → General: Allow GitHub Actions to create and approve pull requests if you want Dependabot to re-run CI / rebase cleanly.
npm run ci
git checkout -b feat/my-feature
git add .
git commit -m "feat: short description"
git push -u origin feat/my-featureOpen a PR to main, then merge (ideally squash-merge with a conventional title). semantic-release runs on the resulting push to main.
- Docker + Docker Compose
- Apache 2.4 with modules:
proxy,proxy_http,proxy_wstunnel,rewrite,headers,ssl - A domain name pointing at the server
- Certbot (Let's Encrypt) for the SSL certificate
git clone <repo> /opt/markit && cd /opt/markit
cp .env.example .env# Generate secrets
openssl rand -base64 32 # → POSTGRES_DB_PASSWORD
openssl rand -base64 32 # → NEXTAUTH_SECRETEdit /opt/markit/.env:
POSTGRES_USER=markit
POSTGRES_DB=markit
POSTGRES_DB_PASSWORD=<generated secret>
NEXTAUTH_URL=https://markit.example.com
NEXTAUTH_SECRET=<generated secret>docker compose up -d --buildContainers start, migrations are applied automatically, and default phrases are loaded.
Verify everything is running:
docker compose ps
docker compose logs -f appEnable required modules:
a2enmod proxy proxy_http proxy_wstunnel rewrite headers sslObtain the SSL certificate:
certbot certonly --standalone -d markit.example.comCopy and enable the Apache configuration:
# Adapt ServerName in the file
cp /opt/markit/apache/markit.conf /etc/apache2/sites-available/markit.conf
# Replace markit.example.com with your real domain
nano /etc/apache2/sites-available/markit.conf
a2ensite markit.conf
apache2ctl configtest # Check syntax
systemctl reload apache2The site is now available at https://markit.example.com.
Certbot installs an automatic cron job. Verify with:
certbot renew --dry-runThe apache/markit.conf file configures:
| Feature | Detail |
|---|---|
| HTTP→HTTPS redirect | Permanent RewriteRule (301) |
| SSL/TLS | TLS 1.2 and 1.3 only, modern ciphers |
| HSTS | max-age=63072000; includeSubDomains; preload (2 years) |
| HTTP proxy | ProxyPass to 127.0.0.1:3000 |
| WebSocket proxy | RewriteRule to ws://127.0.0.1:3000 for Socket.io |
| Security headers | X-Frame-Options, X-Content-Type-Options, Referrer-Policy |
| Forwarded headers | X-Forwarded-Proto: https sent to Next.js |
Important: replace
markit.example.comwith your real domain in the file.
| Variable | Required | Description |
|---|---|---|
POSTGRES_USER |
production | PostgreSQL user (default: markit) |
POSTGRES_DB |
production | Database name (default: markit) |
POSTGRES_DB_PASSWORD |
yes | PostgreSQL password (openssl rand -base64 32 is fine — encoded at Docker startup) |
DATABASE_URL |
optional | Full URL; in Docker prod, leave empty (built by the entrypoint). For local dev, URL to localhost |
PORT |
optional | HTTP port (default 3000). For local/Docker: keep NEXTAUTH_URL in sync |
NEXTAUTH_URL |
yes | Public site URL (https://... in prod, http://localhost:<PORT> in dev) |
NEXTAUTH_SECRET |
yes | JWT signing key — generate with openssl rand -base64 32 |
User → user account
Team → team with invite code
TeamMember → user↔team membership (role: OWNER/ADMIN/MEMBER)
Phrase → phrase bank entry (isDefault=true for shared phrases)
BingoCard → bingo board (rows × cols, freeCenter)
Cell → board cell (phrase + position)
CheckedCell → checked cell (by which user, when)
npm run db:migrate:deploy # Apply migrations (prod / Docker)
npm run db:migrate # Create a migration (dev)
npm run db:push # Sync schema without a migration (prototyping)
npm run db:seed # Load phrases + admin if configured
npm run db:studio # Open Prisma Studio
npm run db:generate # Regenerate the Prisma client# Dump
docker exec markit_db pg_dump -U markit markit > backup_$(date +%Y%m%d).sql
# Restore
docker exec -i markit_db psql -U markit markit < backup_20260101.sql| Measure | Implementation | Detail |
|---|---|---|
| Forced HTTPS | Apache 301 redirect + HSTS | 2 years, includeSubDomains, preload |
| Secure cookies | Secure + HttpOnly when HTTPS |
Via NextAuth useSecureCookies |
| Passwords | bcrypt (cost 12) | ~300ms/attempt, resistant to rainbow tables |
| JWT sessions | Signed with NEXTAUTH_SECRET, 7 days |
Token invalidated if the secret changes |
| API authorization | Team membership checks | Protection against IDOR attacks |
| Socket.io auth | NextAuth JWT verified server-side | userName is resolved on the server, not forgeable |
| Socket.io CORS | Restricted to NEXTAUTH_URL in prod |
* only in dev |
| CSP | Separate for dev/prod, no unsafe-eval in prod |
Helps mitigate XSS |
| X-Frame-Options | SAMEORIGIN |
Anti-clickjacking |
| X-Content-Type-Options | nosniff |
Anti-MIME sniffing |
| X-Forwarded-For | Overwritten by Apache | Prevents client IP spoofing |
| Request size | Apache LimitRequestBody 1MB |
Mitigates simple DoS |
| Field lengths | Validated server-side on all APIs | Prevents oversized payloads |
| Isolated ports | App on 127.0.0.1, DB with no public port |
Unreachable from the Internet |
| Server headers | X-Powered-By and Server removed |
Does not reveal the stack |
| TLS | TLS 1.2/1.3 only, AEAD ciphers | SSLv3/TLS 1.0/1.1 disabled |
-
Auth rate limiting — implement with Fail2ban or Apache
mod_ratelimiton/api/auth/signinand/api/auth/registerto limit brute-force attempts.Example Fail2ban filter (
/etc/fail2ban/filter.d/markit-auth.conf):[Definition] failregex = ^<HOST> .* "POST /api/auth/callback/credentials HTTP.*" 401 ignoreregex =
-
HSTS preload submission — the
preloadheader is set, but submission to https://hstspreload.org/ must be done manually once the domain is stable. -
Regular updates:
docker compose pull && docker compose up -d --build npm audit fix -
Log monitoring — watch
markit_error.logand Docker logs for unusual behavior.
# ── Development ─────────────────────────────────────────────
npm run dev # Development server (port 3000)
npm run build # Production build
npm run lint # ESLint
npm run lint:md # Markdownlint
npm run typecheck # TypeScript check
npm run test # Unit tests (bingo, account, authz)
npm run ci # Local checks (no network audit)
npm run ci:full # Full CI checks (+ npm audit)
npm run release # semantic-release (used by the CI job)
# ── Database ────────────────────────────────────────────────
npm run db:migrate:deploy # Apply migrations
npm run db:migrate # Create + apply a migration (dev)
npm run db:push # Sync schema (prototyping)
npm run db:seed # Load default phrases
npm run db:studio # Prisma Studio UI
# ── User account ────────────────────────────────────────────
# /account page: first name + password change (all users)
# ── Docker ──────────────────────────────────────────────────
docker compose up -d # Start all services
docker compose up -d --build # Rebuild + start
docker compose down # Stop services
docker compose logs -f app # App logs
docker compose logs -f postgres # Database logs
docker compose restart app # Restart the app
# ── Database backup ─────────────────────────────────────────
docker exec markit_db pg_dump -U markit markit > backup.sql
docker exec -i markit_db psql -U markit markit < backup.sql