Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.7331.org/llms.txt

Use this file to discover all available pages before exploring further.

Portal chats come in four types. Each type has distinct visibility, access, and moderation semantics.

OFFICIAL

Admin-created chats that appear in the public directory and are visible to everyone — signed-in or not.
  • Owner: an admin user (appears in moderation checks)
  • Visibility: listed in GET /v1/portal/directory
  • Join: anyone can browse; signed-in users can chat
  • Moderation: owner or anyone with can_act_on(actor, owner)
Used for the curated chats the operators run themselves.

PUBLIC

User-created chats with a vanity slug. Premium-only to create.
  • Owner: the creator (any PREMIUM/LIFETIME user)
  • Invariant: slug IS NOT NULL
  • Visibility: listed in GET /v1/portal/directory
  • Join: anyone with the link; signed-in to chat
  • Moderation: owner or can_act_on(actor, owner) — chat type doesn’t restrict admin moderation
Moving a chat from PUBLIC to PRIVATE clears the slug (the invariant enforces it).

PRIVATE

Invite-only chats — not listed anywhere.
  • Owner: the creator (PREMIUM/LIFETIME)
  • Access: owner + users who used a live invite code
  • Join: POST /v1/portal/resolve/{code} to join via an invite code
  • Moderation: owner only for config/invites/clipboard; owner OR ADMIN+ for delete/mute/kick (ADMIN override exists because we still need to reach behind the curtain for abuse reports)

DIRECT

Ephemeral 1-on-1 chats created by the matcher. Users don’t create these directly.
  • Owner: NULL (the match system “owns” them)
  • Members: exactly two, pinned at creation
  • Lifecycle:
    1. Matcher pairs two queued users → creates direct chat → both socket-joined
    2. Either user can call POST /v1/portal/match/leave or disconnect
    3. Explicit leave → peer gets room:peer_left with grace_until_ms=0, chat destroyed immediately
    4. Disconnect → 60s grace window for reconnect; chat destroyed on timeout
  • Moderation: no per-chat mutes; global hard-mute still applies
  • Encryption: same epoch rotation — a leave destroys the chat and purges keys
See the matchmaking concept for the queue side.

Shared fields

Every chat has:
  • id — integer PK, used in all API paths
  • name — display name (not unique)
  • chat_type — the four values above
  • slug — nullable; required for PUBLIC
  • browser_config — JSONB; BrowserConfig (browser type, start URL, kiosk, adblock)
  • chat_config — JSONB; RoomConfig (wipe preset, history visibility, control locks, …)

Chat vs browser separation

Chat and the neko browser are independent:
  • Chat viewer membership lives in ConnectionTracker; you can join chat before the VM starts
  • VM membership lives in neko; controlled by room:start / room:stop
  • room:stop does NOT evict chat viewers — users keep chatting across VM restarts
  • Wipes rotate the epoch but don’t touch membership
This split is why the quickstart says “open a chat” without mentioning the VM — starting the browser is a later, optional step.