Skip to main content
Tulletin (formerly (Tu)lettin) is the creator worldbuilding app at lettin.tuturuuu.com. Creators organize worlds, characters, locations, lore, stories, and linked wiki pages in a private notebook, then explicitly publish revisions for readers.

Creative studio and dedicated spaces

Tulletin is the public creative identity of the existing Lettin app. Its domain, app-session audience (lettin), and storage remain unchanged. The homepage serves artists, storytellers, creators, and worldbuilders with a quiet illustrative atlas; the illustration is not presented as an actual creator project. The public /spaces directory presents the three creative spaces before login; only this exact path is public. Workspace space pages remain authenticated, covered by apps/lettin/src/proxy.test.ts. The workspace studio is the complete project shelf with title/description/tag search and all/private-draft/published filters. Dedicated routes are /{wsId}/spaces/art, /{wsId}/spaces/story, and /{wsId}/spaces/world. Art projects use the editable art tag; storytelling projects use story; worldbuilding uses world. Tags are matched without case sensitivity. A project can appear in multiple spaces. Existing projects without art or story default to the worldbuilding space, so historical work stays discoverable. The main shelf always includes all accessible projects. Removing a space tag changes its placement, not its content, ownership, or publication status. Starting a project offers blank, art, story, and world notebooks. Space pages preselect their matching starter. Starters save normal editable rich-text prompts and a matching tag through the existing create-world API. Nothing is published until the creator explicitly publishes. Spaces add no new access privileges. Public space cards preserve the chosen space through central login, default workspace selection, and pending workspace invitation acceptance. Unknown space names return a 404. Regression coverage: apps/lettin/src/components/creative-spaces.test.ts verifies legacy placement, multiple-space tags, unknown route rejection, and valid starter content in both languages. creator-experience.test.tsx covers shelf search/filter recovery and workspace-aware tool links, invitation destinations, and nested navigation. /{wsId}/toolkit gives workspace-aware links to Tasks, Drive, Calendar, Chat, Mind, Finance, Meet, and platform AI chat, plus the complete app directory. Tools open in their existing apps and enforce their own permissions, authentication, and any billing rules. These are navigation integrations; project content is not copied into those services or automatically synchronized. The app directory is a global destination. Public toolkit links open each app’s entry point. The homepage uses a small wordmark, restrained serif headings, a brief introduction, and a single set of space cards. Slogans, workflow explanations, duplicate calls to action, and the full app launcher are omitted there. The full launcher is on the workspace toolkit page; dedicated space pages omit the space directory. Motion uses short entrance sequences and subtle state transitions. Essential content is visible without scroll-animation support; reduced-motion preferences disable animation and decorative hover movement.

Access and roles

Lettin uses the platform’s central login and satellite app-session exchange, refresh, and logout. Accounts and workspace membership are not duplicated. Lettin /login is resolved by the request proxy before page streaming begins. Anonymous, explicit refresh=1, denied, or unverified sessions receive HTTP 307 with a Location header to central login, preserving the normalized destination through /verify-token. A successfully verified Lettin session plus a shared browser-session marker redirects to that destination and propagates refreshed cookies. A standalone app claim is insufficient. Localized login URLs first use the existing canonical locale redirect. The page is an inert fallback: it must not perform a session lookup inside streamed Suspense, which would turn HTTP redirects into client-only meta redirects. login-redirect.test.ts checks delayed verification, HTTP status/header/body, destination safety, refresh, and MFA denial; proxy.test.ts verifies dispatch before locale rendering. Private world access requires all three conditions:
  1. An enabled Lettin creator account, or root-workspace administrator status.
  2. Current MEMBER membership in the world’s workspace. Guests are excluded.
  3. The existing manage_documents workspace permission, plus ownership or a collaborator role on that world.
The world owner manages collaborators. Editors save drafts; publishers can also publish and unpublish. Workspace administrators do not automatically gain access to somebody else’s private world. Changing workspace roles or removing membership is effective on the next request. A platform administrator is a root-workspace member with the existing admin permission. They can invite creators and individually enable or revoke the can_invite grant. Delegated inviters can create and revoke their own invitations, but cannot delegate invitation privileges. Invite acceptance never grants workspace membership, workspace permissions, or invitation privileges. Use Creator invitations & access in the studio to create an email-bound invitation. Share the resulting link with its recipient. Lettin does not send invitation emails. The recipient must sign in with that verified email and accept within seven days. World collaborators must already be approved creators and eligible workspace members; manage workspace membership and roles through the existing platform settings.

Drafts and publishing

The private notebook Markdown source field has a stable, localized accessible name independent of its formatting hint or validation message. This applies to Markdown editing on the world notebook and entry editor; read-only documents retain their existing editing restriction. The mounted rich-editor.test.tsx regression checks the exact field label with the real editor, and lettin-wiki.noauth.spec.ts checks browser save and reload behavior. Its first scenario logs fixed phase names and completion/failure markers without fixture IDs, credentials, or response bodies. The project creation response has a 30-second deadline within the unchanged 180-second scenario limit. Use the last phase marker to locate interrupted browser runs; missing trace artifacts or a runner shutdown do not establish that the product scenario passed. lettin-e2e-phase.test.ts covers the bounded response wait, exact response matching, failure propagation, and safe phase diagnostics. Editor initialization and read-only permission transitions are presentation changes, not draft edits. The shared editor must not emit a content-update event for an unchanged document when synchronizing editable state. Saved notebooks therefore remain navigable after reload; genuine typing, checkbox edits, and applying Markdown still mark the draft dirty and keep the navigation guard. This applies to the shared rich-text editor and both private notebook surfaces. rich-editor.test.tsx mounts saved content with the real editor, and editor-readonly-transition.test.tsx checks permission changes without false edits while retaining real checkbox mutations. Browser save/reload assertions remain required in exact-commit CI. Saving never changes the public revision. Publish a world’s notebook to make its public page visible, and publish entries separately. Unpublishing a world hides all its entries from public reads. Publishing it again exposes entries that remain published, so unpublish those entries too if they should remain hidden. Rich text accepts a bounded node vocabulary with no raw HTML, scripts, arbitrary embeds, or private resource mentions. Public reads project only published revisions and filter relationships to unpublished entries. An optimistic version check rejects stale writes with HTTP 409, preserving the editor’s local text for manual merging. Artwork uploads use the private Cloudflare R2 bucket tuturuuu-lettin-media. Stored references point to a stable application URL. The Worker streams artwork only to world collaborators or when a published page in its world references it. Unpublishing revokes future public reads; previously downloaded copies cannot be revoked. PNG, JPEG, WebP, and GIF uploads are limited to 10 MB. External HTTPS artwork links are also supported and remain subject to their host’s availability.

Cloudflare architecture

Lettin deploys as tuturuuu-lettin on Cloudflare Workers at lettin.tuturuuu.com. OpenNext builds the Next.js frontend and its API handlers into the Worker. There is no Vercel deployment or central-web content API. Shared Tuturuuu login, verified email, workspace membership, and workspace permissions remain authoritative in the platform. Lettin does not duplicate accounts or store worldbuilding content in Supabase. Identity checks use the satellite session user and existing workspace permission helpers on every request. All content APIs live in apps/lettin/src/app/api/v1. Shared client access uses @tuturuuu/internal-api/lettin. Local API handlers take precedence over central platform API fallbacks. The public health route /api/v1/lettin/health checks D1 schema and R2 access and reports the Worker version. D1 schema migrations live in apps/lettin/migrations. Writes use parameterized statements with revision and role predicates; invitation acceptance uses an atomic D1 batch. Request-scoped D1 sessions start on the primary so access decisions see current creator grants. API and artwork responses use private, no-store.

Development and rollout

The app uses Portless name lettin.tuturuuu and fallback port 7833. Run these commands from apps/lettin through the shared resource queue:
Local development initializes Cloudflare bindings through OpenNext. For runtime verification, use the collaborative preview or a previously CI-built Worker. Do not run app or package builds on the agent machine. The exact-commit Lettin Cloudflare CI workflow provides the real build and type-check evidence. The private wiki shelf and world routes provide localized route-level loading boundaries. Their request-time pages stream into these placeholders during client navigation; the locale layout’s Suspense boundary sits above the shared workspace layout and cannot cover those transitions. The mounted rich-editor.test.tsx exercises the actual shared TipTap editor and Markdown parser. The authenticated wiki E2E retains creation, save, and reload assertions, with named, bounded editor actions so a pending control is reported before the overall test deadline. Before streaming private workspace pages, the proxy checks the same shared workspace invitation-status contract used by the invitation screen, forwarding only the refreshed request authentication. Missing or unjoined workspaces receive an HTTP 307 to the dashboard; anonymous or MFA-required requests return through login. Pending invitations keep their existing invitation screen, including when the requested child world ID is invalid. Joined workspace world and wiki-section routes reject non-GUID IDs, and wiki routes reject unknown sections, with a localized, private, non-cacheable HTTP 404 before any loading fallback renders. The wiki-section, moderation, and creator-profile pages each have their own localized loading boundary around request-time work. This preserves initial wiki entry selection and actor-scoped profile editing; public worlds, creator pages, and dashboard retain their existing behavior. Deferred request-boundary coverage is in remaining-request-boundaries.test.tsx. The render-time permission checks remain authoritative; this preflight never grants access. Regression coverage is in workspace-route-status.test.ts and proxy.test.ts; mocked membership results and real NextResponse checks do not replace the exact-commit CI app build. The storage tests use disposable Miniflare D1 and R2 resources and cover creator access, invitation delegation, conflicting writes, published snapshots, private relationships, and artwork publication/revocation. Set production shared-identity configuration as Worker secrets using the existing production values: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY, and SUPABASE_SECRET_KEY. If the platform uses a dedicated app-coordination signing secret, configure that same TUTURUUU_APP_COORDINATION_SECRET or APP_COORDINATION_TOKEN_SECRET too. The public Supabase URL and publishable key are also build inputs. Never copy a devbox’s local Supabase settings to the production Worker. Register Lettin’s shared app domain and session audience in the central platform before verifying cross-app login. Central identity registration and the Lettin Worker are separate deployment surfaces; a public homepage is not proof that login works. The Lettin Cloudflare workflow validates pull requests and builds on main and production. Only its production-branch job uploads secrets, applies Lettin’s D1 migrations, and deploys the built artifact. It maps GitHub’s PRODUCTION_SUPABASE_URL, PRODUCTION_SUPABASE_PUBLISHABLE_KEY, and PRODUCTION_SUPABASE_SECRET_KEY into the Worker secret names above. Optional coordination signing secrets retain their names. Use LETTIN_CLOUDFLARE_API_TOKEN for a dedicated deployment credential; the existing account credential is the bootstrap fallback. Secrets are passed over stdin without temporary files. Production pushes apply D1 migrations only when the promoted range changes apps/lettin/migrations; those schema releases require D1 Edit permission on the deployment token. Manual production dispatches apply migrations defensively. The workflow checks the deployed version tag against the exact source SHA. For an authorized manual rollout after local validation, apply only Lettin’s D1 migration and deploy its Worker:
wrangler.jsonc pins the Tuturuuu account, the dedicated resources, and the custom domain. Verify the active Worker version and /api/v1/lettin/health, then verify central login, workspace switching, invite acceptance, collaborator roles, membership revocation, conflicting edits, artwork privacy, and anonymous published world browsing. Require exact-commit CI test, lint, type-check, and real app build gates before release; record unrelated repository blockers separately from Lettin’s focused validation. Do not run bun check locally because its task graph can build. Artwork cleanup runs before uploads and claims at most ten unused objects in that world per request after a seven-day grace period. Draft and published references are retained. Claimed objects cannot be newly referenced; failed R2 deletions remain queued for the next upload. A world with no further uploads retains its unused artwork. Public catalogues use 24-card pages with server-side search and creator filters; full entry documents are loaded only when opening one world.

Shared creator identity

Tulletin uses Tuturuuu’s canonical users.display_name, avatar_url, bio, handle, and additive banner_url fields. A username is globally reserved in handles; renaming retains earlier reservations. Existing legacy usernames remain readable. Public creator surfaces select only public fields and require published notebooks; they never expose email or private account details. update_public_user_profile is a service-role-only transaction. The API must resolve the authenticated actor before calling it. A conflicting username or invalid image rejects the complete patch and reserves no new username. Image URLs use HTTPS; avatar, banner, bio, and username can be cleared explicitly. The database contract fixture public-creator-identity.sql covers actor grants, claims, conflicts, rollback, rename, clearing, and private-field rejection. The Creator identity database contract PR workflow applies the complete schema in a disposable Supabase project, runs strict pgTAP assertions, and generates actual database types. Its exact-head artifact is the type-generation source; manual production schema pushes are prohibited. This contract does not authorize production migration or deployment. The first-class /api/v1/users/me/profile endpoint owns identity updates. It accepts the authenticated actor, preserves same-origin checks for cookie writes, and returns 409 for a reserved username. Avatar, banner, and username writes are validated before database access. Until the additive migration arrives, reads fall back to existing identity fields and biography and avatar changes continue working; display-name, banner, and username changes return 503 rather than silently losing them. Rust migration handlers mirror this behavior; the Next.js API remains the live source of truth. Regression evidence includes the profile route tests and backend profile validation and outbound-transport tests. New usernames require 5–32 lowercase letters, digits, or underscores and start with a letter or digit. Common platform/service names and major brands are reserved; separator variants and obvious official/support/admin/team/numeric aliases are reserved for both service names and brands. Ordinary names containing a brand syllable, such as pineapple, remain valid. The canonical pool lives in packages/utils/src/username-policy.json and is checked against the database migration in regression tests. Existing usernames remain readable and unchanged legacy values do not consume a change. Every actual username change, including clearing it, starts a rolling 14-day cooldown. Display names can change twice in a rolling 7-day window. No-op saves consume neither allowance. Row triggers enforce both rules on direct writes as well as the shared RPC; conflicting, invalid, or rate-limited saves roll back the whole patch. The private per-user change history is pruned to 28 days when a change succeeds. Public identity read surfaces never select this private history. Avatar uploads are automatically optimized to at most 1 MB (1,000,000 bytes); banners to 2 MB (2,000,000 bytes). Source PNG, JPEG, WebP and GIF files may be up to 2 MiB for avatars or 5 MiB for banners. This rule covers canonical personal identity uploads from Tulletin, platform web, Nova and mobile; existing objects remain readable. Browser clients resize and encode WebP before transport. Mobile uses its existing in-house image library off the UI isolate and stores optimized, metadata-free PNG in offline queues; older queued avatar payloads are optimized again before delivery. Contact avatars and public profile-link avatars also use the browser optimizer and single-use central upload capability. Active public links receive a stable, link-scoped budget identity and can only write inside their verified link path. Workspace avatar clients use the same client optimizer and shared server path, with workspace permission checks before issuance. Their tickets share the actor budget with personal images. Satellite workspace endpoints delegate issuance to the central API rather than granting raw Storage writes. Mobile Profile settings supports both avatar and banner selection. Banner removal and replacement use the same actor-scoped, retry-safe lifecycle as web profiles. The server independently decodes, applies EXIF orientation, strips metadata, resizes without enlarging (avatars within 1024 x 1024; banners within 2560 x 1440), and encodes WebP. Quality then dimensions are reduced as needed; output is checked before persistence. GIF and animated WebP use their first frame. Transparency is preserved. Unsupported/corrupt images and sources above 20 million pixels are rejected. Public buckets accept only WebP, with the final byte caps enforced by Storage; originals are never persisted. Identity artwork retains public semantics. Personal banner tickets bind the optimized WebP upload to an immutable actor and operation path. The optimized upload handler checks the issued lifecycle receipt before decoding; replay cannot write a different operation or bypass its budget. Native finalize and web profile saves retain banner retirement and concurrency checks. Before an outstanding ticket expires, a transparent one-pixel WebP replaces retired content while retaining its occupied slot; deletion follows expiry. Regression evidence: banner upload route tests, profile-media-upload-put.test.ts, profile-banner-lifecycle.test.ts, and native banner delivery/actor-race tests. Restrictive Storage policies require server-issued, budgeted upload tickets; ordinary authenticated bucket uploads cannot bypass quotas. These changes build on the shared Postgres counter foundation in storage anti-abuse PR #5736 and require the complete dependent release. The identity contract additionally tests reserved names, rolling-window boundaries, no-ops, direct-write enforcement, atomic rollback, byte/MIME ceilings, and raw upload denial. The shared contract applies to current-user media ticket issuers, including the avatar endpoint in this foundation and the dependent banner endpoint. Existing workspace avatar tickets retain workspace-settings authorization and additionally reserve the resolved account media ceilings and the shared global ceilings before signing. Registered satellites delegate workspace avatar tickets to the central optimized media endpoint, which reserves the same actor budget before issuing a ticket; changing apps cannot bypass the shared account or global limits. Permission denials reserve nothing; quota exhaustion and unavailable protection return 429 and 503 without issuing a ticket. Workspace-user avatar uploads also charge the same verified actor used for manage_users authorization: Web revalidates the request session, while Contacts injects its verified satellite session. Request body identifiers never select the budget account; authentication errors, missing membership, and permission denials cannot reserve or sign. Regression evidence: packages/users-core/src/routes/users/avatar.test.ts. Shared public profile-link avatar tickets reserve the account media ceilings above against the verified workspace and link identity, plus the shared global ceilings, before issuing a signed URL; revoked or unauthorized links cannot reserve tickets. These tickets do not charge the link creator’s account. Reservation failure prevents signing; quota exhaustion returns 429 with a bounded retry delay, while protection outages return 503 without retry timing. The shared avatar byte/MIME ceilings also apply. Regression evidence: the public profile-link and workspace avatar handlers’ colocated tests and packages/satellite/src/routes/workspace-settings.test.ts, plus profile-media-upload-limits.sql for authenticated INSERT and UPDATE restrictions.

Wiki, About profiles, and themes

Workspace wiki pages have dedicated directories for characters, worlds, stories, locations, timeline events, relationships, roles, organizations, lore, and pages. Entries retain a canonical world owner and optimistic version checks. Typed relationships and backlinks are world-scoped: self-references, missing targets, and cross-world edges are rejected atomically. Public rendering drops edges to unpublished entries. Dates order the timeline; undated entries remain accessible. The satellite Structure shell shares workspace navigation, notifications, and account controls with other Tuturuuu apps. The in-house editor accepts rich text and Markdown source, including headings, lists, task lists, tables, links, images, and inline formatting. Applying Markdown is explicit; saving is disabled until source edits are applied or cancelled. Artwork must belong to the notebook. Published inline artwork follows the same privacy and retention rules as cover images. Unsupported embed HTML cannot grant access to private task or workspace data. Canonical identity is available to workspace members. Approved creators with manage_documents can additionally edit their About profile: creative headline, pronouns, location, interests, portfolio links, and rich biography. About data is owned by the creator in Lettin D1; public creator pages require a published notebook and never select private account fields. Paper is the default palette; forest, midnight, and rose are alternatives, with clean or editorial typography. World and About themes save with their content. Reduced motion respects user preference and the saved theme setting.

Exocorpse imports and private moderation

Only actors with a verified @tuturuuu.com email, approved creator access, and workspace document-management permission can see or call the importer. CMS reads use the fixed source workspace 3385bd92-3d5e-42f6-b3ad-0d1394af3509 and additionally require its manage_external_projects permission. Canonical JSON file imports do not require source membership unless they copy managed CMS artwork. Reading the source does not modify it. Imports are reviewed before applying, bounded by source collection and payload limits, and copied into a private notebook. Stable source identifiers map wiki references; excluded or missing targets are omitted. Import application is transactional and idempotent. Managed images are copied into owned private target storage; a failed transaction cleans up its newly copied objects. Interactive CMS collections are excluded. No source content is included in repository fixtures. The creator blacklist is a private personal record, scoped to actor and workspace. It supports adding, editing, and removing records and importing source blacklist entries. It is never published alongside a notebook. It does not change anonymous public browsing or imply platform-wide account blocking.

Profile upload budgets and regression evidence

Avatar and banner tickets always use PR #5736’s atomic Postgres budget primitive, independently of its optional storage-egress activation flag. Per account, both media kinds share two tickets per UTC hour and ten per UTC calendar week. Avatars allow three tickets per UTC day; banners allow two. Their combined reserved bytes are capped at 16 MiB per day and 32 MiB per week. Global caps are 10,000 tickets and 16 GiB per UTC day. Weeks begin Monday. The full source-upload transport ceiling (2 MiB/5 MiB) is charged, even for smaller files or unused tickets; clients cannot claim a lower size to bypass the budget. Quota failures return 429 with a bounded Retry-After; protection or schema outages return 503 without retry timing and issue no ticket. Tickets expire after ten minutes, are scoped to a fresh actor-owned WebP object, and prohibit overwrite. They authorize an unauthenticated binary PUT to the central API so native/offline clients retain their existing signed-upload contract. An atomic shared counter consumes each ticket once before reading or decoding; replay returns 409. Actual streamed source bytes are bounded even without Content-Length. The API stores only the optimized result using service-role credentials. Tickets never grant direct writes to public Storage. Images are public identity artwork. Coverage is in creator identity and profile-media pgTAP contracts, shared username and upload-budget tests, central profile/ticket API tests, actual editor Markdown round-trip tests, and Lettin’s disposable D1/R2 tests. The owned satellite runner includes lettin-wiki.noauth.spec.ts for authenticated editing and reloads, public/private wiki projections, imports, blacklists, About profiles, actual banner uploads, Storage byte ceilings, and rename/upload quotas. CI/E2E success must be verified on the exact candidate before merging; test definitions alone are not runtime evidence. For a server using an internal HTTP Supabase origin, set SUPABASE_PUBLIC_STORAGE_ORIGIN to its externally reachable HTTPS Storage origin. Final profile image URLs use that public Storage origin. Signed upload URLs use the central web origin (NEXT_PUBLIC_APP_URL) and point to the optimization API; clients must not construct Storage upload URLs from the returned token. The E2E runner registers supabase.tuturuuu.localhost through Portless and pins this setting in its disposable environment file, so banner uploads use real HTTPS and the production HTTPS profile validation stays intact. Legacy null-owner handle reservations are reconciled only for a unique existing canonical profile. Non-null ownership is never reassigned, unmatched reservations remain unavailable, and unchanged legacy handles consume no change allowance. The full-schema seed preserves its synthetic names while assigning each profile reservation to its real actor. Regression evidence is the dependent identity fixture public-creator-identity.sql; ordinary authenticated handle INSERTs are denied so new claims use the atomic profile RPC. Both Web and Rust profile PATCH paths preserve bounded unchanged legacy handles verbatim after checking only the authenticated actor’s canonical handle. Changed invalid handles return 400 without claiming a reservation; the locked atomic SQL RPC repeats the new-handle checks and remains authoritative across concurrent saves. API regressions cover short, reserved, and historically mixed-case no-ops and SQL error mapping. Signed profile-media PUTs use only the Storage ticket and image MIME header, with cookies omitted; API authorization/default headers remain on the ticket request. packages/internal-api/src/profile-media.test.ts guards this transport boundary. Changed avatar URLs are also checked by the shared database trigger on direct writes; unchanged legacy values remain readable. The database media contract seeds otherwise-writable objects and verifies restrictive policies block both authenticated INSERT and UPDATE for avatar/banner buckets.

Support API parity safeguards

The support form and Web/Rust support creation APIs enforce the existing database ceilings: 64 characters for a name, 128 for a subject, and 512 for a message. Invalid payloads, including addresses with multiple email separators, return 400 before storage. Cookie-authenticated creation and updates require a matching Origin or Referer; explicit Bearer requests retain their independent authentication checks. Cross-origin cookie updates return 403 before Supabase access. Regression evidence: support-inquiry-guard.test.ts, the first-class inquiry route tests, and Rust support/g12 regressions. Profile HEAD follows GET authentication/cache/status but emits no response body; its Rust regression verifies the same boundary. Support inquiry string limits count Unicode code points consistently in the live Zod schema and Rust source. The contact form bounds its disabled profile-name prefill to 64 code points without splitting astral characters. Both API sources trim inquiry messages before validating the 10–512 character range and persisting them, so blank-looking messages are rejected. Regression coverage includes support-inquiry-guard.test.ts, contact-form-values.test.ts, and the Rust support payload boundary tests. Unported inquiry GET/HEAD methods retain the backend dispatcher’s fall-through sentinel instead of claiming a 405 response.

Mobile avatar selection scope

Mobile Account settings bind the source chooser, native picker, and cropper to the initiating account and ProfileCubit. Leaving that account cancels the intent permanently, including signing out and returning to the same account before a dialog finishes. Cancellation retains the saved avatar; a fresh selection can retry a failed upload. Offline avatar admission also retains the captured actor across cache and connectivity waits, rather than assigning the selected image to a later account. Regression coverage: profile_avatar_picker_test.dart exercises delayed native stages, account switch, logout, ABA, cancellation, retry, and disposal; profile_media_actor_race_test.dart exercises actual outbox admission waits. These checks establish source behavior, not device upload or production delivery.

Personal banner replacement and deletion

The current-user banner endpoints and consolidated mobile Account settings use actor-scoped durable operation receipts. Offline and ambiguous mobile upload/removal retries preserve the same operation ID in the encrypted outbox. Replaying a committed operation finishes cleanup without applying an old upload or removal over a newer profile edit. A staged upload captures the prior banner; an intervening change returns a conflict rather than overwriting it. Avatar behavior is unchanged. The web Account settings also display the current banner and offer upload, replacement and removal. Upload retries retain the same operation receipt after an ambiguous response, keep the saved preview until finalize succeeds, and never patch the profile or delete Storage objects directly. The web and mobile surfaces share the existing server quotas and lifecycle; no client success establishes production migration or Storage availability. Web coverage is in profile-banner.test.ts and settings-banner.test.tsx. Web banner selections are bound to the initiating account. Switching accounts or closing the editor aborts outstanding requests and suppresses stale notifications. Upload, finalize and removal requests include an expected actor checked before privileged effects; existing mobile callers that omit it remain compatible. Deferred image preparation and account-switch regressions cover this boundary. Mobile banner delivery validates the exact operation receipt before accepting an already committed or uploaded response, uploading bytes, or finalizing. A missing, malformed, or different operation ID fails as a response-contract error and cannot be replayed as a transport failure. Capability-upload failures retain HTTP status and Retry-After delta seconds or HTTP-date with a fixed safe category; arbitrary provider error bodies are not propagated. Capability retry delays use a defensive seven-day client wait ceiling; this does not bypass server quota admission. Negative or malformed delays are omitted, and expired HTTP-dates become zero. Mobile media failures offer localized guidance for image limits, authorization, conflicts, cooldowns, temporary availability and invalid receipts. The inline recovery action starts a fresh image selection (or a fresh removal intent), without automatically replaying ambiguous bytes. Native choosers retain the screen-owned context while clearing the transient recovery alert. Banner failure guidance also appears beside the active banner controls and follows the current actor and operation state. Only the current authenticated owner with a confirmed internal email can copy fixed-field diagnostics; messages, provider bodies, capability URLs and credentials are excluded. Starting a new intent, successful recovery or actor departure retires the old failure metadata. Replay-safe banner server failures retain their original operation ID in the pending queue and remain pending success, not a confirmed upload failure. Deferred avatar and banner choosers retain a sticky initiating actor and cubit lease across every native stage, including account ABA and mounted provider replacement. Regression evidence: profile_upload_receipt_failure_test.dart, profile_banner_delivery_test.dart, and profile_avatar_delivery_test.dart. Consumer evidence: profile_media_failure_contract_test.dart, profile_media_failure_scope_test.dart, profile_media_recovery_test.dart and profile_banner_picker_test.dart. Profile writes retire only canonical banner object paths under the configured Storage origin and the authenticated actor’s directory. External URLs and another actor’s objects are never deleted. Retirement is atomic with the existing locked profile mutation, including the live Web and future Rust profile handler. Permanent URL tombstones prevent restoring retired files during asynchronous cleanup. Supabase signed upload tokens are valid for two hours. The server records a conservative three-hour lease before returning a token, and retirement preserves an inert non-sensitive Storage slot until all leases have expired. Cleanup replaces the retired image with a tiny non-image marker using the bucket’s allowed image metadata; outstanding non-upsert tokens cannot overwrite that slot. The slot is removed only after the lease grace period. This removes original bytes at the origin after successful cleanup; CDN propagation and previously downloaded images are not an immediate erasure guarantee. See the Storage signed-upload contract. Storage failures remain in a service-only durable queue. Authenticated banner requests perform bounded sweeps; later requests finish expired-slot deletion. There is no claim that a background scheduler has run. Unfinalized staged receipts are retired after a 30-day replay window; receipts that already committed remain idempotent. Receipt creation is bounded to 100 new actor operations per UTC day; existing ticket/hour/day/week/global/byte quotas remain authoritative. Interrupted quota reservations are conservatively charged again, never refunded or bypassed. Regression evidence: personal-banner-lifecycle.sql covers actor grants, locked replacement, ABA prevention, stale-finalize conflict and committed replay; profile-banner-lifecycle.test.ts and banner route tests cover exact path ownership, lease ordering, storage failure/retry, malformed input and object validation. profile_banner_delivery_test.dart covers stable replay IDs and lost-finalize responses. SQL application/type generation, real Storage/CDN behavior and device offline delivery require exact CI and authenticated deployment verification.

Identity field accessibility

Creator identity fields expose the translated field name as their accessible label. Username policy help is a separate description, so keyboard and assistive technology users can locate the field without treating the help as its name. English and Vietnamese rendered regressions in apps/lettin/src/components/creator-profile-labels.test.tsx cover the exact labels, help association, and reserved-name validation.