A secure, self-contained Next.js 14 portal for managing private beta testers. Testers redeem invite codes, join your GitHub org, and download the app binary — all through a minimal web interface.
- Framework: Next.js 14 App Router (TypeScript)
- Database: SQLite via
better-sqlite3— single file, zero external dependencies - Auth: Custom JWT sessions (
jsonwebtoken) + bcrypt-hashed invite codes - GitHub:
@octokit/restfor org membership invitations - Rate Limiting: In-memory per-IP limiting (resets on server restart)
npm installcp .env.example .envEdit .env and fill in:
| Variable | Required | Description |
|---|---|---|
ADMIN_SECRET |
Yes | Password for the /admin panel |
APP_SECRET |
Yes | Secret key for signing JWT session tokens |
GITHUB_TOKEN |
Yes | GitHub personal access token with admin:org scope |
GITHUB_ORG |
Yes | GitHub organization slug (e.g. my-org) |
DOWNLOAD_URL |
One of these | Public URL to redirect testers to for the download |
DOWNLOAD_FILE_PATH |
One of these | Absolute path to the binary file on the server |
npm run devVisit http://localhost:3000.
Navigate to http://localhost:3000/admin, enter your ADMIN_SECRET, then create a code.
npm run build
npm startThe SQLite database is created automatically at data/dirac.db on first run. Ensure the data/ directory is writable by the process. In production, mount this as a persistent volume.
The GITHUB_TOKEN must be a classic personal access token with the admin:org scope, or a fine-grained token with organization membership write permissions.
- Invite codes are never stored in plaintext — only bcrypt hashes (cost 12) are persisted
- The admin panel uses session storage (cleared on tab close) for the admin secret
- Session tokens (JWT, 24h expiry) are stored in
localStorage— acceptable for this use case since the data is not sensitive beyond download access - Rate limiting is per-IP, 5 attempts per 15-minute window
- All admin API routes verify the
Authorization: Bearer {ADMIN_SECRET}header
/
├── app/
│ ├── layout.tsx # Root layout
│ ├── page.tsx # / — Code entry page
│ ├── welcome/
│ │ └── page.tsx # /welcome — GitHub invite + download
│ ├── admin/
│ │ └── page.tsx # /admin — Admin panel
│ └── api/
│ ├── validate-code/
│ │ └── route.ts # POST /api/validate-code
│ ├── invite-github/
│ │ └── route.ts # POST /api/invite-github
│ ├── download/
│ │ └── route.ts # GET /api/download
│ └── admin/
│ └── codes/
│ ├── route.ts # GET + POST /api/admin/codes
│ └── [id]/
│ └── route.ts # DELETE /api/admin/codes/:id
├── lib/
│ ├── db.ts # SQLite setup, schema, query helpers
│ ├── auth.ts # JWT issue/verify, admin secret validation
│ ├── github.ts # Octokit org invite helper
│ └── rateLimit.ts # In-memory IP rate limiter
├── data/
│ └── dirac.db # SQLite DB (auto-created, git-ignored)
├── .env.example
├── .gitignore
├── next.config.js
├── package.json
└── tsconfig.json
- Tester visits
/and enters their invite code - Code is validated against bcrypt hashes — if valid, a JWT session token is issued
- Tester is redirected to
/welcome?name=Alice - Tester enters their GitHub username — the API sends an org invitation
- Tester clicks "Download Dirac" — the API validates the session and serves the file
- The invite code is marked as used after the GitHub step completes
- Visit
/adminand enterADMIN_SECRET - Create new invite codes (name + code)
- View all codes with status, created date, and GitHub username
- Delete codes as needed