Emote Directory
Overview
The Emote Directory is a public, unauthenticated, edge-cacheable browse of
Lumio's durable cross-platform emote catalog (chat_emotes). It is the read
surface behind the public Stats app's emote-verzeichnis — the founder-scoped
decision on ZAF-583 was that "emote verzeichnis kann öffentlich sein und braucht
kein auth", so this ships as an open read model with no login and no RBAC.
It exposes the same catalog that powers chat rendering and the stream-report image fallback, but as a browseable list rather than a per-channel emote map. The catalog is deliberately tenant-free — every row is public reference data (the same CDN URL every viewer's browser already loads), so there is no account boundary to enforce on the read (see the Emote Catalog developer guide and its migration header).
This is a read-only surface. Ingest and upserts into chat_emotes are owned
by the emote-fetch / chat pipeline; nothing here writes.
Protocols
Per the three-protocol rule, the directory ships in GraphQL (primary) + REST parity — identical fields, filters, pagination and error messages on both:
| Protocol | Surface |
|---|---|
| GraphQL | publicEmotes(platform, provider, channelId, animated, search, page, limit): PublicEmotePage! |
| REST | GET /v1/public-stats/emotes?platform=&provider=&channel_id=&animated=&search=&page=&limit= |
WebSocket is deliberately omitted. A static browse / search directory has no
live-push consumer — there is nothing to stream — so no WS channel is added and
no channel_gate_for / channel_feature_for entry is created. AGENTS.md permits
this justified single-protocol omission, and it is documented at both the GraphQL
resolver and the REST handler call sites.
Filtering, search & pagination
All filters are optional and compose with AND:
| Filter | Matches |
|---|---|
platform | Exact platform the emote is usable on: twitch / youtube / kick / trovo |
provider | Exact host: the platforms above plus 7tv / bttv / ffz |
channel_id (channelId) | Exact platform channel id; omit to include global (platform-wide) sets |
animated | The animated flag (true / false) |
search | Case-insensitive substring match on the emote name. Wildcard characters (%, _) are matched literally, not as SQL wildcards. |
Pagination is page-based: page is 1-based (default 1) and limit is
clamped to 1..=100 (default 50). The response carries total and
total_pages. Results are ordered by name then id, giving a stable
alphabetical directory that paginates deterministically.
Each emote carries: id, platform, provider, channel_id (null for a
global set), name, url, animated, owner_id, source (api_fetch |
observed), first_seen_at, last_seen_at. The internal provider_emote_id is
not exposed.
Not modelled (yet): sorting by usage frequency.
chat_emotescarries no usage counter today (open in ZAF-581), so the directory cannot rank by popularity. That is a possible later addition, not part of this surface.
Public-origin hardening
Because the origin is public and unauthenticated, two protections apply:
- Rate-limiting. Every request is throttled by the anonymous-tier, per-IP
rate limiter that wraps every
/v1request — no special-casing needed. - Edge caching. The REST
200setsCache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=600. The catalog changes on the order of minutes, so edge caching absorbs hotlink / abuse load. The GraphQL surface is aPOST(not edge-cacheable) and is cached at the SSR layer of the public Stats app instead.
Operator kill-switch
The whole public origin is gated by the account-less system:public_stats
feature flag — a system-category master kill-switch, not a per-plan
entitlement. It defaults on (public reads open); flipping it off closes the
directory (and the sibling public channel/stream read model) without a deploy.
For an anonymous caller the flag resolves to its global default, so no account
context is required. When it is off, GraphQL returns
Feature 'system:public_stats' is not available and REST returns 403 FEATURE_DISABLED with the same message.
The directory page (Stats app)
The browseable UI lives on the public Stats app (stats.lumio.vision) at
/emotes, reachable from the left sidebar (Emotes). It is placed on the
cookieless public Stats origin — rather than the logged-in dashboard — because it
is public, needs no auth, and is aggressively edge-cached: no session cookie
leaks and the HTML is identical for every visitor.
The page is server-rendered from the GraphQL read model (publicEmotes) via
the app's public data path — the browser never calls GraphQL directly, and no
session is attached. All filter, search and page state lives in the URL query
(?platform=, ?provider=, ?channel=, ?animated=, ?search=, ?page=), so
every distinct view is its own shareable, edge-cacheable URL and back/forward
navigation just works.
Controls:
- Name search — a debounced case-insensitive substring match on the emote name.
- Platform / Provider / Type (animated / static) dropdowns, and a Channel ID field. Changing any control resets to page 1.
- Pagination —
Previous/Nextlinks carry every active filter forward and show the current page, total pages and the total match count.
Each emote tile shows the emote image (hotlinked from the provider CDN, on a theme-aware checkerboard so transparent emotes stay legible), the emote name, an animated marker on GIF emotes, and provider / owner attribution — the provider (Twitch / YouTube / Kick / Trovo / 7TV / BetterTTV / FrankerFaceZ) with its icon, plus the owning channel id (or Global set for a platform-wide set).
The page is fully bilingual (EN/DE) with the standard Lumio language switcher and supports light and dark themes, consistent with the rest of the Stats app. When the operator kill-switch is off (or the backend is unreachable) the page renders a calm "directory unavailable" state rather than an error.
Hotlinking note. Emote images are hotlinked directly from the provider CDNs (7TV / BetterTTV / FrankerFaceZ / platform CDNs) — the same substance as the in-chat emote resolution the estate already performs. The founder accepted this public exhibition on ZAF-583; provider attribution is always shown.
Related
- Emotes — the per-channel emote fetching / rendering module.
- Emote Catalog — the durable
chat_emotesstore and its read-through cache. - Public Stats — the per-channel publicness (opt-out) layer of the same public app.
- API reference: GraphQL · REST.