## Age verification with Didit
A new provider (`apps/api/src/age/didit.ts`) behind the existing vendor-neutral age interface. A person verifies once with an ID document, a passive liveness check and a face match; after that, all their linked IMVU accounts count as adult. The hub keeps only "over 18": never the document, photo or birth date.
- Webhooks are checked against Didit's signatures (the raw body or V2, with `-Previous` during secret rotation), and old ones are rejected. If the webhook has no decision, the hub fetches it from Didit.
- A later answer replaces an earlier one. An approval that's taken back (say, after a manual review) becomes `revoked`. A check still in review stays pending.
- Limits, for every provider: one unfinished check per person (starting again resumes it), at most 3 checks in 30 days, none once verified, and a monthly cap (`DIDIT_MONTHLY_LIMIT`, default 30: since 2026-11-01, Didit's free plan is $10 of credit a month, and a check costs $0.30).
- People are only ever sent back to the hub's website.
- The Account page gets an Age Verification card.
- Configuration: `AGE_PROVIDERS=imvu,didit`, `DIDIT_API_KEY`, `DIDIT_WORKFLOW_ID`, `DIDIT_WEBHOOK_SECRET`. The webhook is `<API_URL>/api/v1/age/webhook/didit`.
## Closed registration
Registration is open or closed. It's an admin's setting in the console, with its default from `REGISTRATION`.
- When it's closed, new accounts can only sign in until a moderator approves them, or declines them (which deletes the account).
- The website shows people who are waiting a waiting page, and the API treats them as signed out everywhere but `/me`.
- The console gets a Waiting count, a Waiting for approval filter, Approve and Decline on a person's page, and a Registration setting.
- The migration adds `user.approved` (existing accounts stay approved) and `hub_setting`.
## Testing
- New tests cover Didit (with a fake Didit server), the limits, revocation, return addresses and registration.
- `bun run test`: 35 pass. `bun run check-types` passes.
- Checked locally: the account, waiting, sign-up and console pages render.
- Not run against Didit itself yet. The sandbox workflow "T5DE Age Check" is published, and the next step is a real webhook once this is on staging.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Age verification with Didit
A new provider (`apps/api/src/age/didit.ts`) behind the existing vendor-neutral age interface. A person verifies once with an ID document, a passive liveness check and a face match; after that, all their linked IMVU accounts count as adult. The hub keeps only "over 18": never the document, photo or birth date.
- Webhooks are checked against Didit's signatures (the raw body or V2, with `-Previous` during secret rotation), and old ones are rejected. If the webhook has no decision, the hub fetches it from Didit.
- A later answer replaces an earlier one. An approval that's taken back (say, after a manual review) becomes `revoked`. A check still in review stays pending.
- Limits, for every provider: one unfinished check per person (starting again resumes it), at most 3 checks in 30 days, none once verified, and a monthly cap (`DIDIT_MONTHLY_LIMIT`, default 30: since 2026-11-01, Didit's free plan is $10 of credit a month, and a check costs $0.30).
- People are only ever sent back to the hub's website.
- The Account page gets an Age Verification card.
- Configuration: `AGE_PROVIDERS=imvu,didit`, `DIDIT_API_KEY`, `DIDIT_WORKFLOW_ID`, `DIDIT_WEBHOOK_SECRET`. The webhook is `<API_URL>/api/v1/age/webhook/didit`.
## Closed registration
Registration is open or closed. It's an admin's setting in the console, with its default from `REGISTRATION`.
- When it's closed, new accounts can only sign in until a moderator approves them, or declines them (which deletes the account).
- The website shows people who are waiting a waiting page, and the API treats them as signed out everywhere but `/me`.
- The console gets a Waiting count, a Waiting for approval filter, Approve and Decline on a person's page, and a Registration setting.
- The migration adds `user.approved` (existing accounts stay approved) and `hub_setting`.
## Testing
- New tests cover Didit (with a fake Didit server), the limits, revocation, return addresses and registration.
- `bun run test`: 35 pass. `bun run check-types` passes.
- Checked locally: the account, waiting, sign-up and console pages render.
- Not run against Didit itself yet. The sandbox workflow "T5DE Age Check" is published, and the next step is a real webhook once this is on staging.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: dhkatz <[email protected]>
A Download link in the header, beside the account menu or Sign in, to t5de.dev/download: T5DE's website with the latest setup, its version and checksum. Gitea's release pages are confusing for people new to it.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
A Download link in the header, beside the account menu or Sign in, to t5de.dev/download: T5DE's website with the latest setup, its version and checksum. Gitea's release pages are confusing for people new to it.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: dhkatz <[email protected]>
Shown above the header when the site is served from a staging. address, or with HUB_ENVIRONMENT=staging
(production hides it), read per request since staging and production run the same image.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CI now builds the API and website images after the tests, pushes them to git.t5de.dev, and deploys them through
Komodo (.gitea/scripts/deploy.py): pushes to main as :main to hub.staging.t5de.dev, vX.Y.Z tags as :X.Y.Z to
hub.t5de.dev, with the version pinned in t5de/t5de-infra.
The API image installs with --linker hoisted: Bun's isolated layout keeps the workspace links (@t5de/db) in each
package's own node_modules, which the image didn't copy, so the API couldn't start.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A creator deletes a product: it's no longer listed and nobody new can get it, while people who have it
keep it (its page, files and library entries stay, and rooms keep showing it), as old revisions stay
available. Moderation removes one everywhere, with a reason: its page and files answer 410, and it
leaves libraries and rooms. Both are on the product's page and in the audit log.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The header shows T5DE's logo (the client's, also the tab icon) and Online, Rooms, Products and
Catalogs; the person's pages (Account, Library, Console for moderators) and signing out are in a menu
under their picture: their first linked IMVU account's avatar picture, which /me now gives (kept an
hour), or their initial. Creating a product moves to Products (New product, /products/new; /create
redirects there), as creating a room is on Rooms.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Moderators (up to 20 IMVU accounts, set by ID or name) and the owner account boot
and ban over realtime (room.boot); bans are listed and lifted over REST. Private
rooms are unlisted and joined by the owner, moderators and holders of an accepted
invite (room.invite / room.invited / room.invite_reply / room.invite_answer).
Room images (PNG/JPEG by magic bytes, up to 512 KB) are stored in the database
and served with cache headers. Per-person favorites, ratings and recent rooms.
room.watch pushes room.occupancy; participants carry their IMVU name, and
GET .../participants lists them. Contract in docs/hosted-rooms.md, "Phase 2".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The client takes a room's owner from the instance ID's first part; with 2100000000 there, the owner
couldn't decorate their hosted room (putting furniture on went to their home room instead).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
T5 products placed in ordinary IMVU rooms, kept per room instance
(room_decoration, migration 0006). GET /api/v1/decorations/:instance (and
?instances= for up to 20) gives what the viewer may see, with a hash;
PUT is for the room owner's T5DE person only, checks the products exist,
and tells every realtime session with a chat in that room ({"type":
"decor"}). Hosted rooms' IDs are refused.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rooms made on the website (hosted_room, migration 0005), the REST routes in
docs/hosted-rooms.md, and the realtime room.* protocol: joining with the
capacity, adult and linked-account rules, seats, numbered messages with
forged senders dropped and a rate limit, closing. The Rooms page lists them,
with a form to make one and the owner's edit and close.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
Revisions with an IMVU parent store its public info (image, category path, gender)
from IMVU's product API, looked up after registering and in a periodic pass.
Product views gain parent, image and categories; GET /products takes category and
limit. The website shows the image on cards and product pages.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
/products (search), product pages with revisions, getting and staying on a
revision, retracting; /products/:id/edit; /create and new revisions upload
from the browser straight to the catalog with a token from the hub;
/library; /catalogs (directory, registering with a once-shown key);
/console/catalogs to approve or reject. Every page now sends a nonce-based
CSP (proxy.ts), so product descriptions can't run scripts.
buttonVariants now merges classes, so outline links get their border.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The hub registers catalogs (hidden until a moderator approves them), hands out upload tokens, and records the
revisions catalogs register after validating uploads: numbered, never changed, signed by the hub (its own
Ed25519 key, published at /.well-known/t5de-keys.json). Creators edit product info (descriptions cleaned by an
allowlist) and can retract revisions. People get products into their library, optionally staying on a
revision; the proxy asks which revision to load and where. Contract in docs/products.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The realtime connection now carries the IMVU chats a client is in: the hub tells each proxy in a chat which
other T5DE users are there and what community products they wear (adult products only to adult accounts), on
joining and on every change. Rooms T5DE users enter are looked up on IMVU; those that are public, listed and
whose owner turned on "List my public rooms" appear on the new Rooms page, with the T5DE users in them who
show they're online. AP rooms are shown to adults only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Age checks are recorded in one table (age_check) by provider, method and result, never with documents or
birth dates; one rule decides whether an IMVU account counts as adult (its person's check, or its own).
Providers implement a small interface (start, webhook or browser completion, re-check) and are chosen by
configuration, so phone wallets and Didit can be added, or swapped, as adapters. IMVU's answer per account
is re-checked when it expires (about daily). Existing links' results move into the new table.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The hub becomes a Bun monorepo: apps/api (Elysia, Better Auth for accounts, roles, bans, proxy device
sign-in and JWT keys; tagline linking, presence and the console as before), packages/db (Drizzle; Postgres in
production, PGlite in development and tests), and the Next.js website from apps/web. Self-hostable with
docker compose; the website deploys to Vercel-style hosts, the API to any that runs Bun or containers. The
first version, in Go, is removed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
apps/web: Next.js 16 (App Router), Tailwind 4 and shadcn/ui, light and dark from the system. Pages render on the
server and call the API with Eden Treaty (typed by @t5de/api), forwarding the visitor's cookie; forms use server
actions; sign-up, sign-in and sign-out use Better Auth's React client. /api is rewritten to the API so its cookies
are first-party. Who's online follows the API's event stream (Next's gzip is off: it held the stream back).
Standalone output with a Dockerfile; deploys to Vercel as is.
Co-Authored-By: Claude Opus 5.5 <[email protected]>