Files
t5de-hub/docs/design.md
T
dhkatzandClaude Opus 5.5 0c67232425
CI / test (push) Successful in 49s
Badges: the proxy asks which IMVU accounts show the T5 badge
POST /api/v1/badges {imvuIds} (at most 200) answers the linked accounts whose
badge is on, of people who aren't banned, in one query. It uses the existing
per-account showBadge (off by default, set from the account page).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 02:38:01 -07:00

436 lines
29 KiB
Markdown

# T5DE community design
Status: draft for review (2026-10-29). Decisions marked **Decided** were agreed in discussion; the rest are
proposals. Open questions are at the end.
## Goals
- **Community products**: original work that IMVU's shop doesn't allow, hosted by community-run catalogs, worn and
placed in T5DE without IMVU ever seeing it.
- **One account per person** on a central hub, linked to any number of IMVU accounts.
- **T5DE-hosted rooms**: decorated scenes and their chat, hosted by the hub.
- **Social extras**: an opt-in T5DE badge, opt-in T5DE presence, creator pages.
- **Moderation from one place**: the hub's website.
- **Performance** on every backend.
- **Free first**, with payments possible later without reworking the data.
Non-goals: re-hosting IMVU's products (original work only), changing what IMVU or non-T5DE users see (they see
IMVU's defaults), and replacing IMVU's servers for anything else (for now).
## Overview
```
┌──────────── user's PC ─────────────┐
│ T5DE client ── patch ── proxy ─────┼──► IMVU (unchanged; never sees community items)
└────────────────────────────┬───┬───┘
│ └──► catalogs (many, community-run) ──► S3/R2 storage
▼
┌──────────── hub (T5DE-run) ─────────────┐
│ API · realtime gateway · rooms · jobs │──► IMVU public API (linking, age checks)
│ website & console · Postgres · NATS · R2│
└─────────────────────────────────────────┘
```
Repositories (**Decided**):
| Repository | What |
| --- | --- |
| `t5de` | Client patches; gains removing community items from what IMVU sees, and the new settings. |
| `t5de-proxy` | The local proxy; gains the product resolver, IMQ router, hub and catalog clients. |
| `t5de-hub` | The hub: API, realtime gateway, rooms, jobs, website and moderation console (one Go program). |
| `t5de-catalog` | The reference catalog: API and a small admin page. Anyone can run one. |
| `t5de-api` | Shared definitions: OpenAPI specs, WebSocket messages, token formats, and Go types generated from them. |
## What the client already does (found 2026-10-29)
From an experiment with the proxy presenting a made-up product ID as an alias of a real product (t5de-proxy's
local `spike-alias` branch):
- **Products load from whatever the answers say.** Before using a product the client calls `authorizeProducts2`
(XML-RPC: may this user use it, where are its files, their checksum) and product-info endpoints, then downloads
the files from the addresses given. When the proxy answered for an ID IMVU doesn't know, the client loaded and
rendered the product.
- **IMVU signs requests.** `X-imvu-auth` is md5(user ID + session security key + body), with the query string
added for some calls. A changed request is rejected ("invalid authorization data"). The proxy sees the key in the
login response and could re-sign, but the design avoids sending IMVU anything changed on community products'
behalf: the proxy answers those requests itself, and splits requests that mix both.
- **Outfit changes are chat commands.** Wearing or removing sends plain-text `*use <ids>` / `*remove <id>`
messages over IMQ (the home screen is a chat session too), and IMVU relays them to everyone in the session.
- **IMVU didn't keep an unknown ID** in the account's saved outfit after it was worn.
- **The proxy holds the IMVU session key.** It never leaves the user's machine: not to the hub, not to catalogs.
IMVU's public user API (`api.imvu.com/user/user-<id>`) has `is_ageverified`, `is_ap`, `is_adult` (self-reported
age, not verified) and `tagline`.
## Client side
### Patch (`t5de`)
- Removes community product IDs from outgoing `*use` / `*remove` and outfit saves, so IMVU and non-T5DE users see
defaults. Tells the proxy the full outfit through a local endpoint only the proxy answers.
- Settings: T5DE badge, T5DE presence, adult content (each opt-in), hub sign-in.
### Proxy (`t5de-proxy`)
- **IMVU passthrough**: as today.
- **Product resolver**: community products are named (global product ID, revision) everywhere outside the machine.
The client only understands 32-bit integers (XML-RPC), so the proxy maps each to a local ID in a reserved range
near the top (about 1.9-2.1 billion), stored per install. It answers the client's permission, product-info and
file requests for them, and splits mixed requests (the IMVU part forwarded unchanged).
- **File cache**: files named by sha256, kept on disk; worn items keep working when a catalog is down.
- **IMQ router**: chat for T5DE-hosted rooms goes to the hub, the rest to IMVU. Adds other T5DE users' community
items to incoming `*use` messages, so the client's own code loads them.
- **Hub client**: one WebSocket for presence, room channels and hosted rooms; device-code sign-in, refresh token
encrypted for the Windows user (DPAPI).
- **Local pages**: browsing and account pages shown in the client's built-in browser.
- **Creator live preview**: loads a product from a local folder and reloads it as files change.
## Identity (**Decided**)
- A hub **account is a person**. It links to any number of **IMVU accounts**, up to a cap stored per account
(default 10), which moderators can raise on request.
- **Linking**: the hub gives a one-time code, the user puts it in their IMVU tagline, the hub reads it from IMVU's
public API. The user can remove it afterwards. **The most recent proof wins**: linking an account already linked
elsewhere moves it (accounts get sold or given away), and the old link ends.
- **Sign-in**: device-code flow (approve in a browser). Tokens are short-lived Ed25519 JWTs with key IDs; keys
rotate and are published as a JWKS.
- **Alts stay private**: tokens for catalogs and rooms carry an ID per linked IMVU account (pairwise), never the
person's ID, so other people and catalogs can't connect a person's accounts. Only the person and moderators can.
- **Creators** have a public creator profile, separate from (and not revealing) their IMVU accounts.
- **Switching accounts in the client**: the proxy sees which IMVU account logs in and tells the hub. Not linked:
offer to link. Linked to another hub account: no community features that session.
- **Bans** apply to the person, so all linked accounts.
## Age (**Decided**: free, several providers, vendor-neutral)
No payment for verification (adult content rules out mainstream merchants of record; see the discussion of
2026-10-29). Several ways to prove age, tried in order of privacy, behind one interface so any can be swapped:
1. **IMVU** (live): the IMVU account is age verified (`is_ageverified`) or has Access Pass (`is_ap`), read by the hub
from IMVU's public API. Scope: that IMVU account. Re-checked about daily; a revocation shows at the next check.
Self-reported `is_adult` doesn't count.
2. **Phone wallets** (planned): a government ID in Google Wallet, Apple Wallet or an EU age-verification wallet,
asked for "age over 18" only, through the browser's Digital Credentials API. Free for the hub; no documents or
birth date reach it. Scope: the person.
3. **ID document and selfie** (planned): Didit's hosted check, within its free monthly allowance. Scope: the person
(the liveness step stops someone using another person's ID).
### How it's built
- **One table of results**, `age_check`: subject (a person, or one of their linked IMVU accounts), provider,
method (e.g. `imvu_age_verified`, `imvu_access_pass`, `wallet_mdoc`, `id_document`), status (`pending`,
`complete`, `failed`, `revoked`), over 18 yes/no, the provider's reference, when it was checked and when it
expires. Never a birth date, document, photo or name: providers that see those keep them.
- **One rule**: an IMVU account counts as adult if its person has a current complete check that says over 18, or
the account itself has one. Everything (website, tokens for catalogs, room channels) asks this one function.
- **Providers** implement a small interface: `start` (begin a check: done at once, a page to send the person to,
or a request for the browser to hand to a wallet), `complete` (finish from the provider's callback or signed
webhook), and optionally `recheck` (for providers whose answer can change, like IMVU). Which ones run is
configuration (`AGE_PROVIDERS`), with each provider's keys in its own settings. Adding or replacing a vendor is
one new adapter; nothing else changes.
- Webhooks arrive at `/api/v1/age/webhook/<provider>` and each provider checks its own signature.
- Enforced at three points: catalogs (adult files need an adult token), the hub (adult items left out of room
channels for viewers who aren't adult and opted in), and the viewer's proxy.
### To add the planned providers
- Didit: an account, an API key and webhook secret, and a workflow set to ID document plus liveness, returning
only "over 18". Confirm its free monthly allowance covers age checks.
- Wallets: Google's Digital Credentials verifier registration (free), and an `age_over_18` request (OpenID4VP,
mdoc); Apple and the EU wallet through the same browser API where supported.
## Products and catalogs
- **The hub is the registry.** Products have **global IDs** owned by their creator. A **revision** is a file list
(path, sha256, size), signed by the hub on the creator's behalf (creator-held keys later). Revisions never change.
- **Catalogs host and curate**: their own rules, ratings ceiling and community. A catalog stores files and tells the
hub which revisions it hosts. The same revision on several catalogs is mirrored, not duplicated: uploading a file
list identical to an existing revision links to it. Variants link with **derived from** (credit, and the original
creator's derive permission).
- **Outfits and rooms refer to products and revisions** (see Revisions), so any catalog hosting a revision can serve
it.
- **Files** are named by hash, immutable, served from object storage or a CDN; never through Go.
- **Ratings**: general and adult, as IMVU. Catalogs declare the highest rating they host.
- **Uploads** go from the creator's browser (or the proxy) straight to the catalog's API with a hub token, so large
files never pass through the hub. The catalog validates them: formats parsed, size limits, nothing executable,
thumbnails generated, extra texture maps accepted (see Better looks).
- **Format**: IMVU's own product files, as the client loads them, so creators keep using IMVU's tools.
### Revisions (**Decided**: public history)
A **revision** is the product itself: its files. IMVU has revisions too (its file addresses carry a revision
number, `productdata/<id>/<revision>/`) but hardly shows them; T5DE shows them openly.
- **Numbered** 1, 2, 3... per product. Uploading changed files makes a new revision; editing info doesn't.
- **History on every product's page**, for everyone: each revision's number and date, the creator's notes (what
changed, required when publishing), a preview image, the files added, removed or changed (with sizes), its
rating, and which catalogs host it. Any two revisions can be compared.
- **Which revision people get** (**Decided**): always the **latest**, everywhere, by default. A person can opt in,
per product, to **stay on** a revision they prefer: a setting on the product's page, kept per person (all their
IMVU accounts), and only for what they wear. Rooms always use the latest revision of their furniture. Outfits and
scenes store just the product; the proxy picks the revision when loading (the pin if there is one, else the
latest), and what a wearer broadcasts in a room is always a concrete revision, so everyone sees the same one.
- **Old revisions stay available**, so outfits and rooms keep working. A creator can **retract** a broken
revision: it's marked retracted in the history, and anyone following or staying on it moves to the nearest
revision that isn't. Only moderation removes a revision outright (shown as "removed by moderation", with the
files gone).
- **Derived from** points to a specific revision of the original.
- Moderators see the same history, plus who uploaded each revision and from where.
### Product info (**Decided**: editable)
- A product's **files** come in revisions that never change; its **info** belongs to the product and its creator
can edit it at any time on the hub's website: name, description, icon, tags, category. Stored at the hub, so an
edit shows everywhere (every catalog, every client) without re-uploading anything.
- **Icon**: an image uploaded to the hub, re-encoded there (decoded and saved again as PNG or WebP at fixed sizes,
which drops anything hidden in the file) and served from R2.
- **Description**: HTML, as on IMVU, made safe by an **allowlist** (in Go, bluemonday or equivalent), never a
blocklist:
- Tags: text formatting (`b`, `i`, `u`, `s`, `em`, `strong`, `small`, `big`, `sub`, `sup`, `font` with `color`
and `size`, `span`, `div`, `p`, `br`, `hr`, `center`), lists, simple tables, headings, `a`, `img`.
- Styles: only a short list of properties (color, background color, font size, weight and style, text
alignment and decoration) with checked values; no `url()`, no positioning, nothing that can cover the page.
- Links: `http`/`https` only, with `rel="nofollow ugc noopener noreferrer"`, opening outside the page.
- Images: `https` only, fetched through the hub's image proxy (re-encoded, size-limited), so viewers' addresses
don't reach outside servers and pages don't mix in insecure content.
- Everything else is removed: scripts, event attributes (`onclick`, ...), `iframe`, `object`, `embed`, `form`,
`style` and `link` elements, `javascript:` and `data:` addresses, comments.
- Size limits on the description.
- **Cleaned twice**: when saved (the stored HTML is already safe) and again when shown, by whatever shows it: the
hub's website (with a strict Content-Security-Policy that allows no inline scripts) and the proxy.
- **In the client, the proxy is the last line**: IMVU's pages run in Gecko 1.9.2 with chrome privileges (they can
call into the client's Python), so a script there could do far more than on a website. The proxy cleans
descriptions again with the same allowlist before handing them to the client, and gives the client text-only
fields (name, tags) as escaped text.
- **History and moderation**: every edit is kept as a revision; moderators see the history and can revert or lock
a product's info. Changing the rating to *adult* is immediate; changing it to *general* is reviewed first.
### Catalogs (`t5de-catalog`)
- Go program, SQLite or Postgres, S3-compatible storage, docker-compose for operators.
- **API**: catalog and search, revisions, files by hash, uploads, reports, takedowns.
- **No user-facing site** (**Decided**): users, creators and operators use the hub's website. Operators' rights
come from hub tokens ("operator of catalog X"); the catalog enforces them itself, so operators stay in charge of
their server.
- **Small admin page** for what must work without the hub: first setup, storage, keys, registering with the hub,
health, emergency takedown.
- General-audience files are public (cacheable, and catalogs learn less about who wears what); adult files need a
token.
- Operators can build their own branded site on the public API.
### Joining the hub and access (**Decided**)
- **Any catalog can ask to join** the hub. It's hidden until a hub moderator approves it.
- To join, a catalog must give **its own takedown and legal contact**. It stores the files, so the hub forwards
takedown notices and reports about its products to that contact (and can still block a product network-wide).
- Two settings per catalog:
- **Visibility**: *public* (listed in the hub's directory and search) or *private* (not listed; found through an
invitation or link).
- **Membership**: *open* (any hub user can use it), *approval* (users ask, the catalog's operators approve) or
*invite only*.
Any combination works: a public catalog can still require approval; a private one can be open to anyone who has
the link.
- **Seeing versus wearing** (**Decided**): anyone can *see* any catalog's products on someone else (subject to their
own age settings); getting and wearing them needs membership where the catalog requires it.
## In the client's shop and inventory (planned, the user's goal)
- **"Buying" is saving**: getting a T5 product (free) adds it to the person's library on the hub. The proxy shows
library products in the client's own inventory, next to IMVU's, so they're worn like any other.
- **Browsing in the shop**: T5 products appear in the client's shop, with a T5 filter beside IMVU's rating filter
and a T5 badge on T5 products (inventory, shop, product cards). Images are drawn and kept for this (the user's
Pictures\T5DE badges, made by its `draw.py`): badges 18x14, 25x21 and 23x19; filter strips for IMVU's two
layouts with T5 added last (`GA|all|AP|T5`, `GA|AP|AP+|all|T5`) and `all|T5` for people without Access Pass,
who get no rating filter today (T5 products aren't only adult ones).
- **Where the T5 badge goes** (every place the client shows AP/AP+/VIP badges, checked in the client's UI files):
- *Products and outfits* (always, a property of the product): shop results and product pages, inventory,
the outfits viewer and outfit cards (an outfit with any T5 product gets it, as AP outfits do), product
recommendation widgets, checkout ("getting" a T5 product).
- *People* (only if they turned on "Show T5DE badge" for that IMVU account, the same choice everywhere): avatar
cards, the friends list (page and sidebar), the right-click menu on an avatar, chat names, the inbox, the
"invited to chat" and Shop Together invite dialogs, the gifting friend picker, home, and Find People and Get
Matched (with a "T5DE users" filter there).
- *Rooms* (always, not optional: visitors need to know a room needs T5DE to be seen as meant; hosted rooms, and
IMVU rooms decorated with T5 products): the chat rooms browser, room cards, the owner's room manager, and the
room's tab.
The client learns who and what is T5 from the proxy, which asks the hub (badge choices, products, rooms).
- Needs: the hub's product registry and catalogs serving files (so products don't have to be local packs), the
library on the hub, the proxy answering the inventory's and shop's calls for T5 products, and a patch to the
shop page for the filter and badges.
## Outfits
1. The user puts on a community item. The client shows it (proxy answers for it); the patch sends IMVU the outfit
without it.
2. The proxy sends the full outfit to the hub, which keeps the current outfit per linked IMVU account and relays
it on the room channel.
3. Other T5DE users' proxies add the items to that user's incoming `*use`, filtered by their own age settings.
Non-T5DE users see the default.
How it's built (2026-10-29):
- The proxy sees the client's IMQ subscriptions to chat queues (`/chat/<id>`) and tells the hub which chats it's
in; for a room's chat, also the room (from `chat.getOrMakeChat`, `activity: publicroom-<owner>-<n>`).
- The hub tells each proxy in a chat about the other T5DE users there and what they wear (adult products only to
clients whose account counts as adult), on joining and on every change.
- The proxy delivers `*use` / `*remove` lines from those users to the client (IMQ messages of its own, without a
sequence number, which the client doesn't track), and adds their community products to the whole outfits they
send (`*use` with the body, re-sent on joins and every 30 s or so), which would otherwise put IMVU's back.
- Products the viewer has no pack for are skipped (logged), until catalogs serve them.
- The client also posts its outfit to IMVU's chat participant endpoint (`legacy_outfit_message`); the proxy takes
community products out of it and re-signs it.
- Anyone signed in can say they're in a chat; what they learn is what T5DE users in it wear, which anyone in the
chat sees anyway.
Saved outfits are per IMVU account (avatars differ). The library (products added, favorites, followed creators)
is per person, shared across their accounts.
## Rooms (**Decided**: both)
- **Decorations over IMVU rooms**: community furniture placed in an IMVU room, seen only by T5DE users. Placements
travel on the room channel like outfits.
- **T5DE-hosted rooms**: the room lives on the hub: scene, furniture placement, owner, moderators, permissions,
bans, chat, seats, presence. The proxy routes that room's IMQ traffic to the hub, which speaks the subset of
IMQ the client needs (subscribe, state changes, mounts, messages) and the room-joining XML-RPC calls, so the
client treats it as an ordinary room. Needs our own definitions of those messages, written from observation.
- **Chat retention** (**Decided**): not stored by default. When someone reports a conversation, the recent messages
around the report are kept as evidence for 30 days, then deleted.
- Room shells and furniture are ordinary products.
- **Rooms list** (2026-10-29, the user's request): the hub's Rooms page lists IMVU rooms of T5DE users that are
public, listed and not expired on IMVU (`privacy: public`, `type: listed`), and whose owner turned on "List my
public rooms" for that IMVU account (off by default, like the badge). IMVU's public API can't list a user's rooms,
so the hub learns of a room when a T5DE user enters it, asks IMVU about it, and asks again hourly while it's
used; rooms nobody entered for 30 days drop off. AP rooms are shown only to people with an adult account. Each
shows the T5DE users in it now who show they're online.
## Presence and badge (**Decided**: both opt-in, per IMVU account)
- **Badge**: other T5DE users see that this account uses T5DE. The proxy asks the hub which of the accounts it's
showing have it (`POST /api/v1/badges`, docs/api.md).
- **Presence**: friends see "online in T5DE". The proxy asks the hub about the user's IMVU friends; the hub doesn't
store friend lists, but it sees them when asked (stated in the privacy text).
## Better looks
Revisions can carry extra texture maps (normal, roughness, ...) beside the standard ones. The proxy hands them to
T5DE's renderer, which uses them when drawing the item; IMVU products and other clients get the standard textures.
Hub and catalogs only store and label the files; the work is in t5de-renderer.
## Creator pages
Profile, products across all catalogs, follows, and release notifications (over the realtime gateway).
## Moderation and policy
- **One policy at the hub**, binding every catalog to stay listed: original work only; no logos, characters or
real people's likenesses; zero tolerance for sexualized depictions of minors, with the legal reporting duties
that come with it; a takedown process and contact.
- **Levers**: delist a catalog; block a product (global ID, and its file hashes to catch re-uploads) network-wide,
pushed to every proxy; ban a person; close a hosted room; mute or ban in rooms.
- **Console** on the hub's website: report queue, live views (rooms, reports) over server-sent events, actions,
audit log. Catalog operators get their catalog's queue there too.
## Payments later (**Decided**: free first)
Built in from the start so payments can be added without reworking data: getting a product creates an
**ownership record** (source "free", price 0); products have a price (always 0); creators have an empty payout
field. A payment service plugs in later.
## Hub (`t5de-hub`)
One Go program in modules (split into services only if load requires it):
- **Identity**: accounts, linked IMVU accounts, link caps, device-code sign-in, tokens, JWKS.
- **IMVU checks**: background jobs reading IMVU's public API (tagline when linking; `is_ageverified` when linking
and at most daily). Called rarely and cached: IMVU could block the hub's address, and nothing but linking may
depend on IMVU being up.
- **Registry**: products, revisions, ratings, derivations, catalogs and what they host, search (Postgres full-text
first; Meilisearch when needed).
- **Realtime gateway** (WebSocket): presence, badge, room channels, hosted rooms. Live room state in memory on the
node owning the room; NATS passes it between nodes once there's more than one.
- **Rooms**: hosted rooms' stored state.
- **Moderation**: reports, actions, blocklist, audit log.
- **Website and console** (**Decided**: in this repo): browse catalogs, products, creators, rooms, news; manage
accounts (links, privacy, data export and deletion); catalog operators' screens; moderation console.
**Decided**: server-rendered. Built with Go's html/template (no code generation step) and htmx, embedded in the
program: fast, cacheable at Cloudflare, indexable, no JS build. How it looks comes from its CSS (a small design system of our own), not from
rendering on the server; small scripts where a page needs richer interaction (image galleries, drag and drop).
### Data (sketch)
```
person(id, link_cap, settings, banned_at)
imvu_link(person, imvu_id, pairwise_id, linked_at, age_verified, age_checked_at, badge, presence)
creator(id, person, name, page)
catalog(id, url, operators, max_rating, status, signing_key)
product(id, creator, rating, slot, derived_from, price)
revision(product, number, manifest_sha256, signature, notes, created_at, status, extra_maps)
hosting(catalog, revision)
ownership(person, product, source, price)
pin(person, product, revision)
outfit(imvu_link, items[product])
room(id, owner, scene, furniture[], roles, bans)
report(...), moderation_action(...), audit(...)
```
## Infrastructure
- **Hub**: two small VMs or containers behind Cloudflare (already in front of Gitea; supports WebSockets),
managed Postgres with point-in-time backups, R2 for the hub's own assets and T5DE's example catalog (no egress
fees). Jobs in Postgres (River). NATS when the gateway has more than one node.
- **Deploys**: Gitea CI builds container images and runs migrations; OpenTofu once there are more than a couple
of machines.
- **Monitoring**: Bugsink (already used for crash reports) for server errors; Prometheus and Grafana (or Grafana
Cloud) for metrics; a status page.
- **Limits**: rate limits at Cloudflare and per account; upload limits at catalogs.
## Performance
Go throughout. Product files never pass through Go (hash-named, from a CDN or object storage). pgx with sqlc (plain
SQL, no ORM) and indexes designed up front. Public pages cached at Cloudflare. Live rooms in memory on their node.
The proxy streams and adds no waiting.
## Failure
- **Hub down**: community items fall back to IMVU's defaults; everything IMVU keeps working; hosted rooms are
unavailable.
- **Catalog down**: its items come from the proxy's cache or another catalog hosting the same revision, else
defaults.
- **IMVU's API down**: linking and age checks wait; existing links and cached checks keep working.
## Privacy
- IMVU session keys never leave the user's machine.
- Linked accounts are private (pairwise IDs).
- The hub sees room presence and, for presence, the friends asked about; it stores neither beyond what's needed.
- Catalogs see addresses of those fetching adult files (with a token) and of uploaders.
- All stated in the setup and on the website.
## Status
- Phase 1 (local packs) done: t5de-api, t5de-proxy `internal/products`.
- Phase 2 (hub identity) under way: accounts, tagline linking, age (vendor-neutral, IMVU's re-checked daily), link
caps, device sign-in, presence and badge, console, the room channel and the rooms list. Not yet: a sign-in button
in the client (the proxy's `signin` command for now), deployment, hosted rooms. The room channel is tested with
fake proxies and one live client; two live clients need a second throwaway account.
- **Stack changed** (2026-10-29, the user's choice): the hub is a Bun monorepo: Elysia API, Better Auth, Drizzle on
Postgres, Next.js website with Tailwind and shadcn/ui (README). It replaces the first version, in Go. Where this
document says Go or templ for the hub, read that; catalogs are still to be decided (a Bun program compiled to one
file would keep them easy to run).
## Phases
1. Spec (`t5de-api`); local product packs, removing community items from what IMVU sees, creator live preview. No
servers.
2. Hub identity: linking, age checks, link caps, badge, presence; website basics.
3. Registry and catalogs: creator pages, community outfits visible to T5DE users, moderation console.
4. Decorations over IMVU rooms.
5. T5DE-hosted rooms.
6. Better looks (renderer work; can run alongside 3 or later).
7. Later: payments.
## Open questions
- The takedown contact and legal entity for the hub itself.
- Hosted rooms: how much of IMQ and the room-joining calls the client needs (see docs/imq-rooms.md).