Register private Storage receipts and authorize media delivery in Convex - #33
Conversation
The wedding album is the second real consumer of Transloadit Storage and Viewer. Guest uploads need durable, private originals with receipts the app can authorize, which Assembly result URLs cannot provide. - Component: register verified Storage receipts from completed Assemblies once per Workspace, asset and version, with Assembly provenance. A malformed or cross-Workspace receipt fails the whole status update. Paginated local reads, exact-version lookup and a deletion ledger that hides assets before Storage deletion and keeps failures retryable. - Example: server-created upload records choose each upload's Storage prefix; photo originals go to Storage with a ThumbHash when TRANSLOADIT_WORKSPACE is set. media:forDelivery is the single authorization query for preview, original and download. Hosted albums keep Convex Auth tokens in httpOnly cookies through the official Next.js integration, so server routes can authorize. - Cleanup: dry runs cover Convex, Storage and R2; expiry hides first, deletes bytes, then drops references; nothing outside the deployment's demo prefix is ever deleted. The Viewer delivery route and gallery switch follow with the Viewer alpha. Video stays on R2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
- Use the shared extractStoredAssemblyResults from @transloadit/zod/v3/storageResults instead of the provisional selector. - Add /api/transloadit/media with Viewer's createStorageRoute: explicit server-only Smart CDN credentials, one Convex authorization per request through the guest's cookie, uniform 404s. - The gallery renders Storage photos with @transloadit/viewer/react Image, pages through media:list and offers original downloads. With Storage enabled, photos no longer get public R2 renditions; video keeps R2. - Completed deletions leave tombstones so a delayed notification or refresh cannot register a deleted asset again (red-first regression). - Vendor the unpublished SDK preview (source SHA in manifest) so CI can verify the integration; the QA template resolves those file: specs. Replace with published versions before merging. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Council review of the demo cleanup: - The deletion ledger is album-scoped and paginated, so failures never block later assets. - An entry is completed only after Storage deletes the asset or reports it gone. Entries whose receipt or current location lies outside this deployment's demo prefix stay hidden and pending with a reason, instead of being released or deleted. - The backend reports the prefix it assigns to uploads, so cleanup cannot derive a different one. - Without Storage access nothing is hidden; dry runs count every page and disclose truncation. - Document that guests from the browser-only session provider enter once more. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A real Convex deployment rejects `.paginate()` inside components ("only supported in the
app"); convex-test does not. The gallery now reads a bounded, newest-first window that "Show
more" grows, which also keeps a reactive gallery free of gaps when new photos arrive. The
deletion ledger pages with an explicit (deletionRequestedAt, _creationTime) index cursor.
- Vendor SDK commit 4b904e5 (clean committed artifacts, source hash in the manifest), which fixes
preview grant lifetimes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- listStoredAssets pages with an explicit (createdAt, _creationTime) index cursor, including an inclusive endCursor; ties within one registration instant and 1200-row walks are tested. The gallery freezes loaded pages with end cursors, so live uploads never shift photos between pages. - Status, results and receipts persist in one transaction; completed Assemblies without results still record their status; albumless receipts can enter the deletion ledger; Assembly listings skip hidden versions before their limit. - wedding:listGallery returns only the R2 renditions it renders, never Storage receipts or their ThumbHash; private photos keep their Assembly provenance for data-assembly-id. - Document the Community-plan watermark scope of filtered originals and the activation order for Storage and delivery credentials. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- requestStoredAssetDeletion keeps an asset whose newest visible version is not yet expired, so an overwrite after the cutoff never loses its fresh version to expiry (red-first test). - Expiry scanning continues from a cursor, so skipped fresh assets cannot stall later eligible ones; the cleanup script follows it. - Document in the README recipe that filtered originals are watermarked on the Community plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Council review of the integrated branch: - A reset records unregistered Storage objects as hidden ledger entries before deleting them, so a delayed notification finds a tombstone instead of registering deleted media. - A frozen gallery page that outgrows the 1000-row limit reports a split cursor; the client splits it into two contiguous frozen pages instead of skipping rows. - Dry runs use the same selection as expiry (distinct assets, newest version across albums) through a read-only previewStoredAssetExpiry query. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Opening or closing the viewer remounts thumbnails (the Motion boundary is removed while a photo is shared with the viewer). With `sizes="auto, ..."` the remounted images picked a larger candidate, so each toggle re-authorized and re-rendered every visible photo at 1200px. Thumbnails now use explicit sizes derived from the justified row layout, so a remount selects the same URL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The viewer's picture is `display: contents`, so its <source> elements (displayed by default) became extra grid rows and pushed the photo about 143px below centre on a 390px screen. Sources are now hidden from layout; browsers still select candidates from them. On narrow screens the viewer toolbar wraps, so the actions no longer truncate the photo name. Browser proof at 390x844 after all finite animations: image centre offset 142.88px -> -0.01px, grid items source,source,img -> img, name truncated -> readable; desktop unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Resets refuse to run while R2 or Storage is unconfigured unless skipped explicitly (--skip-r2, --skip-storage), so references are never purged while media stays reachable. - Cleanup summaries read one page per transaction and dry runs count each expired asset once across pages, matching the real deletion. - Local and self-hosted deployments need TRANSLOADIT_STORAGE_NAMESPACE before using Storage, so developers sharing a Workspace never write to or clean up one another's prefix. - The gallery drops a page whose query fails and shows a load error instead of stale photos. - The release workflow refuses to version or publish while runtime dependencies use local specs, such as the vendored SDK preview. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ences - A configured TRANSLOADIT_WORKSPACE without a unique namespace now fails with an actionable error instead of silently choosing public R2; only an unset Workspace keeps the R2 pipeline. - --skip-r2 no longer purges Convex results that still reference R2 media (or when a truncated count cannot rule that out); the run reports them as kept and incomplete. - An S3 DeleteObjects response with per-key Errors is a failed batch, so results are never forgotten after a partial R2 deletion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upload binding or missing geometry can leave a page without listable photos while later pages still have them. The empty state hid the only way forward. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The committed component bindings had been hand-edited because `convex codegen --component-dir` needs a root convex.json and a backend. A root convex.json now points the CLI at the example app, and example/convex/tsconfig.json extends the example config so the tsconfig that `convex init` would otherwise write cannot hide the paths mapping to src/. `yarn codegen` runs the official command and ignores CONVEX_DEPLOY_KEY from .env. CI regenerates against a throwaway anonymous local backend (no credentials) and fails on drift. The official output differs from the hand-edited file only in indentation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Thumbnails opt into Viewer's placeholder="blur" with an explicit cover fit; the letterboxed fullscreen photo passes contain and no placeholder, so the stylesheet cannot change what Viewer's guard sees. No load handlers: loaded opaque pixels cover the inline background. A ThumbHash is preview pixels, so media:list and media:forDelivery now return it only for versions an admitted guest may preview. Vendors the Viewer preview from the uncommitted SDK follow-up (sha256 d93bc693...441e4), recorded in the vendor manifest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The runtime is identical to the previous preview pack; only its README and reference docs changed. The manifest records the SDK commit and the tarball hash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the hand-rolled index positions and the example's useStablePages with convex-helpers' paginator and its React usePaginatedQuery. Pages now follow Convex's cursor protocol: pinned loaded pages keep their range under live inserts and deletes, and pageStatus/splitCursor split pages that outgrow the read limit. The component keeps its guard rails: cursors from another album, an earlier release or garbage are refused as InvalidCursor so the hook restarts, page sizes stay clamped, clients may only lower maximumRowsRead, and a pinned page that hits the read limit keeps its end so splitting never drops rows. The expiry scan keeps its explicit mid-page positions. Non-cursor failures now reach a small error boundary that shows the load failure instead of stale photos. erasableSyntaxOnly moves to tsconfig.node.json, which covers only what Node runs by stripping types, because convex-helpers ships TypeScript sources. The convex peer floor rises to ^1.43.0, the paginator's own requirement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Viewer blur background now covers named crops instead of stretching, and the Storage extractor keeps Assembly errors when ok is null. Both remain unpublished previews until the SDK release lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…loor The README and changeset now describe listStoredAssets' Convex cursor protocol instead of a growing window, show storedAssets in the data model, and say plainly what a tombstone keeps (the receipt minus its ThumbHash, provenance, album/user/upload linkage) and that nothing purges it. A separate changeset calls out the convex ^1.43.0 peer, which the tested convex-helpers 0.1.124 paginator declares; a consumer pinned to convex 1.43.0 typechecks and passes the component tests. CONTRIBUTING records the helper behavior keepLoadedRange repairs and the regression test that guards it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Checklist, privacy boundary, measured pagination tradeoffs, guard parity and the remaining release and merge gates for PR #33. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pin @transloadit/zod 5.0.1 and @transloadit/viewer 0.0.3 exactly from npm and drop the vendored SDK packs, their resolutions, the QA template's file: overrides and the tarball Git attributes. Utils resolves to the unchanged 4.9.0. The registry tarballs carry the same runtime JavaScript as the vendored e521c00 packs (Zod differs only in .d.ts member order). The Yarn age gate stays on with exact exceptions for the two same-day releases, and the release guard still refuses local dependency specs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Capture the node-sdk merges, the published versions, the transient npm staging race (404 metadata, 409 retry, rerun without a bump) and what the registry swap changed, since the SDK finish checklist predates the release. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Convex numbers include NaN, which passes Math.min and Math.max. With an end cursor the paginator then had no read limit at all: one query returned all 1055 rows past the 1000-row ceiling. Both list paths now refuse NaN and infinite page limits; finite clamping is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
--skip-storage and --skip-r2 only relaxed the missing-configuration error. With both backends configured, the flags still hid receipts, deleted Storage assets and R2 objects, and purged results. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The component and the other ledger calls already treat the album as optional. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The muted grey this PR introduced for the alert measured 4.28:1 on the page background, below 4.5:1 for 13px text. The existing error colour measures 7.20:1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ving notes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge and production verification — 2026-09-25Kevin explicitly approved merging the green PR and following its automatic production deployment.
Separate release automation blockerRelease run 36167722300 failed while Changesets tried to update the existing The deployment is successful, but this run did not prepare/publish a new Convex npm version. No protections were changed, release branches deleted, or force-push workaround attempted. Release automation needs a separate scoped decision/fix. |
Release automation unblocked — 2026-09-25With Kevin’s explicit approval, the organization policy is now split:
The replacement force-push rule was created and verified before removing that rule from the original combined ruleset, so other branches had no protection gap. Effective-rule readback confirms unchanged main/feature protections in Convex, Node SDK, Content and API2. Uppy’s main/release rules and its two separately inspected rulesets are unchanged. Release run 36167722300, attempt 2 is green and created Version Packages #34. Its commit The full before/after snapshots and restore payload are saved in |
Why
The wedding album is the second real consumer of Transloadit Storage and
@transloadit/viewer, after the website. Guest uploads need durable, private originals and receipts that the app can authorize. Assembly result URLs provide neither. This PR is the Convex half of the plan agreed with the DAM/Viewer side: receipts only in reactive queries, and a single request-authorized delivery route.What
Component (
@transloadit/convex, minor)storageWorkspace/TRANSLOADIT_WORKSPACE).extractStoredAssemblyResultsfrom@transloadit/zod/v3/storageResults, so a malformed or cross-Workspace receipt fails the whole update.listStoredAssetsusesconvex-helpers' indexed paginator and its ReactusePaginatedQuery. InclusiveendCursorranges and split signals keep loaded pages stable. Old/malformed or cross-album cursors reset; non-finite limits are rejected and finite reads stay bounded to 1,000 rows.getStoredAssetreturns one exact version.makeTransloaditAPIexposes no receipt reads.Example
uploadsrecords choose each upload's prefix:convex-demo/<deployment>/<album>/<upload>/. Photo originals go through/transloadit/storewith a ThumbHash. With Storage enabled, photos get no public R2 renditions.media:listshows only receipts bound to a matching upload.media:forDeliveryis the single authorization forpreview,originalanddownload: it checks the live session, the current invitation, the album and the exact version.proxy.ts, httpOnly cookies)./api/transloadit/mediais Viewer'screateStorageRoutewith explicit server-only Smart CDN credentials. The gallery renders@transloadit/viewer/reactImagewith stable cursor pages and opt-in receipt ThumbHash blur. Preview metadata is authorized before reaching the browser; transparent images and letterboxed fullscreen images stay blur-free. The viewer offers original downloads.scripts/cleanup-demo.ts:Scope and limitations
/transloadit/storeSteps that use:originaldirectly from its upload watermark. This album stores a photo filter of:original, so on the Community plan the stored photos are watermarked. The API2 fix is deferred to transloadit/api2#9253, with no workaround here. The byte-identity results below come from theopen-test-prodtest workspace and say nothing about Community-plan uploads.file:overrides are removed. The publishability guard and Yarn age gate remain; only these two same-day exact versions are exempt. The component now requires Convex 1.43+, matching the helper's peer requirement; a minimum-version consumer was verified.TRANSLOADIT_WORKSPACE.Evidence
Tests. Final
yarn checkpasses: 178 tests in 26 files, with no type errors. Red-first regressions cover:endCursorpages and split pagesCurrent verification — September 25. Final head
f253b0b0724a519a61d1cd99ef006aa00272a051is fully green in CI, including Node 24/26, package/example builds, lint, official component-codegen drift, browser tests and deployment QA. The production deploy was skipped. Final council findings and the lead's boundary finding were reproduced red-first and fixed:A local harness renders the real gallery, published Viewer and pagination hook with synthetic data: desktop/mobile blur-to-image loading, transparency, 24→30 pagination, filtered empty page→six photos, empty/error states, fullscreen and Escape/focus all pass. It is not a new production Storage canary. The evidence-based user test found the new error alert's low contrast, now fixed; pre-existing muted-caption contrast remains a follow-up. The loaded count is intentionally not an exact total, and the isolated empty-page harness is not the full page chrome.
The paginator substitution removes 115 net maintained runtime/config lines including its non-finite guard, while adding regression coverage. Its client pagination bundle grows by about 843 bytes gzip and server bundle by about 4.4 KB gzip; the benefit is less bespoke pagination code, not smaller bundles. Living notes record measurements, review reconciliation, evidence and release facts. No new production canary, credentials or deployment were performed in this follow-up.
Earlier production canary on the
open-test-prodtest workspace, synthetic assets only, no signed URLs recorded (prior implementation evidence, not a fresh canary onf253b0b):smart_cdn:signkeyBrowser spike with Chromium, the local production build and an isolated Convex preview, 80 synthetic photos uploaded through the real Uppy → Assembly → verified-webhook path:
__convexAuthJWTand__convexAuthRefreshTokenare httpOnly,SameSite=Laxand persistent.media:listis 15.1 KB for 24 receipts and 50.3 KB for 80 (~630 B per receipt, no URLs); HTTP p50 is about 110 ms.private, no-store.attachment; filename="spike-81.jpg"with exact bytes. This is test-workspace evidence only: it does not cover Community-plan filtered uploads, see api2#9253.auth:signOutcompletes.AUTH_EXPIRED. Fresh route redirects keep working.spike-77downloads with the same SHA-256; keyboard activation and Escape return focus; 0 auth cookies after a completed logout, then media HEAD → 404private, no-store. There were no console or page errors; the only cancelled fetches were caused by the native download.Viewer remount (fixed in c19ac54). The existing Motion design unmounts the thumbnails' shared boundary when a photo opens. With
sizes="auto,…", the remounted thumbnails picked 1200 px: 24 extra no-store authorizations per toggle.sizes/srcsetidentical across opening and closing the lightbox.Mobile viewer layout (fixed in c2a7f0a). The viewer's
<picture>isdisplay: contents, so its<source>elements became extra grid rows.Council follow-ups (c1c280a, 5ffce0e).
Canary cleanup.
retryNeeded: false.Independent review. Browser pass by the DAM/Viewer owner:
private, no-storeafter a completed logoutNot in this PR
TRANSLOADIT_WORKSPACErepository variable and production delivery credentials.🤖 Generated with Claude Code