🎴 Vibe-Draft Architecture

System Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        Val Town                              β”‚
β”‚                                                              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚               Frontend (React)                      β”‚    β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”‚    β”‚
β”‚  β”‚  β”‚ CardDeckβ”‚  β”‚PromptStackβ”‚ β”‚VibeOutput  β”‚       β”‚    β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜       β”‚    β”‚
β”‚  β”‚       β”‚             β”‚               β”‚              β”‚    β”‚
β”‚  β”‚       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β”‚    β”‚
β”‚  β”‚                     β”‚                               β”‚    β”‚
β”‚  β”‚              β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”                       β”‚    β”‚
β”‚  β”‚              β”‚   App.tsx   β”‚                       β”‚    β”‚
β”‚  β”‚              β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜                       β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚                        β”‚                                    β”‚
β”‚                   SSE  β”‚  HTTP                             β”‚
β”‚                        β”‚                                    β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚            Backend (Hono)                          β”‚    β”‚
β”‚  β”‚                                                     β”‚    β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚    β”‚
β”‚  β”‚  β”‚SSE Hub  β”‚  β”‚ API Routesβ”‚  β”‚ Vibe Engine   β”‚   β”‚    β”‚
β”‚  β”‚  β”‚broadcastβ”‚  β”‚  /api/*   β”‚  β”‚ (OpenAI)      β”‚   β”‚    β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚    β”‚
β”‚  β”‚       β”‚             β”‚                 β”‚            β”‚    β”‚
β”‚  β”‚       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β”‚    β”‚
β”‚  β”‚                     β”‚                               β”‚    β”‚
β”‚  β”‚              β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”                     β”‚    β”‚
β”‚  β”‚              β”‚  Database      β”‚                     β”‚    β”‚
β”‚  β”‚              β”‚  (SQLite)      β”‚                     β”‚    β”‚
β”‚  β”‚              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                     β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚                                                              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Data Flow

1. Join Session

Browser β†’ POST /api/session/:id/join
        ↓
     Create/Find Session in SQLite
        ↓
     Add Player with Turn Order
        ↓
     Broadcast "player-joined" via SSE
        ↓
     Return Session + Player Data

2. Play Card

Browser β†’ POST /api/session/:id/play
        ↓
     Insert Card into prompt_stack Table
        ↓
     Update current_turn in Session
        ↓
     Broadcast "card-played" via SSE
        ↓
     All Clients Update UI

3. Generate Vibe

Browser β†’ POST /api/session/:id/generate
        ↓
     Fetch All Cards from prompt_stack
        ↓
     Vibe Engine Synthesizes Prompt
        ↓
     Call OpenAI API
        ↓
     Save Result to vibe_results Table
        ↓
     Broadcast "vibe-update" via SSE
        ↓
     All Clients Show Response

SSE Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Client 1 β”‚     β”‚ Client 2 β”‚     β”‚ Client 3 β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
     β”‚                β”‚                β”‚
     β”‚   EventSource  β”‚   EventSource  β”‚   EventSource
     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
     β”‚                β”‚                β”‚
     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                      β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚   SSE Hub      β”‚
              β”‚ (Maintains     β”‚
              β”‚  connections)  β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                      β”‚
              broadcast(message)
                      β”‚
     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
     β”‚                                  β”‚
     β–Ό                                  β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  card-playedβ”‚              β”‚ turn-changedβ”‚
β”‚ vibe-update β”‚              β”‚player-joinedβ”‚
β”‚ player-left β”‚              β”‚    etc.     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Database Schema

sessions_v1
β”œβ”€β”€ id (PK)
β”œβ”€β”€ created_at
β”œβ”€β”€ current_turn
β”œβ”€β”€ max_players
└── status

players_v1
β”œβ”€β”€ id (PK)
β”œβ”€β”€ session_id (FK)
β”œβ”€β”€ name
β”œβ”€β”€ avatar
β”œβ”€β”€ is_active
β”œβ”€β”€ turn_order
└── last_seen

prompt_stack_v1
β”œβ”€β”€ id (PK)
β”œβ”€β”€ session_id (FK)
β”œβ”€β”€ player_name
β”œβ”€β”€ card_type
β”œβ”€β”€ content
β”œβ”€β”€ image_url
└── timestamp

vibe_results_v1
β”œβ”€β”€ id (PK)
β”œβ”€β”€ session_id (FK)
β”œβ”€β”€ prompt
β”œβ”€β”€ response
β”œβ”€β”€ token_count
└── timestamp

Turn Flow

Session: current_turn = 0

Players: [Alice, Bob, Charlie] (turn_order: 0, 1, 2)

Turn 0: Alice plays β†’ current_turn = 1
Turn 1: Bob plays   β†’ current_turn = 2
Turn 2: Charlie plays β†’ current_turn = 0 (wraps around)
Turn 0: Alice again β†’ ...

Card Stack Processing

Stack: [
  {type: "role", content: "Act as Rust expert"},
  {type: "tone", content: "ELI5"},
  {type: "token-limit", content: "50 tokens"}
]

Vibe Engine:
1. Extract role β†’ "Act as Rust expert"
2. Extract tone β†’ "ELI5"
3. Extract token limit β†’ 50
4. Build prompt:
   """
   Act as Rust expert

   ELI5

   IMPORTANT: Keep your response under 50 tokens.
   """

5. Call OpenAI with prompt + user query
6. Return response

Component Hierarchy

App
β”œβ”€β”€ PlayerList
β”‚   └── Player Cards (active + spectators)
β”œβ”€β”€ PromptStack
β”‚   └── Played Card History
β”œβ”€β”€ VibeOutput
β”‚   β”œβ”€β”€ Query Input
β”‚   β”œβ”€β”€ Generate Button
β”‚   └── AI Response Display
└── CardDeck
    β”œβ”€β”€ Card Grid (5 cards)
    └── Custom Card Modal

State Management

App Component State:
β”œβ”€β”€ sessionId: string
β”œβ”€β”€ playerName: string
β”œβ”€β”€ session: Session
β”œβ”€β”€ players: Player[]
β”œβ”€β”€ stack: PlayedCard[]
β”œβ”€β”€ hand: Card[]
β”œβ”€β”€ currentPlayer: Player
β”œβ”€β”€ isMyTurn: boolean
β”œβ”€β”€ latestResult: PromptResult
└── generating: boolean

Effects:
β”œβ”€β”€ SSE Connection (updates on message)
└── fetchSessionState (on join/updates)

Security Notes

  • No Authentication: Sessions are public by ID
  • Input Validation: Sanitize all user inputs
  • Rate Limiting: Consider adding to /generate endpoint
  • XSS Protection: React handles by default
  • CSRF: Not applicable (no cookies/sessions)

Performance Considerations

  • Cold Start: ~2-3 seconds for database init
  • SSE Overhead: ~1KB per client per 30s (heartbeat)
  • Database: SQLite in-memory for active sessions
  • OpenAI: 2-5 seconds per generation (gpt-4o-mini)
  • Concurrent Players: Tested with 10 simultaneous users

Scaling Strategies

  1. Session Cleanup: Cron job to archive old sessions
  2. Connection Pooling: Val Town handles automatically
  3. CDN for Static Assets: Val Town edge caching
  4. Database Sharding: Separate tables per date range
  5. Rate Limiting: Add Redis-like counter for /generate

Legend:

  • β†’ : Data flow
  • ↓ : Sequential step
  • β”œβ”€β”€ : Component/structure hierarchy
  • (FK) : Foreign key
  • (PK) : Primary key