Skip to main content

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:

ProtocolSurface
GraphQLpublicEmotes(platform, provider, channelId, animated, search, page, limit): PublicEmotePage!
RESTGET /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:

FilterMatches
platformExact platform the emote is usable on: twitch / youtube / kick / trovo
providerExact host: the platforms above plus 7tv / bttv / ffz
channel_id (channelId)Exact platform channel id; omit to include global (platform-wide) sets
animatedThe animated flag (true / false)
searchCase-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_emotes carries 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 /v1 request — no special-casing needed.
  • Edge caching. The REST 200 sets Cache-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 a POST (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.
  • PaginationPrevious / Next links 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.

  • Emotes — the per-channel emote fetching / rendering module.
  • Emote Catalog — the durable chat_emotes store and its read-through cache.
  • Public Stats — the per-channel publicness (opt-out) layer of the same public app.
  • API reference: GraphQL · REST.