Skip to main content

Data Model

StreamGate uses Prisma ORM with three core models: Event, Token, and ActiveSession. The schema is defined in platform/prisma/schema.prisma.

Entity Relationship Diagram

┌─────────────────────────────────────┐
│ Event │
│─────────────────────────────────────│
│ id String (PK, UUID) │
│ title String │
│ description String? │
│ streamUrl String? │
│ posterUrl String? │
│ startsAt DateTime │
│ endsAt DateTime │
│ accessWindowHours Int (default 48)│
│ isActive Boolean (default T) │
│ isArchived Boolean (default F) │
│ createdAt DateTime (auto) │
│ updatedAt DateTime (auto) │
└─────────────┬───────────────────────┘
│ 1:N

┌─────────────▼───────────────────────┐
│ Token │
│─────────────────────────────────────│
│ id String (PK, UUID) │
│ code String (UNIQUE) │
│ eventId String (FK→Event) │
│ label String? │
│ isRevoked Boolean (default F) │
│ revokedAt DateTime? │
│ redeemedAt DateTime? │
│ redeemedIp String? │
│ expiresAt DateTime │
│ createdAt DateTime (auto) │
└─────────────┬───────────────────────┘
│ 1:N

┌─────────────▼───────────────────────┐
│ ActiveSession │
│─────────────────────────────────────│
│ id String (PK, UUID) │
│ tokenId String (FK→Token) │
│ sessionId String (UNIQUE) │
│ lastHeartbeat DateTime (auto) │
│ clientIp String │
│ userAgent String? │
│ createdAt DateTime (auto) │
└─────────────────────────────────────┘

Relationships:

  • An Event has many Tokens (one-to-many, cascade delete)
  • A Token has many ActiveSessions (one-to-many, cascade delete)
  • In practice, single-device enforcement means at most one ActiveSession per Token at any given time

Event Model

An event represents a streaming occasion — a live broadcast, recorded session, or on-demand content.

FieldTypeDefaultDescription
idStringUUID (auto)Primary key
titleStringDisplay title shown to viewers
descriptionString?nullOptional description
streamUrlString?nullUpstream origin URL (proxy/hybrid mode)
posterUrlString?nullPoster image URL for pre-event/ended screens
startsAtDateTimeScheduled start time
endsAtDateTimeScheduled end time
accessWindowHoursInt48Hours after endsAt that tokens remain valid
isActiveBooleantrueWhether the event is accessible to viewers
isArchivedBooleanfalseWhether the event is archived (hidden from active listings)
createdAtDateTimenow()Creation timestamp
updatedAtDateTimeautoLast modification timestamp

Event States

isActive=true, isArchived=false → Active (viewers can access)
isActive=false, isArchived=false → Deactivated (all tokens effectively revoked)
isActive=true, isArchived=true → Archived (hidden but technically accessible)
isActive=false, isArchived=true → Archived + Deactivated
info

Deactivating an event triggers a revocation cascade: the HLS server's revocation sync picks up all token codes for deactivated events via the eventDeactivations array in the sync response.

Token Model

A token is a unique access code that grants a viewer permission to watch a specific event.

FieldTypeDefaultDescription
idStringUUID (auto)Primary key
codeString12-char base62 access code (unique)
eventIdStringForeign key to Event
labelString?nullAdmin-assigned label (e.g., "VIP Guest 1")
isRevokedBooleanfalseWhether the token has been revoked by admin
revokedAtDateTime?nullWhen the token was revoked
redeemedAtDateTime?nullWhen the token was first used
redeemedIpString?nullIP address of first redemption
expiresAtDateTimeComputed: event.endsAt + event.accessWindowHours
createdAtDateTimenow()Creation timestamp

Token Code Generation

Codes are generated using cryptographically secure random bytes:

import crypto from 'node:crypto';

const TOKEN_CODE_LENGTH = 12;
const TOKEN_CODE_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

function generateTokenCode(): string {
const bytes = crypto.randomBytes(TOKEN_CODE_LENGTH);
let code = '';
for (let i = 0; i < TOKEN_CODE_LENGTH; i++) {
code += TOKEN_CODE_CHARSET[bytes[i] % TOKEN_CODE_CHARSET.length];
}
return code;
}
  • Character set: 62 characters (a-z, A-Z, 0-9) — base62
  • Length: 12 characters
  • Entropy: ~71 bits (log₂(62¹²) ≈ 71.45)
  • Uniqueness: Guaranteed within batches using a Set; database UNIQUE constraint as final guard

Token Expiry Computation

Token expiry is computed at creation time, not checked dynamically:

expiresAt = event.endsAt + event.accessWindowHours (in hours)

For example, an event ending at 2025-03-15T17:00:00Z with a 48-hour access window produces tokens expiring at 2025-03-17T17:00:00Z.

warning

If the event's endsAt or accessWindowHours is updated after tokens are created, existing tokens do not automatically update their expiresAt. Generate new tokens if schedule changes are significant.

Token Status

Tokens have a computed status based on their fields:

StatusCondition
unusedredeemedAt is null AND not expired AND not revoked
redeemedredeemedAt is set AND not expired AND not revoked
expiredexpiresAt < now
revokedisRevoked is true

ActiveSession Model

An active session represents a currently viewing user. It enforces single-device access.

FieldTypeDefaultDescription
idStringUUID (auto)Primary key
tokenIdStringForeign key to Token
sessionIdStringUUID (auto)Unique session identifier (matches JWT sid claim)
lastHeartbeatDateTimenow()Last heartbeat timestamp
clientIpStringViewer's IP address
userAgentString?nullBrowser user agent string
createdAtDateTimenow()Session creation time

Session Lifecycle

1. CREATE Viewer validates token → createSession()
Creates ActiveSession with fresh sessionId
Cleans up any stale sessions for this token

2. HEARTBEAT Every 30 seconds → updateHeartbeat()
Updates lastHeartbeat to now()

3. RELEASE Player closes → releaseSession()
Deletes the ActiveSession record

4. TIMEOUT No heartbeat for 60 seconds
Session considered abandoned
Next validation request cleans it up
Another device can now use the token
tip

The session timeout is configurable via SESSION_TIMEOUT_SECONDS (default: 60). A session is stale when lastHeartbeat < now - SESSION_TIMEOUT_SECONDS.

Access Rules

Platform-Level Validation (Token Validation Endpoint)

A token is valid for access when all of these conditions are met:

#CheckHTTP Status on Failure
1Token code exists in database401
2expiresAt > now410
3isRevoked === false403
4Associated event has isActive === true403
5No active session exists (or existing session timed out)409

HLS-Level Validation (Every Streaming Request)

The HLS server performs these checks on every .m3u8 and .ts request:

#CheckHTTP Status on Failure
1JWT signature is valid (HMAC-SHA256)403
2JWT has not expired (exp > now)403
3Request path starts with JWT's sp claim403
4If probe === true, request must be HEAD403
5Token code (sub) is not in the revocation cache403

Prisma Schema

platform/prisma/schema.prisma
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}

datasource db {
provider = "sqlite"
}

model Event {
id String @id @default(uuid())
title String
description String?
streamUrl String?
posterUrl String?
startsAt DateTime
endsAt DateTime
accessWindowHours Int @default(48)
isActive Boolean @default(true)
isArchived Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

tokens Token[]
}

model Token {
id String @id @default(uuid())
code String @unique
eventId String
label String?
isRevoked Boolean @default(false)
revokedAt DateTime?
redeemedAt DateTime?
redeemedIp String?
expiresAt DateTime
createdAt DateTime @default(now())

event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
activeSessions ActiveSession[]

@@index([eventId])
@@index([code])
@@index([isRevoked])
}

model ActiveSession {
id String @id @default(uuid())
tokenId String
sessionId String @unique
lastHeartbeat DateTime @default(now())
clientIp String
userAgent String?
createdAt DateTime @default(now())

token Token @relation(fields: [tokenId], references: [id], onDelete: Cascade)

@@index([tokenId])
@@index([sessionId])
@@index([lastHeartbeat])
}

Database Indexes

ModelIndexPurpose
Tokencode (unique)Fast O(1) lookup during token validation
TokeneventIdFast listing of tokens per event
TokenisRevokedFast filtering for revocation sync queries
ActiveSessionsessionId (unique)Fast session lookup during heartbeat/release
ActiveSessiontokenIdFast check for existing sessions per token
ActiveSessionlastHeartbeatFast identification of stale sessions

Migration Workflow

Development

cd platform

# Create a new migration after modifying schema.prisma
npx prisma migrate dev --name describe-your-change

# Reset database (drops all data)
npx prisma migrate reset

# Open Prisma Studio (visual database browser)
npx prisma studio

Production

# Apply pending migrations (no interactive prompts)
npx prisma migrate deploy
danger

Never use prisma migrate dev in production. It can reset data. Always use prisma migrate deploy for production deployments.