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:
- An enabled Lettin creator account, or root-workspace administrator status.
- Current
MEMBERmembership in the world’s workspace. Guests are excluded. - The existing
manage_documentsworkspace permission, plus ownership or a collaborator role on that world.
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 mountedrich-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 astuturuuu-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 namelettin.tuturuuu and fallback port 7833.
Run these commands from apps/lettin through the shared resource queue:
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 canonicalusers.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 withmanage_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 includeslettin-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 inprofile-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 inapps/lettin/src/components/creator-profile-labels.test.tsx cover the exact labels,
help association, and reserved-name validation.