Feature Owner (original implementation): Mich Tapawan — original Background Assets / Asset Library build
Updated by / current owner: Patrick Babala — this revision prepared 2026-10-01
Module: Develop (Content Production)
Priority: P1 — core content-production dependency (Feature Audit ID 3.1)
Status: Handoff — Draft (v1.0)
Date: 2026-10-01
Issue: [3.1] Asset Library (Feature Audit May 2026); related [3.6] Archive Asset, [3.7] Secure Asset Delete, #437 (trash soft-delete), #777 (typed-DELETE confirmation). The GitHub issue number for [3.1] could not be confirmed in this session and is intentionally not fabricated.
PRs covered by this hand-off: #695 (audio background-asset upload fix), #711 / #723 / #725 / #726 / #770 (feature/3.6-archive-asset — archive, bulk operations, sorting, 120-day purge), #867 (SVG upload stored-XSS hardening). Security ticket: M4 / #852.
Companion doc: Internal Technical Guide — Internal-Technical-Guide-Asset-Library.md (architecture, full DB schema, RLS, API request/response specs, helpers, ops, troubleshooting). This Hand-Off stays product-facing; where engineering depth is needed it cross-references the ITG instead of duplicating it.
EXECUTIVE SUMMARY
What is this feature?
The Asset Library (surfaced in the UI as “Background Assets”) is a creator-owned repository of reusable media. Creators upload a file once — image, audio, video, or PDF document — or register an external media link, and then attach that asset across quests, adventures, and activity cards through the editor’s “From Assets” picker. Assets can be renamed, archived (soft-deleted), restored, and permanently deleted.
Why does it matter?
Assets are the raw material of every quest. Without a shared library, creators re-upload the same background images, audio, and video for each new quest, media ownership is scattered, and content is harder to keep consistent. The library is the single place a creator manages reusable media.
What’s the MVP scope?
Upload image / audio / video / PDF files with server-enforced MIME and size limits
Add external media links (YouTube, Vimeo, audio URLs, image URLs)
Browse the active library and sort by name or date added
Rename an asset
Archive (soft-delete) an asset and view it in the Archive page
Restore an archived asset
Permanently delete an asset with a typed
DELETEconfirmationBulk select, bulk archive, and bulk permanent delete
Agency-scoped visibility (an agency member sees the agency’s assets; a standalone creator sees only their own)
Automatic video thumbnail extraction (first frame) with placeholder fallbacks
“From Assets” selection inside the quest/adventure editors
Shipped scope added after the original build:
PDF (
DOCUMENT) assets and a 5-minute video duration cap (6bf936f0).120-day archive + auto-purge across quests, adventures, and background assets (
ae533e9b,b813aaa8, PR #770).Sorting and a selection action bar for bulk operations (
5d3eeea3,7de73df0).Standardized destructive confirmation via the shared
ConfirmDestructiveModal(13373adb, Issue #777).Upload security hardening — SVG hard-reject, server-pinned content types, and a
public-assetsbucket MIME allow-list (fix/852-svg-upload-xss, PR #867, M4).
Still deferred: folder organization/sharing for assets, content-hash deduplication, per-creator storage quotas, asset versioning, and any asset management beyond the read-only reviewer picker.
1. USER PAIN POINT & SOLUTION
Current State (Without Feature)
A creator building multiple quests re-uploads the same images, audio, and video for each one. There is no single place to see, rename, reuse, or retire media, and there is no safe way to remove media that is no longer wanted.
Pain Point
Emotional: Repetitive uploads are tedious, and accidental permanent deletion is frightening.
Functional: No reusable media layer means duplicated storage objects and inconsistent quest visuals.
Business Impact: Slower quest production and higher storage waste; media cannot be governed or cleaned up.
Future State (With Feature)
Creators upload media once, reuse it everywhere through a consistent picker, rename it for clarity, and retire it safely — archiving first (restorable for 120 days) and only then deleting permanently behind a typed confirmation.
Marketing Hook
“Build once, reuse everywhere — one media library for every quest, adventure, and activity.”
2. CODEBASE ASSESSMENT
Current Implementation Status
The Asset Library is implemented as a creator dashboard (/creator/background-assets) plus an archive page (/creator/archive), backed by eight API routes under the (background-assets) route group and a set of shared upload helpers. Metadata lives in the public.asset_metadata table; bytes live in the public-assets Supabase Storage bucket.
Primary Files
app/creator/background-assets/page.tsx— main “Background Assets” dashboard: fetches active assets, sorts by name/date, and hosts the upload dialog and archive link.app/creator/archive/page.tsx— the Archive (“Trash Bin”) page for soft-deleted assets, with restore, permanent delete, bulk actions, and a days-remaining badge.components/creator/background-assets/AssetList.tsx— the asset grid: thumbnail/audio/music/document rendering, rename, archive, permanent delete, and bulk selection.components/creator/background-assets/assetUploadingDialog.tsx— “Upload Asset” dialog offering file upload or external link.components/creator/background-assets/DeleteAssetModal.tsx— “Move to Archive” confirmation (states the 120-day restore window).components/creator/background-assets/DisplayAsset.tsx— full-screen preview modal for image, video/audio (YouTube embed or native player), and PDF (<iframe>+ “Open in new tab” fallback).lib/backgroundAssets/useBackground.ts— client hook that fetches/api/creator/list-assetswithcache: "no-store".lib/backgroundAssets/uploadHandlers.ts— client-side validation (MP4-only, size limits, 5-minute duration) and multipart/JSON submission.app/api/creator/(background-assets)/upload-assets/route.ts— file + external-link upload endpoint (validation, storage, metadata insert).app/api/creator/(background-assets)/list-assets/route.ts— active assets for the current creator/agency.app/api/creator/(background-assets)/get-assets/route.ts— legacy duplicate oflist-assets.app/api/creator/(background-assets)/get-trash-assets/route.ts— archived assets.app/api/creator/(background-assets)/edit-assets/route.ts— rename.app/api/creator/(background-assets)/delete-assets/route.ts— archive (soft-delete; sets a 120-day expiry).app/api/creator/(background-assets)/restore-assets/route.ts— un-archive.app/api/creator/(background-assets)/purge-assets/route.ts— permanent delete (DB row + Storage object).lib/uploadAssets/validate.ts— shared MIME/extension allow-lists, SVG hard-reject, content-type resolution.lib/uploadAssets/uploadFileToStorage.ts— Buffer upload with server-pinned content type.lib/uploadAssets/saveMetadataToDB.ts/parseRequestData.ts/probeMp4Duration.ts— insert, request parsing, MP4 duration probe.lib/archive/purgeArchivedContent.ts/lib/archive/getDaysRemaining.ts— 120-day retention and purge logic.app/api/cron/check-pending-deletion/route.ts/app/api/cron/purge-expired-archived/route.ts— 30-day warning email and 120-day auto-purge.
Current Behavior Summary
Creators upload a file or register an external link; files are validated server-side for type and size, and videos are capped at 5 minutes.
Every library asset belongs to a creator, and visibility is agency-scoped.
Archiving a single or multiple assets removes them from the active library immediately and starts a 120-day countdown.
Archived assets are restorable; permanent deletion requires typing
DELETEand is re-validated server-side.A scheduled job emails a 30-day warning and a later scheduled job purges assets older than 120 days.
Strengths Already Present
Server-side validation and limits that cannot be bypassed by a direct API call.
Safe destructive paths: archive is reversible, permanent delete is typed-confirmation gated on both client and server.
Reusable media across the editor through the “From Assets” picker.
Agency-aware ownership and listing.
Automatic thumbnails for uploaded videos with graceful placeholder fallbacks.
Gaps / Hardening Opportunities
Orphaned Storage objects: if the metadata insert fails after a successful upload, the stored object is not cleaned up (
upload-assets/route.ts:194-218).Best-effort Storage cleanup: purge logs
"Storage cleanup failed:"and continues; failures are not retried.Stale copy:
lib/uploadAssets/uploadFileToStorage.ts:111says “5 MB limit” while the configured image limit is 10 MB; migration comments describe a 30-day retention while the code enforces 120 days.expires_atis write-only: written on archive and cleared on restore, but the purge job uses thedeleted_atcutoff instead.Duplicate endpoint:
get-assetsduplicateslist-assets, and its fallback type inference does not coverDOCUMENT.E2E drift:
tests/e2e/creator/archive-asset.spec.tsandtests/e2e/creator/secure-asset-delete.spec.tsstill expect older copy (“Move to Trash”, “30d left”) and should be refreshed.
3. 4D FRAMEWORK MAPPING
Diagnose
Creators need a durable, reusable store of media so quests can be assembled quickly and consistently, and so obsolete media can be retired without risk.
Design
Media is modeled as first-class creator-owned assets (asset_metadata rows + Storage objects) with an explicit lifecycle: active → archived → permanently deleted.
Develop
Creators upload or link media, tag it with an asset/usage type, browse and rename it, and select it from the editor’s “From Assets” picker.
Deliver
Quests, adventures, and activities render the referenced media, and the archive/retention lifecycle keeps storage clean and recoverable.
4. USER FLOWS
Entry Point
Creator dashboard:
/creator/background-assetsCreator archive:
/creator/archiveEditor pickers: “From Assets” inside the quest/adventure editors (e.g. Select Background Image → From Assets)
Success Criteria
An asset uploads successfully and appears in the library.
The asset can be renamed and previewed.
The asset can be attached to content via the “From Assets” picker.
An archived asset disappears from the active library and appears in the Archive.
A restored asset returns to the active library.
A permanently deleted asset is gone from both the library and Storage (behind a typed
DELETE).
Main Flow (Happy Path)
Creator opens Background Assets.
Creator clicks Upload Asset and chooses Upload File (or Add External Link).
The client validates type/size (and video duration), then submits to the API.
The API validates again server-side, uploads bytes to Storage, and writes the
asset_metadatarow.The asset appears in the grid with a thumbnail (video thumbnails are extracted from the first frame).
The creator reuses the asset from an editor’s “From Assets” picker.
When done, the creator archives the asset (restorable for 120 days) or permanently deletes it with typed confirmation.
Edge Cases
Oversized file: Rejected client- and server-side with a size-specific message.
Video over 5 minutes: Rejected after the metadata duration probe.
Non-MP4 video: Rejected client-side (“Only MP4 video files are allowed.”).
SVG/SVG-named-raster: Rejected before storage (stored-XSS guard).
Already archived: Archiving again returns “Assets are already archived”.
Already active: Restoring again returns “Assets are not archived”.
Permanent delete typed incorrectly: Button stays disabled and the server rejects any token other than
DELETE.Upload succeeds but metadata insert fails: The request returns an error; the Storage object may be orphaned and needs manual reconciliation.
Decision Points
IF the asset is uploaded as a file → bytes are stored in
public-assetsand afile_path/file_urlare recorded.IF the asset is an external link → no bytes are stored;
sourceis external andfile_urlpoints at the third-party URL.IF
usage_contextisBADGE→ the file is stored but noasset_metadatarow is created (badge images do not clutter the library).IF the creator is in an agency → the library is scoped to the agency’s users; otherwise only the creator’s own assets are visible.
5. INFORMATION ARCHITECTURE
Primary Information (Always visible)
Asset thumbnail / type placeholder (image, music, file icon)
Asset name
File size (or “Link” for external assets)
Secondary Information
Asset type / usage context (used by the picker and editors)
Sort control (Alphabetical, Date Added) and Archive link
Archive page: days-remaining badge per asset
Tertiary Information (Hidden until needed)
file_path/file_url(Storage or external URL)asset_type/sourceInternal asset IDs used by the API
Actions
Primary CTA: Upload Asset / Add External Link
Secondary Actions: Rename (pencil), Archive (box), Delete Permanently (trash), Restore, bulk select / Select All / Deselect All
6. WIREFRAMES
No standalone wireframe artifact is stored for this feature.
Key Screens:
Background Assets dashboard (header: title, sort dropdown, Archive link, Upload Asset; grid of asset cards)
Upload Asset dialog (choose method → file picker or external link form)
Move to Archive confirmation modal
Permanently Delete File confirmation modal (typed
DELETE)Archive page (Cards with thumbnail, days-remaining badge, Restore / Delete Permanently)
Full-screen asset preview modal
Annotations:
Archive is a separate page reached from the dashboard header.
Bulk actions appear in a floating selection action bar once any asset is selected.
Destructive modals are the shared, standardized typed-confirmation component.
7. WIREFLOWS
Creator opens Background Assets → Uploads a file or adds an external link → asset appears in the grid → creator picks it from an editor’s “From Assets” picker → later archives it → asset moves to the Archive with a 120-day countdown → creator restores it or permanently deletes it with a typed DELETE.
8. PROTOTYPE
Figma Prototype Link: Not currently available for this feature.
How to test:
Open
/creator/background-assets.Click Upload Asset → Upload File, and upload an image, an MP3, an MP4 (under 5 min), and a PDF.
Add an external link (e.g. a YouTube URL) and confirm it appears as a card.
Rename one asset with the pencil action and confirm the name updates.
Archive one asset, open
/creator/archive, and confirm it shows with a days-remaining badge.Restore it from the Archive and confirm it returns to the library.
Archive another asset and permanently delete it by typing
DELETE.Open a quest editor, choose a background image → From Assets, and confirm the library assets are selectable.
9. DATA MODEL
Core Table
The feature depends on the public.asset_metadata table with the following concepts:
id— asset UUIDcreator_id— owner, the internalapp_users.id(not the auth UID)file_name— display name (editable)file_path/file_url/external_url— Storage path and public/external URLfile_type/file_size— MIME type and byte size (NULL for external)asset_type—IMAGE,VIDEO,AUDIO,DOCUMENT,BADGE, profile/cover contexts, orquest_*usage_context— e.g.BACKGROUND,BADGE,quest:profilesource—uploadedorexternalthumbnail_url— preview image (video/audio/document)deleted_at/expires_at— soft-delete and retention timestampdeletion_warning_sent_at— when the 30-day warning email was sentcreated_at/updated_at
Retention Shape
Archiving sets deleted_at = now and expires_at = now + 120 days. A scheduled job emails a warning 30 days before the 120-day boundary and a later job permanently purges assets whose deleted_at is older than 120 days. Retention is defined once in lib/archive/getDaysRemaining.ts (ARCHIVE_RETENTION_DAYS = 120).
Relationship to Users
Assets link to app_users by creator_id (logical relationship; there is no database foreign key). Visibility is resolved through agencyResourceFilter() — agency members see the agency’s assets, standalone creators see only their own.
Full column types, indexes, triggers, and RLS policies are in ITG §3–4 (Data Model / Schema).
10. API CONTRACTS
Creator — List Active Assets
Endpoint:
GET /api/creator/list-assetsBehavior: Returns non-deleted assets for the current creator/agency, newest first, normalized to
{ id, name, type, uploaded_at, url, thumbnail_url, source, file_size, asset_type, usage_context }.
Creator — List Archived Assets
Endpoint:
GET /api/creator/get-trash-assetsBehavior: Same shape as list-assets plus
deleted_at, ordered by deletion time.
Creator — Upload Asset
Endpoint:
POST /api/creator/upload-assetsRequest:
multipart/form-datawithfile,file_name,asset_type,usage_context, optionalthumbnail_file; or JSON withexternal_url,file_name,asset_type,usage_context.Behavior: Validates asset type/usage, MIME/extension, size, and (for video) duration; uploads to
public-assets; writesasset_metadataunlessusage_contextisBADGE; returns the created asset.
Creator — Rename Asset
Endpoint:
PATCH /api/creator/edit-assetsRequest:
{ "asset_id": "uuid", "new_name": "New name" }Behavior: Verifies ownership, then updates
file_name.
Creator — Archive Asset(s)
Endpoint:
DELETE /api/creator/delete-assetsRequest:
{ "asset_id": "uuid" }or{ "asset_ids": ["uuid", ...] }Behavior: Ownership check per asset; soft-deletes only non-archived rows and returns
archivedIds.
Creator — Restore Asset(s)
Endpoint:
PATCH /api/creator/restore-assetsRequest:
{ "asset_id": "uuid" }or{ "asset_ids": ["uuid", ...] }Behavior: Restores archived rows and returns
restoredIds,missingIds, andskippedIds.
Creator — Permanently Delete Asset(s)
Endpoint:
DELETE /api/creator/purge-assetsRequest:
{ "asset_id" | "asset_ids": ..., "confirm_text": "DELETE" }Behavior: Requires the literal
DELETE; deletes DB rows first, then removes their Storage objects (and the video thumbnail) for rows confirmed deleted.
System — Retention Jobs
GET /api/cron/check-pending-deletion— sends the 30-day warning email (requiresCRON_SECRET).GET /api/cron/purge-expired-archived— purges assets/quests/adventures older than 120 days (requiresCRON_SECRET).
Auth checks, exact error messages, and full request/response shapes are in ITG §3–4 (API Specification).
11. DATA REQUIREMENTS
Frontend Needs
The creator dashboard needs:
The list of active assets for the current creator/agency.
The list of archived assets with
deleted_atfor the countdown badge.Upload support for file and external-link modes.
Rename, archive, restore, and permanent-delete actions.
The ability to select the asset’s
asset_type/usage_contextso editors can filter the picker.
Picker Needs
Editor “From Assets” pickers need:
Active, non-archived assets only (archived assets must not be selectable).
asset_typeand the publicurl/thumbnail_url.
12. SECURITY & AUTHORIZATION
Who can access this feature?
Creator: ✔ (full CRUD on owned/agency assets)
Agency Owner / Agency member: ✔ (agency-scoped)
Admin: ✔
Reviewer: 👁 read-only (asset picker / review flows)
Learner: ✘
Authorization Logic
Every route calls
getCreatorAuthContext(), which requires an authenticated user with a platform role ofCREATOR,AGENCY, orADMIN.Reads are scoped by
agencyResourceFilter()— agency members see all agency users’ assets, standalone creators see only their own.Mutations call
verifyAgencyResourceOwnership()per selected asset before writing.Permanent deletion is gated by a typed
DELETEtoken that is re-validated server-side (Zod literal).Uploads are hardened against stored XSS: SVG is hard-rejected before storage, stored content types are server-derived rather than client-supplied, and the
public-assetsbucket carries a MIME allow-list that excludes SVG/HTML/XML/JS.asset_metadatais protected by Row Level Security; the latest policies resolve the caller throughapp_usersbyclerk_id = auth.uid().
Auth note: authentication is Supabase Auth (not Clerk). The legacy-named
app_users.clerk_idcolumn now stores the Supabase auth UUID. The app writes the internalapp_users.idintoasset_metadata.creator_id. See ITG §3–4.
13. ERROR HANDLING
Common Errors
Missing
asset_type/usage_context("Missing asset_type"/"Missing usage_context").Invalid usage context (
"Invalid usage_context: ...").MIME/extension not allow-listed (
"Invalid MIME type for <type>: <mime>").SVG upload (
"SVG files are not allowed").Size violations: video
"Video size must not exceed 25MB.", audio"Audio size must not exceed 10MB.", document"Document size must not exceed 20MB.", image"Image size must not exceed 10MB.".Video duration (
"Video exceeds 5 minutes.").Asset not found (
"Assets not found","Asset not found or not authorized").Already archived (
"Assets are already archived") / not archived ("Assets are not archived").Missing IDs (
"No asset IDs provided").Bad purge payload (
"Invalid permanent purge request payload").Upload/DB failures (
"Failed to upload asset","Failed to save metadata").
Handling Guidance
Return clear, size/type-specific messages so the client toast is actionable.
Keep archive reversible and reserve the typed
DELETEfor permanent deletion only.Re-validate every destructive token server-side; never trust the client modal.
On upload failure after Storage succeeds, reconcile the orphaned object manually until cleanup is added.
14. TESTING CHECKLIST
Happy Path
Upload an image, audio, MP4 (< 5 min), and PDF; each appears in the library.
Add an external link and confirm it appears with a placeholder/preview.
Rename an asset and confirm the new name persists after reload.
Attach an asset from the editor’s “From Assets” picker.
Archive an asset; confirm it leaves the library and appears in the Archive with a countdown.
Restore an archived asset; confirm it returns to the library.
Permanently delete an asset by typing
DELETE; confirm it is gone after reload.Bulk select and archive/delete multiple assets.
Edge Cases
Oversized file is rejected with the correct type-specific message.
Video over 5 minutes is rejected.
Non-MP4 video is rejected.
An SVG renamed to a raster extension is rejected.
Re-archiving an archived asset returns “already archived”.
Restoring an active asset returns “not archived”.
Permanent delete is blocked until
DELETEis typed exactly; wrong text shows “Text does not match”.Archived assets do not appear in the editor’s “From Assets” picker.
A direct API call with an oversized/typed payload is still rejected server-side.
15. OPEN QUESTIONS
For Product
Should assets be organizable into folders or shared across creators? — Still open.
Should the library surface storage usage / quotas per creator or agency? — Still open.
Should badge images become first-class assets rather than reference-only uploads? — Still open.
For Engineering
Should orphaned Storage objects be cleaned up when a metadata insert fails? — Still open (known gap).
Should the purge job retry failed Storage cleanups instead of logging and continuing? — Still open.
Should
expires_atdrive purge instead of a recomputeddeleted_atcutoff? — Still open.Should the duplicate
get-assetsendpoint be removed in favor oflist-assets? — Still open.Should the archive/permanent-delete E2E specs be refreshed to the current UI copy and 120-day window? — Still open (tests currently expect “Move to Trash” / “30d left”).
16. OUT OF SCOPE (v1.1+)
Folder organization and cross-creator asset sharing — still out of scope.
Content-hash deduplication and per-creator storage quotas — still out of scope.
Asset versioning / history — still out of scope.
Reviewer or learner asset management (read-only picker only) — still out of scope.
Paid/seat-based limits on library size — still out of scope.
17. SUCCESS METRICS
Assets uploaded per active creator.
Asset reuse rate (assets referenced by more than one quest/activity).
Archive → restore rate (how often creators recover assets).
Permanent-deletion volume vs. archive volume.
Upload failure rate by type (size/MIME/duration).
Storage reclaimed by the 120-day auto-purge.
18. DEPENDENCIES
This feature depends on
The
asset_metadatadata model.The
public-assetsSupabase Storage bucket and its MIME allow-list.The creator authentication layer (Supabase Auth) and agency ownership helpers.
The shared upload validators (
lib/uploadAssets/*).The shared typed-confirmation modal (
ConfirmDestructiveModal).The retention/purge jobs and the email service (deletion warnings).
These features depend on this
Quest/Adventure editors’ “From Assets” media pickers.
Activity Cards banner/image selection.
Badge image upload (reference-only, intentionally not in the library).
Content deletion flows that clean up referenced
asset_metadatarows and Storage objects.
19. TIMELINE & OWNERSHIP
Implementation Ownership
Original owner: Mich-Tapawan — original Background Assets / Asset Library build (Aug 2025), with later maintenance by Christian Denzon (bulk operations, archive/120-day purge, sorting, M4 hardening), Patrick Babala (PDF/DOCUMENT, video duration, external links, confirmation modal), Jethro Lagmay (soft-delete/trash), scorevi (migration/API standardization), and clydetims (RLS →
auth.uid()).Current owner / updater: Patrick Babala — this Hand-Off revision.
QA: Not enumerated in this session — Unknown / TBD — needs product input.
Shipped: core library (2025-08); archive + 120-day purge, bulk operations, sorting, PDF/DOCUMENT, duration cap, and upload hardening (2026).
Estimated Completion: core complete; remaining items are the Open Questions and Out of Scope sections above.
REFERENCES
Issue:
[3.1] Asset Library(Feature Audit May 2026) — GitHub issue number not confirmable in this session.Related features:
[3.6] Archive Asset,[3.7] Secure Asset Delete.Related issues: #437 (Trash Bin soft-delete), #777 (typed-DELETE confirmation).
PRs: #695 (audio background-asset upload fix); #711, #723, #725, #726, #770 (
feature/3.6-archive-asset— archive, bulk operations, sorting, 120-day purge); #867 (SVG upload stored-XSS hardening).Security ticket: M4 (#852) — SVG stored XSS via the upload flow.
Companion ITG:
Internal-Technical-Guide-Asset-Library.md.Offline limitation: the
[3.1]issue number and QA owner could not be resolved without GitHub access.
VERSION HISTORY
1.0 — 2026-10-01 — Patrick Babala. Initial Hand-Off for the Asset Library (Background Assets): product summary, codebase assessment, 4D mapping, user flows, information architecture, data model, API contracts, security/authorization, error handling, testing checklist, open questions, out-of-scope, metrics, dependencies, ownership, and references. Reconstructed from the current codebase and git history.