Implement the two-tier storage architecture with:
- Redis (ioredis) → Active game state (hot, 24hr TTL)
- Supabase (supabase-js) → Users, history, content, invites (cold)
No ORM needed. Supabase-js provides typed query builder + type generation from schema.
Backend Services (NestJS)
├── ioredis → Active game state
├── @supabase/supabase-js → Users, history, content, invites
└── Supabase Auth → User authentication
| Table | Purpose | Key Fields |
|---|---|---|
users |
Managed by Supabase Auth | id, email, created_at |
profiles |
Extended user data | user_id, display_name, avatar_url |
game_history |
Archived completed games | id, user_ids[], final_state (JSONB), completed_at |
game_variants |
Game content (variants) | id, template_id, content (JSONB), is_published |
stories |
Narrative content | id, variant_id, content (JSONB) |
story_translations |
Localized content | story_id, locale, content (JSONB) |
invite_links |
Shareable game links | id, room_id, code, expires_at, max_uses, use_count |
| Pattern | Purpose | TTL |
|---|---|---|
game:{roomId} |
Active game state (JSON) | 24h |
game:{roomId}:players |
Player connection status | 24h |
session:{sessionId} |
User session data | 7d |
- Create Supabase project (or use existing)
- Define tables via Supabase dashboard or migrations
- Generate TypeScript types:
supabase gen types typescript - Add types to
packages/shared/src/database.types.ts
- Install
@supabase/supabase-js - Create
SupabaseServiceinapps/backend/src/core/ - Initialize client with service role key (server-side)
- Expose typed query methods
- Create
RedisServiceinapps/backend/src/core/ - Replace in-memory
Map<string, GameState>in GameStateManager - Implement key patterns with TTL
- Add connection pooling and error handling
- Add Supabase Auth middleware for tRPC context
- Extract user from JWT in request headers
- Create
profilestable for extended user data - Wire auth to game.join to track participation
- Create
invite_linkstable - Add
game.createInvite/game.joinByCodeprocedures - Generate short codes (e.g., 6 chars alphanumeric)
- Support expiration and usage limits
- On
GAME_OVERevent:- Read final state from Redis
- Insert into
game_historytable - Delete Redis key
- Link to user IDs for match history queries
Create a repository pattern to abstract content storage:
Interface (apps/backend/src/core/content-loader/):
export interface GameContentRepository {
getTemplate(templateId: string): Promise<GameTemplate | null>;
getVariant(variantId: string): Promise<GameVariant | null>;
getStory(storyId: string, locale?: string): Promise<StoryDefinition | null>;
listVariants(options?: { templateId?: string; tags?: string[]; published?: boolean }): Promise<GameVariant[]>;
}Implementations:
FileSystemContentRepository- Reads from/game-content/(development)DatabaseContentRepository- Queries Supabase tables (production)
Environment toggle:
CONTENT_SOURCE=filesystem # dev default
CONTENT_SOURCE=database # productionCaching layer (sits in front of repository):
Request → Redis Cache (30min TTL) → Repository (FS or DB) → Response
↓
Cache miss: fetch, store, return
Cache hit: return cached
Content seeding script:
pnpm content:seed # Migrate JSON files → Supabase tables
pnpm content:export # Export DB content → JSON files (backup)Migration steps:
- Create
GameContentRepositoryinterface - Implement
FileSystemContentRepository(extract from current ContentLoaderService) - Implement
DatabaseContentRepository(query Supabase) - Add
CachedContentRepositorywrapper (Redis caching) - Wire via NestJS DI based on
CONTENT_SOURCEenv var - Create
content:seedscript using Zod validation
# Backend
cd apps/backend
pnpm add @supabase/supabase-js ioredis
# Dev tools
pnpm add -D supabase# Supabase
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_KEY=eyJ... # Server-side only
# Redis
REDIS_URL=redis://localhost:6379
# Or separate:
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=# Generate types from Supabase schema
npx supabase gen types typescript --project-id YOUR_PROJECT_ID > packages/shared/src/database.types.ts
# Re-run after schema changesThese work streams can be developed simultaneously by different developers or in parallel branches:
| Track | Steps | Dependencies | Can Start After |
|---|---|---|---|
| A: Infrastructure | 1, 2, 3 | None | Immediately |
| B: Auth | 4 | Step 2 (SupabaseService) | Track A basics |
| C: Invite Links | 5 | Step 2, 4 | Track A + B |
| D: Game History | 6 | Step 2, 3 | Track A |
| E: Content Repository | 7 | Step 2, 3 | Track A |
Week 1:
├── Dev 1: Steps 1-3 (Supabase setup, Redis service)
└── Dev 2: Frontend work / other features
Week 2 (after Track A):
├── Dev 1: Step 4 (Auth integration)
├── Dev 2: Step 7 (Content repository)
└── Both can work in parallel
Week 3:
├── Dev 1: Step 5 (Invite links - needs auth)
└── Dev 2: Step 6 (Game history archival)
These can be built anytime:
FileSystemContentRepositoryimplementation (refactor existing code)- Content seeding script structure (add DB calls later)
- Redis key pattern utilities
- Type definitions in
packages/shared
- Auth middleware + tRPC context (Step 4)
- GameStateManager + RedisService swap (Step 3)
- ContentLoaderService + Repository pattern (Step 7)
-
Guest vs registered users?
- Support both: guests with localStorage ID, accounts for history
- Guest sessions stored in Redis, convert to account on signup
-
Row Level Security (RLS)?
- Enable on all tables
- Users can only read own profile/history
- Game content readable by all authenticated users
-
Realtime subscriptions?
- Not needed - using Socket.IO for game updates
- Could use for admin dashboard (content changes)
-
Database migrations?
- Use Supabase CLI:
supabase db push/supabase migration - Or manage via dashboard for simplicity
- Use Supabase CLI:
-
Local development?
- Option A: Use Supabase hosted (free tier)
- Option B:
supabase startfor local Docker instance