> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kanaeru.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Blueprint examples

> Every blueprint type from Nadia's Mafia game: real editor screenshots, rendered views, and the source Kana writes.

These blueprints come from **Nadia's** online Mafia game, the example product in the Kanaeru episodes: a phone-first, chat-only game for 8 to 15 friends, with private rooms, secret roles, and timed rounds. Watch her [Vision](https://www.kanaeru.ai/blog/2026-08-31-vision-session-from-idea-to-plan), [Planning](https://www.kanaeru.ai/blog/2026-09-01-planning-session-from-outcome-to-reviewable-work), and [Review](https://www.kanaeru.ai/blog/2026-09-02-review-session-caught-the-bug-before-code) sessions to see them made.

Most examples are from her **automated rounds** outcome: the game deals roles, runs night and day on a shared clock, and counts votes, so nobody has to sit out as narrator. The screenshots are from her **private rooms** outcome, as shown in the episodes.

<Frame caption="A journey in Plan & Review: its acceptance criteria, and the seven blueprints that support them.">
  <img src="https://mintcdn.com/kanaerulabs/_FsoVBjQWp9cMeEf/images/nadia/journey-scenarios-blueprints.jpg?fit=max&auto=format&n=_FsoVBjQWp9cMeEf&q=85&s=8c1cfa5fcaee8b9bde0a569567c7720e" alt="Plan & Review showing a journey's acceptance criteria and a Blueprints row with seven blueprints" width="1600" height="900" data-path="images/nadia/journey-scenarios-blueprints.jpg" />
</Frame>

Each example has up to three tabs: **In Kanaeru** is the real editor, **Rendered** is a simplified view you can read here, and **Source** is exactly what Kana writes. You can [export](/guides/blueprint-editor#exporting) the source of any blueprint.

## Intent Spec

Kana writes the intent spec first. Every other blueprint has to serve it.

<Tabs>
  <Tab title="Rendered">
    **Intent:** The game runs the night and day rounds itself, on a clock every player can see, so nobody has to sit out and narrate.

    **Why:** Somebody currently has to leave the game to deal roles, wake people up and count votes. That person never plays. Every part of this outcome exists so that job disappears rather than rotates.

    | Success | Verifiable |
    | - | - |
    | A full game runs from first night to a win with no human narrator | Yes |
    | A phase change reaches every player's screen within 2 seconds | Yes |
    | An action submitted before a phase closes is always counted | Yes |

    * **Non-negotiable:** a player never receives another player's role; the server owns the clock; a client only displays it
    * **Acceptable tradeoffs:** a player who is offline when a phase closes loses that phase's action; the round history is kept for the game only, not archived

    **Escalate**

    * **When** two players report seeing different phases of the same game: Freeze the round and raise it with the owner — a split clock silently invalidates every vote after it
    * **When** a role appears in a response sent to a player who does not own it: Stop the game and rotate the deal — the game is unwinnable and nobody can tell
  </Tab>

  <Tab title="Source">
    ```json intent-spec.json theme={null}
    {
      "intent": "The game runs the night and day rounds itself, on a clock every player can see, so nobody has to sit out and narrate.",
      "why": "Somebody currently has to leave the game to deal roles, wake people up and count votes. That person never plays. Every part of this outcome exists so that job disappears rather than rotates.",
      "success": [
        {
          "statement": "A full game runs from first night to a win with no human narrator",
          "verifiable": true
        },
        {
          "statement": "A phase change reaches every player's screen within 2 seconds",
          "verifiable": true,
          "changeType": "modified"
        },
        {
          "statement": "An action submitted before a phase closes is always counted",
          "verifiable": true
        }
      ],
      "stakes": {
        "nonNegotiable": [
          "A player never receives another player's role",
          "The server owns the clock; a client only displays it"
        ],
        "acceptableTradeoffs": [
          "A player who is offline when a phase closes loses that phase's action",
          "The round history is kept for the game only, not archived"
        ]
      },
      "constraints": [
        {
          "rule": "Must work in a mobile browser, one-handed",
          "hard": true
        },
        {
          "rule": "No accounts; a seat is a one-time token from the join link",
          "hard": true
        }
      ],
      "escalate": [
        {
          "when": "Two players report seeing different phases of the same game",
          "action": "Freeze the round and raise it with the owner — a split clock silently invalidates every vote after it"
        },
        {
          "when": "A role appears in a response sent to a player who does not own it",
          "action": "Stop the game and rotate the deal — the game is unwinnable and nobody can tell"
        }
      ]
    }
    ```
  </Tab>
</Tabs>

## UX Mockup

A live, clickable prototype of the real route, not a picture. Nadia's private rooms mockup has 21 screens: the happy path, failures, recovery, privacy, and a full room. Each change is marked, so she can review it in place.

<Tabs>
  <Tab title="In Kanaeru">
    <Frame caption="The screen list on the left, the live prototype on the right. Review 38 counts the changes left to review.">
      <img src="https://mintcdn.com/kanaerulabs/_FsoVBjQWp9cMeEf/images/nadia/review-ux-scope.jpg?fit=max&auto=format&n=_FsoVBjQWp9cMeEf&q=85&s=40682409e0fb146cdcd106263bfb8f91" alt="UX mockup editor listing 21 screens beside a live Mafia room prototype" width="1600" height="900" data-path="images/nadia/review-ux-scope.jpg" />
    </Frame>

    <Frame caption="After Nadia decides how invitation rotation works, Kana updates the prototype to show it.">
      <img src="https://mintcdn.com/kanaerulabs/_FsoVBjQWp9cMeEf/images/nadia/review-invite-rotation.jpg?fit=max&auto=format&n=_FsoVBjQWp9cMeEf&q=85&s=dd479972768f8825de3139d7aab4f4e0" alt="Prototype screen: new invitation card, three seats retained" width="1600" height="900" data-path="images/nadia/review-invite-rotation.jpg" />
    </Frame>
  </Tab>
</Tabs>

## User Journey

The steps a person takes, grouped into phases, with who does each step and how it feels. Higher is better.

<Tabs>
  <Tab title="Rendered">
    ```mermaid theme={null}
    %%{init: {"theme":"base","themeVariables":{"fontFamily":"Inter, ui-sans-serif, system-ui, sans-serif","fontSize":"16px","primaryColor":"#FFF1E0","primaryBorderColor":"#A94604","primaryTextColor":"#3B1A02","secondaryColor":"#FFE4C2","tertiaryColor":"#FFF8F0","lineColor":"#A94604","clusterBkg":"#FFF8F0","clusterBorder":"#E8B98A","edgeLabelBackground":"#FFFFFF","git0":"#A94604","git1":"#FF9F0A","git2":"#D97706","git3":"#B45309","gitBranchLabel0":"#FFFFFF","gitBranchLabel1":"#3B1A02","gitBranchLabel2":"#FFFFFF","commitLabelColor":"#3B1A02","commitLabelBackground":"#FFF1E0"}}}%%
    journey
      title Host opens a private room and shares invitations
      section Set up
        Choose 8 to 15 seats: 4: Host
        Create the private room: 4: Host
      section Invite
        Copy a one-time invite: 5: Host
        Share it in the group chat: 4: Host
      section Gather
        Friend joins the lobby: 5: Friend
        Replace a leaked invite: 3: Host
    ```
  </Tab>
</Tabs>

## Sequence Diagram

How the parts of the system talk to each other, in order. This one shows a night action arriving before or after the deadline.

<Tabs>
  <Tab title="Rendered">
    ```mermaid theme={null}
    %%{init: {"theme":"base","themeVariables":{"fontFamily":"Inter, ui-sans-serif, system-ui, sans-serif","fontSize":"16px","primaryColor":"#FFF1E0","primaryBorderColor":"#A94604","primaryTextColor":"#3B1A02","secondaryColor":"#FFE4C2","tertiaryColor":"#FFF8F0","lineColor":"#A94604","clusterBkg":"#FFF8F0","clusterBorder":"#E8B98A","edgeLabelBackground":"#FFFFFF","git0":"#A94604","git1":"#FF9F0A","git2":"#D97706","git3":"#B45309","gitBranchLabel0":"#FFFFFF","gitBranchLabel1":"#3B1A02","gitBranchLabel2":"#FFFFFF","commitLabelColor":"#3B1A02","commitLabelBackground":"#FFF1E0"}}}%%
    sequenceDiagram
      autonumber
      actor P as Player
      participant R as Round service
      participant D as Postgres
      participant C as Phase timer
      actor O as Other players
      P->>R: submit night action
      alt while the phase is open
        R->>R: check the role allows this action
        R->>D: store action, stamped with the phase number
        R-->>P: accepted
      else after the deadline
        R-->>P: rejected, that phase is already resolved
      end
      C->>R: night timer expires
      R->>D: close the phase
      R->>R: resolve this night's actions together
      R->>O: broadcast the outcome to everyone
      R->>P: broadcast the outcome to everyone
      P-->>P: sees what happened at dawn
    ```
  </Tab>

  <Tab title="Source">
    ```text sequence-diagram.mmd theme={null}
    sequenceDiagram
      autonumber
      actor P as Player
      participant R as Round service
      participant D as Postgres
      participant C as Phase timer
      actor O as Other players
      P->>R: submit night action
      alt while the phase is open
        R->>R: check the role allows this action
        R->>D: store action, stamped with the phase number
        R-->>P: accepted
      else after the deadline
        R-->>P: rejected, that phase is already resolved
      end
      C->>R: night timer expires
      R->>D: close the phase
      R->>R: resolve this night's actions together
      R->>O: broadcast the outcome to everyone
      R->>P: broadcast the outcome to everyone
      P-->>P: sees what happened at dawn
    ```
  </Tab>
</Tabs>

## State Machine

Every state a game can be in, and what moves it from one to the next.

<Tabs>
  <Tab title="Rendered">
    ```mermaid theme={null}
    %%{init: {"theme":"base","themeVariables":{"fontFamily":"Inter, ui-sans-serif, system-ui, sans-serif","fontSize":"16px","primaryColor":"#FFF1E0","primaryBorderColor":"#A94604","primaryTextColor":"#3B1A02","secondaryColor":"#FFE4C2","tertiaryColor":"#FFF8F0","lineColor":"#A94604","clusterBkg":"#FFF8F0","clusterBorder":"#E8B98A","edgeLabelBackground":"#FFFFFF","git0":"#A94604","git1":"#FF9F0A","git2":"#D97706","git3":"#B45309","gitBranchLabel0":"#FFFFFF","gitBranchLabel1":"#3B1A02","gitBranchLabel2":"#FFFFFF","commitLabelColor":"#3B1A02","commitLabelBackground":"#FFF1E0"}}}%%
    stateDiagram-v2
      [*] --> Lobby
      Lobby --> Dealing: every seat filled
      Dealing --> Night: roles dealt
      Night --> Dawn: night timer expires
      Dawn --> Day: resolve night actions
      Day --> Voting: discussion timer expires
      Voting --> Tally: votes tallied
      Tally --> Day: tied
      Tally --> Resolution: elimination confirmed
      Resolution --> Night: no winner yet
      Resolution --> MafiaWin: mafia win
      Resolution --> TownWin: town wins
      MafiaWin --> [*]
      TownWin --> [*]
    ```
  </Tab>

  <Tab title="Source">
    ```text state-machine.mmd theme={null}
    stateDiagram-v2
      [*] --> Lobby
      Lobby --> Dealing: every seat filled
      Dealing --> Night: roles dealt
      Night --> Dawn: night timer expires
      Dawn --> Day: resolve night actions
      Day --> Voting: discussion timer expires
      Voting --> Tally: votes tallied
      Tally --> Day: tied
      Tally --> Resolution: elimination confirmed
      Resolution --> Night: no winner yet
      Resolution --> MafiaWin: mafia win
      Resolution --> TownWin: town wins
      MafiaWin --> [*]
      TownWin --> [*]
    ```
  </Tab>
</Tabs>

## Data Model

The tables, their columns, and the rules the database enforces. Kana writes it in DBML. Notice the unique indexes: one action and one vote per seat per phase is a database rule, not application code.

<Tabs>
  <Tab title="Rendered">
    ```mermaid theme={null}
    %%{init: {"theme":"base","themeVariables":{"fontFamily":"Inter, ui-sans-serif, system-ui, sans-serif","fontSize":"16px","primaryColor":"#FFF1E0","primaryBorderColor":"#A94604","primaryTextColor":"#3B1A02","secondaryColor":"#FFE4C2","tertiaryColor":"#FFF8F0","lineColor":"#A94604","clusterBkg":"#FFF8F0","clusterBorder":"#E8B98A","edgeLabelBackground":"#FFFFFF","git0":"#A94604","git1":"#FF9F0A","git2":"#D97706","git3":"#B45309","gitBranchLabel0":"#FFFFFF","gitBranchLabel1":"#3B1A02","gitBranchLabel2":"#FFFFFF","commitLabelColor":"#3B1A02","commitLabelBackground":"#FFF1E0"}}}%%
    erDiagram
      games ||--o{ seats : has
      seats ||--|| role_assignments : "dealt"
      games ||--o{ night_actions : records
      games ||--o{ votes : records
      seats ||--o{ night_actions : "acts in"
      seats ||--o{ votes : casts
      games {
        uuid id PK
        varchar join_code UK
        varchar phase
        int phase_number
        timestamptz phase_ends_at
      }
      seats {
        uuid id PK
        uuid game_id FK
        varchar display_name
        varchar seat_token UK
        boolean alive
      }
      role_assignments {
        uuid seat_id PK
        varchar role
      }
      night_actions {
        uuid id PK
        uuid game_id FK
        int phase_number
        uuid actor_seat_id FK
        uuid target_seat_id FK
      }
      votes {
        uuid game_id FK
        int phase_number
        uuid voter_seat_id FK
        uuid target_seat_id FK
      }
    ```
  </Tab>

  <Tab title="Source">
    ```text data-model.dbml theme={null}
    Table games {
      id uuid [pk]
      join_code varchar [not null, unique]
      phase varchar [not null]
      phase_number int [not null, default: 0]
      phase_ends_at timestamptz
      note: 'phase_ends_at is the only clock. A client never decides a phase is over.'
    }

    Table seats {
      id uuid [pk]
      game_id uuid [not null, ref: > games.id]
      display_name varchar [not null]
      seat_token varchar [not null, unique]
      alive boolean [not null, default: true]

      indexes {
        (game_id, display_name) [unique, name: 'one_seat_per_name_per_game']
      }
      note: 'seat_token is what a player proves a seat with. The name is public; the token is not.'
    }

    Table role_assignments {
      seat_id uuid [pk, ref: > seats.id]
      role varchar [not null]
      note: 'Read only by the server, and only ever served to the seat that owns it.'
    }

    Table night_actions {
      id uuid [pk]
      game_id uuid [not null, ref: > games.id]
      phase_number int [not null]
      actor_seat_id uuid [not null, ref: > seats.id]
      target_seat_id uuid [ref: > seats.id]

      indexes {
        (game_id, phase_number, actor_seat_id) [unique, name: 'one_action_per_seat_per_phase']
      }
      note: 'Stamped with the phase it was submitted in, so a late arrival is rejected rather than applied to the next night.'
    }

    Table votes {
      game_id uuid [not null, ref: > games.id]
      phase_number int [not null]
      voter_seat_id uuid [not null, ref: > seats.id]
      target_seat_id uuid [not null, ref: > seats.id]

      indexes {
        (game_id, phase_number, voter_seat_id) [unique, name: 'one_vote_per_seat_per_phase']
      }
    }
    ```
  </Tab>
</Tabs>

## Domain Model

The business concepts, which ones form a consistent unit (an **aggregate**), and the rules they keep. Kana writes it in D2, grouped into core, supporting, and generic tiers. `@change` marks what this outcome adds or modifies.

<Tabs>
  <Tab title="In Kanaeru">
    <Frame caption="Reviewing a domain model change by change: Room is new, with three attributes. Approve or Flag each one.">
      <img src="https://mintcdn.com/kanaerulabs/_FsoVBjQWp9cMeEf/images/nadia/review-domain-progressive.jpg?fit=max&auto=format&n=_FsoVBjQWp9cMeEf&q=85&s=2a069d449874daf85291b1d3712717a3" alt="Domain model canvas with Session, Room, and Invite entities marked ADDED and a review panel" width="1600" height="900" data-path="images/nadia/review-domain-progressive.jpg" />
    </Frame>
  </Tab>

  <Tab title="Source">
    ```text domain-model.d2 theme={null}
    # ──── Core Tier ────

    core: {
      round: {
        Round: {
          shape: class
          \# Aggregate Root
          gameId: uuid
          phase: Phase
          phaseNumber: int {constraint: monotonic}
          phaseEndsAt: timestamptz

          open()
          close()
          resolve()
        }

        # @change: added
        NightAction: {
          shape: class
          actorSeatId: uuid {constraint: unique with phaseNumber}
          targetSeatId: uuid
          phaseNumber: int
        }

        Vote: {
          shape: class
          voterSeatId: uuid {constraint: unique with phaseNumber}
          targetSeatId: uuid
          phaseNumber: int
        }

        Round -> NightAction: holds
        Round -> Vote: holds
      }

      roster: {
        Game: {
          shape: class
          \# Aggregate Root
          id: uuid
          joinCode: string {constraint: unique}
        }

        Seat: {
          shape: class
          id: uuid
          displayName: string
          seatToken: string {constraint: secret}
          alive: boolean
        }

        RoleAssignment: {
          shape: class
          seatId: uuid
          role: Role {constraint: server-only}
        }

        Game -> Seat: holds
        Game -> RoleAssignment: deals
      }
    }

    # ──── Supporting Tier ────

    supporting: {
      presence: {
        Connection: {
          shape: class
          seatId: uuid
          lastSeenAt: timestamptz
        }
      }
    }

    # ──── Generic Tier ────

    generic: {
      delivery: {
        # @change: modified
        PhaseBroadcast: {
          shape: class
          phase: Phase
          sentAt: timestamptz
        }
      }
    }

    # ──── Cross-Context Relationships ────

    core.roster.Seat -> core.round.NightAction: targets
    core.round.Round -> core.roster.Game: advances
    supporting.presence.Connection -> core.roster.Seat: occupies
    core.round.Round -> generic.delivery.PhaseBroadcast: broadcasts
    ```
  </Tab>
</Tabs>

## Architecture

The components, what each one is responsible for, and how data flows between them. Kana writes it in LikeC4, which has component, deployment, integration, and data views.

<Tabs>
  <Tab title="Rendered">
    ```mermaid theme={null}
    %%{init: {"theme":"base","themeVariables":{"fontFamily":"Inter, ui-sans-serif, system-ui, sans-serif","fontSize":"16px","primaryColor":"#FFF1E0","primaryBorderColor":"#A94604","primaryTextColor":"#3B1A02","secondaryColor":"#FFE4C2","tertiaryColor":"#FFF8F0","lineColor":"#A94604","clusterBkg":"#FFF8F0","clusterBorder":"#E8B98A","edgeLabelBackground":"#FFFFFF","git0":"#A94604","git1":"#FF9F0A","git2":"#D97706","git3":"#B45309","gitBranchLabel0":"#FFFFFF","gitBranchLabel1":"#3B1A02","gitBranchLabel2":"#FFFFFF","commitLabelColor":"#3B1A02","commitLabelBackground":"#FFF1E0"}}}%%
    flowchart TD
      player([Player])
      subgraph mafia[Mafia game]
        web[Game screen<br/>Next.js, mobile browser]
        round[Round service<br/>owns the phase clock]
        timer[Phase timer]
        db[(Postgres)]
      end
      player -->|plays on| web
      web -->|actions and votes| round
      round -->|stores, stamped with the phase| db
      timer -->|expires the phase| round
      round -->|phase changes, no roles| web
    ```
  </Tab>

  <Tab title="Source">
    ```text architecture.c4 theme={null}
    specification {
      element actor
      element system
      element container
      element database
    }

    model {
      player = actor "Player"
      mafia = system "Mafia game" {
        web = container "Game screen" "Next.js, in a mobile browser"
        round = container "Round service" "Owns the phase clock and resolves actions"
        timer = container "Phase timer" "Closes each phase on time"
        db = database "Postgres" "Games, seats, roles, actions, votes"
      }
      player -> mafia.web "plays on"
      mafia.web -> mafia.round "submits actions and votes"
      mafia.round -> mafia.db "stores, stamped with the phase"
      mafia.timer -> mafia.round "expires the phase"
      mafia.round -> mafia.web "broadcasts phase changes (no roles)"
    }

    views {
      view rounds { include * }
    }
    ```
  </Tab>
</Tabs>

## API Contract

The endpoints the outcome adds or changes, as an OpenAPI document. The smallest surface a round needs. Note what is absent: there is no endpoint that returns anyone else's role.

<Tabs>
  <Tab title="Rendered">
    | Method | Path | Does | Responses |
    | - | - | - | - |
    | `GET` | `/games/{id}/state` | Current state, as this seat sees it | `200` phase, time remaining, who is alive, and this seat's own role — no other seat's |
    | `POST` | `/games/{id}/actions` | Submit a night action | `202` accepted, stamped with the current phase number, `403` this seat's role does not permit that action, `409` that phase is already resolved |
    | `POST` | `/games/{id}/votes` | Cast a vote | `202` accepted, `409` this seat has already voted in this phase |
    | `GET` | `/games/{id}/events` | Phase change stream | `200` server-sent phase changes, carrying no role information |
  </Tab>

  <Tab title="Source">
    ```json api-contract.json theme={null}
    {
      "openapi": "3.0.3",
      "info": {
        "title": "Mafia round API",
        "version": "0.1.0",
        "description": "The smallest surface a round needs. Note what is absent: there is no endpoint that returns anyone else's role."
      },
      "paths": {
        "/games/{id}/state": {
          "get": {
            "summary": "Current state, as this seat sees it",
            "parameters": [
              {
                "name": "id",
                "in": "path",
                "required": true,
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            ],
            "responses": {
              "200": {
                "description": "Phase, time remaining, who is alive, and this seat's own role — no other seat's"
              }
            }
          }
        },
        "/games/{id}/actions": {
          "post": {
            "summary": "Submit a night action",
            "responses": {
              "202": {
                "description": "Accepted, stamped with the current phase number"
              },
              "403": {
                "description": "This seat's role does not permit that action"
              },
              "409": {
                "description": "That phase is already resolved"
              }
            }
          }
        },
        "/games/{id}/votes": {
          "post": {
            "summary": "Cast a vote",
            "responses": {
              "202": {
                "description": "Accepted"
              },
              "409": {
                "description": "This seat has already voted in this phase"
              }
            }
          }
        },
        "/games/{id}/events": {
          "get": {
            "summary": "Phase change stream",
            "responses": {
              "200": {
                "description": "Server-sent phase changes, carrying no role information"
              }
            }
          }
        }
      }
    }
    ```
  </Tab>
</Tabs>

## Tech Stack

Each decision names the choice, why it was made, what else was considered, and the tradeoff. In Kanaeru, each card shows the technology's logo.

<Tabs>
  <Tab title="Rendered">
    <CardGroup cols={2}>
      <Card title="Language: TypeScript" icon="code">
        One set of phase and role types shared by the server and the screen, so one side cannot drift from the other. **Instead of:** Python, Go.
      </Card>

      <Card title="Framework: Next.js" icon="layer-group">
        The join screen and the round screen come off one runtime, and server-sent events need no extra service. **Instead of:** Remix, SvelteKit. **Tradeoff:** Past a few hundred concurrent players this needs a dedicated socket layer.
      </Card>

      <Card title="Database: PostgreSQL (changed)" icon="database">
        One action per seat per phase is a unique index, not application code. **Instead of:** MySQL, DynamoDB.
      </Card>

      <Card title="Hosting: Vercel" icon="cloud">
        Small deploy unit, and straightforward delivery for a link people open on a phone. **Instead of:** Fly.io, Render.
      </Card>

      <Card title="Auth: Seat tokens, no accounts (new)" icon="key">
        A display name is visible to everyone on the call, so it cannot prove a seat. A one-time token issued at join can. **Instead of:** Auth0, Clerk.
      </Card>

      <Card title="Monitoring: Structured logs + error tracking" icon="chart-line">
        Inherited from the workspace default rather than chosen again for this outcome.
      </Card>
    </CardGroup>
  </Tab>

  <Tab title="Source">
    ```json tech-stack.json theme={null}
    {
      "schemaVersion": 1,
      "decisions": [
        {
          "category": "language",
          "choice": "TypeScript",
          "icon": "typescript",
          "rationale": "One set of phase and role types shared by the server and the screen, so one side cannot drift from the other.",
          "alternatives": [
            "Python",
            "Go"
          ]
        },
        {
          "category": "framework",
          "choice": "Next.js",
          "icon": "nextdotjs",
          "rationale": "The join screen and the round screen come off one runtime, and server-sent events need no extra service.",
          "alternatives": [
            "Remix",
            "SvelteKit"
          ],
          "tradeoffs": "Past a few hundred concurrent players this needs a dedicated socket layer."
        },
        {
          "category": "database",
          "choice": "PostgreSQL",
          "icon": "postgresql",
          "rationale": "One action per seat per phase is a unique index, not application code.",
          "alternatives": [
            "MySQL",
            "DynamoDB"
          ],
          "changeType": "modified"
        },
        {
          "category": "hosting",
          "choice": "Vercel",
          "icon": "vercel",
          "rationale": "Small deploy unit, and straightforward delivery for a link people open on a phone.",
          "alternatives": [
            "Fly.io",
            "Render"
          ]
        },
        {
          "category": "auth",
          "choice": "Seat tokens, no accounts",
          "rationale": "A display name is visible to everyone on the call, so it cannot prove a seat. A one-time token issued at join can.",
          "alternatives": [
            "Auth0",
            "Clerk"
          ],
          "changeType": "added"
        },
        {
          "category": "monitoring",
          "choice": "Structured logs + error tracking",
          "rationale": "Inherited from the workspace default rather than chosen again for this outcome."
        }
      ]
    }
    ```
  </Tab>
</Tabs>

## Quality Profile

The quality attributes the work must meet: where the product is now, and the target. Kanaeru draws it as a radar chart.

<Tabs>
  <Tab title="Rendered">
    | Dimension | Now | Target | Means |
    | - | - | - | - |
    | Role secrecy | 100 | 100 | A seat's own session never receives another seat's role. Non-negotiable. |
    | Phase agreement | 74 | 95 | Every player sees the same phase at the same moment. |
    | Action delivery | 91 | 99 | An action submitted before the deadline is always counted. |
    | Reconnect recovery | not measured | 80 | A returning player lands on the right phase with their own role. Not yet measured — separate outcome. |
    | Mobile usability | 83 | 90 | Usable one-handed while chatting in the room. |
  </Tab>

  <Tab title="Source">
    ```json quality-profile.json theme={null}
    {
      "dimensions": [
        {
          "name": "Role secrecy",
          "target": 100,
          "current": 100,
          "weight": 1,
          "description": "A seat's own session never receives another seat's role. Non-negotiable.",
          "slaTarget": "zero leaked roles",
          "linkedRequirements": [
            "J2-S1",
            "J2-S2"
          ]
        },
        {
          "name": "Phase agreement",
          "target": 95,
          "current": 74,
          "weight": 1,
          "description": "Every player sees the same phase at the same moment.",
          "slaTarget": "within 2s across clients",
          "changeType": "modified"
        },
        {
          "name": "Action delivery",
          "target": 99,
          "current": 91,
          "weight": 0.9,
          "description": "An action submitted before the deadline is always counted.",
          "slaTarget": "zero dropped actions"
        },
        {
          "name": "Reconnect recovery",
          "target": 80,
          "weight": 0.6,
          "description": "A returning player lands on the right phase with their own role. Not yet measured — separate outcome.",
          "changeType": "added"
        },
        {
          "name": "Mobile usability",
          "target": 90,
          "current": 83,
          "weight": 0.8,
          "description": "Usable one-handed while chatting in the room.",
          "slaTarget": "one tap for every round action"
        }
      ]
    }
    ```
  </Tab>
</Tabs>


## Related topics

- [Blueprint editor and tools](/guides/blueprint-editor.md)
- [Blueprints and understanding gaps](/concepts/blueprints.md)
- [Quickstart](/quickstart.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.