Files
t5de-hub/docs/design.md
T
dhkatzandClaude Opus 5.5 7fea63fc4f
CI / test (push) Successful in 37s
Design: everywhere the T5 badge appears
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:41:43 -07:00

29 KiB

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.
  • 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).