A complete step-by-step guide to set up, run, and use the ticket-gated video streaming platform.
- System Overview
- Architecture at a Glance
- Prerequisites
- Step 1: Project Setup
- Step 2: Environment Configuration
- Step 3: Generate Admin Password Hash
- Step 4: Database Initialization
- Step 5: Prepare Test Streams
- Step 6: Start the Services
- Step 7: Using the System
- API Reference
- Common Tasks
- Troubleshooting
This is a two-service video streaming platform with JWT-based token authentication:
- Viewers enter a token code to access restricted video streams
- Admins manage events, generate access tokens, and monitor usage
- HLS Media Server validates tokens on every segment request using cryptographic signatures (no database queries needed)
- Tokens can be revoked in real-time with <30-second propagation
✅ JWT-based playback authentication (HMAC-SHA256)
✅ In-memory revocation cache synced every 30 seconds
✅ Single-device enforcement (one token = one active viewer at a time)
✅ Session heartbeat & automatic release
✅ Local file serving + optional upstream proxy support
✅ Segment caching with LRU eviction
┌─────────────────────────────────────────┐
│ VIEWER PORTAL (Public) │
│ 1. Enter token code │
│ 2. Receive playback JWT │
│ 3. Watch stream with auto-refresh │
└──────────────┬──────────────────────────┘
│ JWT in Authorization header
│
┌──────▼─────────────────────────────────┐
│ PLATFORM APP (Next.js Port 3000) │
├──────────────────────────────────────┤
│ • Token validation API │
│ • JWT issuance & refresh │
│ • Admin console (protected) │
│ • Event/token CRUD │
│ • Revocation polling endpoint │
└──────────────┬──────────────────────┬──┘
│ │
│ │
│ Polls /api/revocations
│ every 30 seconds
│ ┌────────────────────┐
│ │ │
┌──────────────▼──────┴──────┐ ┌────────▼────────────────────────┐
│ DATABASE (SQLite/Postgres) │ │ HLS MEDIA SERVER (Express 4000)│
│ - Events │ │ • JWT signature verification │
│ - Tokens │ │ • Revocation cache check │
│ - Active sessions │ │ • Serve .m3u8 & .ts files │
│ - Admin passwords │ │ • Optional upstream proxy │
└─────────────────────────────┘ │ • Segment caching │
└─────────────────────────────────┘
Key Communication Flow:
- Viewer enters token code → Platform validates → Issues JWT (1-hour expiry)
- Browser attaches JWT to every HLS request (manifests & segments)
- HLS server validates JWT signature (CPU-only, ~0.01ms, no DB query)
- HLS server checks revocation cache (synced from Platform every 30s)
- If valid → serve stream | If revoked/expired → reject with 403
- Node.js 20+ (check with
node --version) - npm 10+ (check with
npm --version) - Optional: Docker & Docker Compose (for simplified local dev)
node --version # Should output v20.x.x or higher
npm --version # Should output 10.x.x or higher- macOS:
brew install node@20 - Windows: Download from nodejs.org
- Linux:
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt-get install -y nodejs
cd c:\code\VideoPlayer # Already in this directory for this guidels -la # On Windows: dirYou should see:
├── platform/ # Next.js Platform App
├── hls-server/ # Express.js HLS Media Server
├── shared/ # Shared types & utilities
├── scripts/ # Helper scripts (password hashing, etc.)
├── .env.example # Environment variable template
├── package.json # Root workspace config
├── README.md # Project README
├── PDR.md # Full product specification
└── DEPLOYMENT.md # Deployment guide
npm installThis installs dependencies for all three workspaces (shared, platform, hls-server).
Environment variables tell each service where to find the other service, what signing secret to use, and how to behave in dev vs. production.
cp .env.example .envOpen .env in your editor. You'll see:
# === Shared (CRITICAL: Must match both services) ===
PLAYBACK_SIGNING_SECRET= # You'll generate this in Step 3
INTERNAL_API_KEY= # You'll generate this in Step 3
# === Platform App ===
DATABASE_URL=file:./dev.db # SQLite for local dev (auto-created)
ADMIN_PASSWORD_HASH= # You'll generate this in Step 3
HLS_SERVER_BASE_URL=http://localhost:4000
NEXT_PUBLIC_APP_NAME=StreamGate
SESSION_TIMEOUT_SECONDS=60
# === HLS Media Server ===
PLATFORM_APP_URL=http://localhost:3000
STREAM_ROOT=./streams
UPSTREAM_ORIGIN= # Leave blank for local-only
SEGMENT_CACHE_ROOT=
SEGMENT_CACHE_MAX_SIZE_GB=50
SEGMENT_CACHE_MAX_AGE_HOURS=72
REVOCATION_POLL_INTERVAL_MS=30000
CORS_ALLOWED_ORIGIN=http://localhost:3000
PORT=4000You need three random secrets:
- PLAYBACK_SIGNING_SECRET — 32+ random characters (HMAC signing key)
- INTERNAL_API_KEY — Random string for internal endpoint protection
- ADMIN_PASSWORD_HASH — Bcrypt hash of your admin password
The next step will handle this.
npm run hash-passwordFollow the prompt:
Enter password: ___________
Confirm password: ___________
Output:
ADMIN_PASSWORD_HASH=<long bcrypt hash starting with $2a$>
Generate a 32+ character random string. Use any of these methods:
Method 1: PowerShell (easiest on Windows)
[System.Convert]::ToBase64String([System.Security.Cryptography.RandomNumberGenerator]::GetBytes(32))Method 2: Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"Method 3: OpenSSL
openssl rand -base64 32node -e "console.log(require('crypto').randomBytes(16).toString('hex'))"Edit .env and fill in the three values:
PLAYBACK_SIGNING_SECRET=<your-32-char-secret>
INTERNAL_API_KEY=<your-random-string>
ADMIN_PASSWORD_HASH=<output-from-hash-password>PLAYBACK_SIGNING_SECRET value must appear in both services. When you start the services, they will share this secret automatically.
The Platform App uses Prisma ORM to manage the database. By default, it uses SQLite (dev.db) for local development.
Migrations create the database schema (tables for Events, Tokens, ActiveSessions, etc.):
cd platform
set -a
source ../.env
set +a
npx prisma migrate dev --name init
npx prisma generatePrisma v7 reads the datasource from platform/prisma.config.ts, so DATABASE_URL must be available in the shell before running the migrate command.
The npx prisma generate step creates platform/src/generated/prisma/*, which the app imports at runtime.
First run will:
- Create
dev.dbin theplatform/directory - Apply all migrations (creating tables)
- Optionally seed sample data
When prompted:
? Do you want to continue? [Y/n] › y
Populate the database with test events and tokens:
npx prisma db seedThis runs platform/prisma/seed.ts, which creates:
- 2 test events
- 5 test tokens per event
- Timestamps for demo purposes
Open Prisma Studio to visually browse the database:
npx prisma studioThis opens a web UI at http://localhost:5555 showing all records. Useful for debugging!
HLS requires actual video stream files. For local testing, you need at least:
- A playlist file (
stream.m3u8) - Media segment files (
segment-000.ts,segment-001.ts, etc.)
The HLS server expects streams at:
hls-server/streams/
└── <event-id>/
├── stream.m3u8 # HLS playlist
├── segment-000.ts # First 2-second segment
├── segment-001.ts # Second segment
└── ... more segments
The <event-id> matches the Event ID in the database.
Assuming you seeded the database, it created an event with ID 847fd0b6-3ac3-48a8-9027-d7b7d09fb9a2. Create the stream directory:
mkdir -p hls-server/streams/847fd0b6-3ac3-48a8-9027-d7b7d09fb9a2Create hls-server/streams/847fd0b6-3ac3-48a8-9027-d7b7d09fb9a2/stream.m3u8:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:2
#EXTINF:2.0,
segment-000.ts
#EXTINF:2.0,
segment-001.ts
#EXTINF:2.0,
segment-002.ts
#EXTINF:2.0,
segment-003.ts
#EXT-X-ENDLISTFor testing JWT validation without actual video, create minimal TS files:
# PowerShell on Windows
cd hls-server/streams/847fd0b6-3ac3-48a8-9027-d7b7d09fb9a2
# Create empty segment files (minimal valid TS container)
foreach ($i in 0..7) {
$bytes = [System.Text.Encoding]::UTF8.GetBytes("dummy-segment-$i")
[System.IO.File]::WriteAllBytes("segment-00$i.ts", $bytes)
}Or on macOS/Linux:
cd hls-server/streams/847fd0b6-3ac3-48a8-9027-d7b7d09fb9a2
for i in {0..7}; do echo "dummy-segment-$i" > segment-00$i.ts; donels -la hls-server/streams/847fd0b6-3ac3-48a8-9027-d7b7d09fb9a2/You should see:
stream.m3u8
segment-000.ts
segment-001.ts
... segment-007.ts
You need two terminal windows (or tabs) — one for each service. They run independently.
cd platform
set -a
source ../.env
set +a
PORT=3000 npm run devUse PORT=3000 here because the shared root .env includes PORT=4000 for the HLS server.
Expected output:
> next dev
▲ Next.js 14.2.0
- Local: http://localhost:3000
- Environments: .env
✓ Ready in 1203ms
The Platform App is now running at http://localhost:3000
cd hls-server
set -a
source ../.env
set +a
npm run devExpected output:
HLS Media Server listening on port 4000
Revocation sync started with interval 30000ms
The HLS Media Server is now running at http://localhost:4000
Test the Platform App:
curl http://localhost:3000/
# Should return HTML (Next.js page)Test the HLS Server Health Endpoint:
curl http://localhost:4000/health
# Should return JSON like: {"status":"ok","database":"connected","cache":"ready"}Now that both services are running, let's walk through a complete user journey.
Open your browser and navigate to:
http://localhost:3000
You should see:
- A Token Entry screen with an input field
- Branding (default: "StreamGate" or your custom
NEXT_PUBLIC_APP_NAME) - Instructions: "Enter your access token"
You have two ways to get a token:
Option A: Use a Seeded Token (If you ran npx prisma db seed)
If you seeded the database, tokens were auto-generated. You can find them by:
cd platform
npx prisma studioThen navigate to the Token table. Copy any token with isRevoked = false.
Example token codes: ABC123DEF456, XYZ789UVW012, etc.
Option B: Generate a New Token via the Admin Console
This is covered in Step 7.4 below.
-
Back in the Viewer Portal, paste a token code into the input field.
-
Click "Start Watching" (or press Enter).
-
If valid, you'll be taken to the Player Screen showing:
- A full-screen HLS video player
- Stream controls (play, pause, volume, fullscreen, seek)
- A back button to enter a new token
- A countdown timer showing time until token expires
-
The player will:
- Fetch the playlist (
stream.m3u8) from the HLS server - Attach your JWT to every request (
Authorization: Bearer <jwt>) - Stream segments (
segment-000.ts, etc.) - Auto-refresh the JWT every 50 minutes to keep it alive
- Send heartbeat every 30 seconds to maintain the active session
- Fetch the playlist (
-
Close the tab or click the back button to release the session (allowing another device to use the token).
Navigate to:
http://localhost:3000/admin
You'll see a Login page. Enter your admin password (the one you hashed in Step 3).
Expected login credentials:
- Username:
admin(implicit, auto-filled) - Password:
<your-password-you-chose-in-step-3>
After login, you'll see the Admin Console with three sections:
A. Events Tab
- List of all streaming events (live & archived)
- Create Event form:
- Title: Name of the event
- Description: Details about the stream
- Starts At / Ends At: Time window
- Stream URL: Local path (e.g.,
./streams/event-id/) or upstream origin - Poster Image URL: Thumbnail
- Access Window: Hours from
endsAtwhen tokens are still valid (e.g., 48 hours) - Actions: Archive, Deactivate, Delete
Deactivating an event immediately revokes all active tokens for that event.
B. Tokens Tab
- List of all tokens for the selected event
- Generate New Tokens form:
- Event: Select target event
- Quantity: Number of tokens to generate (1–1000)
- Auto-generated: 12-character alphanumeric codes
- Actions: Copy, Revoke, Delete
Once a token is revoked, it's blocked on the HLS server immediately (≤30 seconds).
C. Revocations Tab (if available)
- Shows recently revoked tokens and deactivated events
- Useful for debugging revocation sync
-
Create Event:
- Title: "Live Concert 2026"
- Starts At:
2026-03-05 14:00:00 - Ends At:
2026-03-05 15:30:00 - Access Window:
48hours - Click "Create"
-
Generate Tokens:
- Select the event from the dropdown
- Quantity:
5 - Click "Generate"
- Tokens appear in the list (e.g.,
K7F2X9M4B1C3)
-
Distribute Tokens:
- Copy each token code
- Share via email, QR code, or print
-
Viewer Uses Token:
- Viewer enters the token on the Viewer Portal
- Portal validates against the database
- Portal issues a JWT
- Viewer can now watch the stream
Validate an access token and get a playback JWT
Request:
{
"code": "K7F2X9M4B1C3"
}Response (200 OK):
{
"data": {
"accessToken": "eyJhbGci...",
"tokenType": "Bearer",
"expiresIn": 3600,
"eventId": "847fd0b6-3ac3-48a8-9027-d7b7d09fb9a2",
"streamPath": "/streams/847fd0b6-3ac3-48a8-9027-d7b7d09fb9a2/"
}
}Error Responses:
400— Invalid/missing code404— Token not found410— Token expired409— Token already in use (single-device enforcement)403— Token revoked or event deactivated
Refresh a nearly-expired JWT
Request:
Authorization: Bearer <jwt>
Response (200 OK):
{
"data": {
"accessToken": "eyJhbGci...",
"expiresIn": 3600
}
}Update session's lastHeartbeat timestamp (keep session alive)
Request:
Authorization: Bearer <jwt>
Response (200 OK):
{
"data": {
"status": "ok",
"lastHeartbeat": 1741200000
}
}Release the active session (free up the token for another device)
Request:
Authorization: Bearer <jwt>
Response (200 OK):
{
"data": {
"status": "released"
}
}Sync revoked tokens & deactivated events (INTERNAL, requires X-Internal-Api-Key header)
Request:
GET /api/revocations?since=1741190000
X-Internal-Api-Key: <INTERNAL_API_KEY>
Response (200 OK):
{
"data": [
{
"type": "token_revocation",
"code": "K7F2X9M4B1C3",
"revokedAt": 1741195000
},
{
"type": "event_deactivation",
"eventId": "847fd0b6-xxx",
"deactivatedAt": 1741195100
}
]
}Health check (no auth required)
Response (200 OK):
{
"status": "ok",
"uptime": 12345,
"cache": { "size": 15, "hits": 1000, "misses": 50 },
"lastSync": 1741200000
}Fetch HLS playlist (requires valid JWT)
Request:
GET /streams/847fd0b6-3ac3-48a8-9027-d7b7d09fb9a2/stream.m3u8
Authorization: Bearer <jwt>
Response (200 OK):
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:2
#EXTINF:2.0,
segment-000.ts
#EXTINF:2.0,
segment-001.ts
...
#EXT-X-ENDLISTError Responses:
401— Missing or invalid JWT403— Token revoked, event deactivated, or path mismatch410— JWT expired
Fetch media segment (requires valid JWT)
Same as above — every segment request requires a valid JWT in the Authorization header.
- Log in to Admin Console (
http://localhost:3000/admin) - Go to Events tab
- Fill in the form:
- Title:
"My Live Event" - Starts At:
2026-03-05 18:00:00 - Ends At:
2026-03-05 19:00:00 - Access Window:
24(hours afterendsAt) - Click Create
- Title:
- Go to Tokens tab
- Select your new event from the dropdown
- Enter
Quantity: 10 - Click Generate
- Tokens appear in the list — copy and distribute
- Log in to Admin Console
- Go to Tokens tab
- Find the token you want to revoke
- Click the Revoke button next to it
- Token is immediately revoked — viewers using it will see "access ended" within 30 seconds
- Log in to Admin Console
- Go to Events tab
- Find the event
- Click Deactivate
- All tokens for that event are immediately revoked (within 30 seconds)
For production, you need actual HLS-encoded video files:
-
Encode video to HLS using FFmpeg:
ffmpeg -i input.mp4 -c:v libx264 -c:a aac -f hls \ -hls_time 2 -hls_playlist_type event \ -hls_segment_filename "segment-%03d.ts" \ stream.m3u8 -
Place in streams directory:
cp stream.m3u8 segment-*.ts hls-server/streams/<event-id>/
-
Or use upstream proxy (set
UPSTREAM_ORIGINin.envto upstream server URL, and HLS server will proxy segments)
To clear all events, tokens, and sessions:
cd platform
rm dev.db # Delete the SQLite database
npx prisma migrate dev # Re-run migrations to create new empty DB
npx prisma db seed # (Optional) Re-seed with test dataVisually inspect all records:
cd platform
npx prisma studioOpens http://localhost:5555 showing all tables, records, and relationships.
Symptom:
error: listen EADDRINUSE: address already in use :::3000
Solution:
Kill the process using the port:
Windows PowerShell:
Get-Process -Name "node" | Stop-Process -ForcemacOS/Linux:
lsof -ti:3000 | xargs kill -9 # Platform App
lsof -ti:4000 | xargs kill -9 # HLS ServerThen restart the services.
Symptoms:
- Viewer enters token, gets "Token not found" error
- Token was just generated but doesn't work
Causes:
- Token code has typo or wrong case (should be case-sensitive)
- Event is archived or deactivated
- Token is already revoked
- Token doesn't exist in database
Solution:
-
Verify token exists:
cd platform npx prisma studio # Check Tokens table for the code you entered
-
Check token status:
- If
isRevoked = true, token is revoked - If
expiresAtis in the past, token is expired - Make sure event
isActive = true
- If
-
Re-generate a fresh token:
- Go to Admin Console → Tokens tab
- Select the event
- Generate new tokens
- Use the new code immediately
Symptoms:
- Video player opens but stays at 0%
- Console shows 404 or 403 errors on
.m3u8request
Causes:
- Stream files not in correct directory
- JWT validation failing (secret mismatch)
HLS_SERVER_BASE_URLmisconfigured- HLS server not running
Solution:
-
Verify HLS server is running:
curl http://localhost:4000/health # Should return JSON with status: "ok" -
Check stream directory exists:
ls -la hls-server/streams/<event-id>/ # Should show stream.m3u8 and segment files
-
Verify PLAYBACK_SIGNING_SECRET matches in both
.envfiles:grep PLAYBACK_SIGNING_SECRET platform/.env grep PLAYBACK_SIGNING_SECRET hls-server/.env # Should output identical values -
Check HLS server logs: Look for JWT validation errors in Terminal 2. Verify JWT signature is being validated correctly.
-
Test JWT manually:
# Get a JWT first curl -X POST http://localhost:3000/api/tokens/validate \ -H "Content-Type: application/json" \ -d '{"code":"<your-token-code>"}' # Then test with that JWT on HLS server curl -H "Authorization: Bearer <jwt-from-above>" \ http://localhost:4000/streams/<event-id>/stream.m3u8 # Should return the .m3u8 content, not 403
Symptom:
- You revoke a token in Admin Console
- Viewer can still use the token for >30 seconds
Cause: HLS server caches revocations for the interval. Revocation propagation is eventual consistent:
- Maximum delay:
REVOCATION_POLL_INTERVAL_MS(default: 30 seconds) - HLS server polls Platform App for revoked tokens every 30 seconds
Solution:
- This is by design — the trade-off for sub-millisecond JWT validation
- If you need instant revocation, reduce
REVOCATION_POLL_INTERVAL_MSin.env(HLS server):REVOCATION_POLL_INTERVAL_MS=5000 # Poll every 5 seconds instead of 30 - Then restart the HLS server
Symptom:
- Can't log in to Admin Console
- "Incorrect password" message
Solution:
-
Verify password hash is in
.env:grep ADMIN_PASSWORD_HASH .env # Should show a hash starting with $2a$ or $2b$ -
Re-generate hash if needed:
npm run hash-password # Enter your desired password again # Copy the output to .env
-
Restart Platform App:
- Stop the running
npm run devin Terminal 1 - Update
.env - Restart the Platform App
- Try logging in again
- Stop the running
Symptom:
Error: database is locked
Solution:
-
Stop all services:
Get-Process -Name "node" | Stop-Process -Force
-
Remove database file:
rm platform/dev.db* -
Re-run migration:
cd platform npx prisma migrate dev -
Restart services
Symptom:
Access to XMLHttpRequest blocked by CORS policy
Solutions:
-
Verify
CORS_ALLOWED_ORIGINin HLS server.env:CORS_ALLOWED_ORIGIN=http://localhost:3000
-
Restart HLS server so changes take effect
-
In development, if using a proxy or different port, adjust
CORS_ALLOWED_ORIGINaccordingly
After you've got the system running locally:
- Read the PDR.md for full API contracts and database schema
- Review IMPLEMENTATION_PLAN.md for development task breakdown
- Check DEPLOYMENT.md for production deployment (Docker, cloud platforms, etc.)
- Explore the code in
platform/src,hls-server/src, andshared/src - Write tests for custom features
# Check Node.js version
node --version
# View environment variables loaded
grep -v "^#" .env | grep -v "^$"
# Kill all Node processes
killall node # macOS/Linux
Get-Process node | Stop-Process -Force # Windows PowerShell
# View active connections on ports
lsof -i:3000 -i:4000 # macOS/Linux
netstat -ano | findstr ":3000\|:4000" # Windows
# Clean install dependencies
rm -rf node_modules package-lock.json
npm install
# Type-check without running
npm run typecheck # In platform/ or hls-server/
# Lint code
npm run lint # In root-
Platform App logs (Terminal 1):
- Look for
ready,compiled, or error messages - Check for API routes being loaded
- Look for
-
HLS Server logs (Terminal 2):
- Look for
listening on port 4000 - Check for revocation sync starting
- Watch for JWT validation errors when streaming
- Look for
-
Browser console (
F12in viewer/admin):- Check for fetch/XHR errors
- Look for JWT issues or CORS errors
- Check video player (hls.js) errors
You're all set! Start with Step 1 if you haven't already, and work through each section sequentially. 🚀