Author: Patrick Miguel M. Babala
Developers & Reviewers: Clyde Ador, Angelo Rico Alipit, Patrick Babala, Sean Caintic, Onise Cortez, Christian Denzon, Jethro Lagmay, Mich Tapawan, and Joshua Uriel Tribiana
Creation Date: September 29, 2026
Status: Published
References: Feature Audit ID 3.1 (Asset Library), Feature Audit ID 3.6 (Archive Asset), Feature Audit ID 3.7 (Secure Asset Delete), Issue #437, Issue #777, Issue #852/M4, Issue #139, supabase/migrations/20260424053322_create_asset_metadata_table.sql, supabase/migrations/20260424052731_soft_delete_optional_purge_columns.sql, supabase/migrations/20260729_backfill_archived_at_and_warning_tracking.sql, supabase/migrations/20260813_add_document_asset_type.sql, supabase/migrations/20260911_supabase_rls_auth_uid.sql, supabase/migrations/20260923_set_public_assets_allowed_mime_types.sql, PR #770, PR #726, PR #725, PR #723, PR #711, PR #695, docs/FEATURE_AUDIT_MAY2026.md:79, docs/FEATURE_AUDIT_MAY2026.md:406
Introduction & Goals
Problem Summary
Creators need a reusable library of media (images, audio, video, PDFs, and external links) that can be attached to quests, adventures, and activity cards, without re-uploading the same file. The library must support ownership/agency scoping, soft-delete with retention, typed-confirmation permanent deletion, and safe storage of uploaded bytes on a public CDN bucket.
Goals & Non-Goals
Goals:
Store uploaded/external asset metadata in
public.asset_metadatawith per-creator / per-agency scoping (supabase/migrations/20260424053322_create_asset_metadata_table.sql:9).Provide upload, list, rename, archive (soft-delete), restore, and permanent-purge operations over HTTP (
app/api/creator/(background-assets)/*).Enforce server-side limits and MIME/extension allow-lists that cannot be bypassed by direct API calls (
lib/uploadAssets/validate.ts,lib/constants.ts:1-5).Retain archived assets for 120 days with an email warning 30 days before purge (
lib/archive/getDaysRemaining.ts:1,app/api/cron/check-pending-deletion/route.ts:7).Prevent executable content (SVG/HTML/XML/JS) from being stored on the public bucket (migration
20260923_set_public_assets_allowed_mime_types.sql).
Non-Goals:
Cross-creator sharing / folder permissions for assets (folders exist for quests/adventures, not for the asset library).
Per-asset RBAC beyond creator/agency ownership.
Asset versioning, dedup by content hash, or quota accounting.
Client-side uploads directly to Storage for this feature — assets go through the Next.js API route (the only signed-URL path is the separate quest-content-nodes flow).
Glossary
Term | Definition |
Asset | A row in |
Asset Library / Background Assets | The feature and |
Archive / Trash | Soft-delete state where |
Purge | Permanent delete of the DB row and its Storage object. |
Usage context | Free-ish string classifying where an asset is used, e.g. |
Internal user id |
|
High-Level Architecture
System Diagram
+----------------------------------------------------------------------------------------------------+| Browser || +--------------------------------+ +--------------------------------+ +----------------------+ || | /creator/background-assets | | /creator/archive (page.tsx) | | | || | (page.tsx) | +---------------+----------------+ | | || +---------------+----------------+ | | | || | | | | || | +---------------+ | | || | | | | | || +---------------v----------------+ | +------------v----------------+ | +----------------+ || | assetUploadingDialog.tsx | | | AssetList.tsx | | | (UI Components)| || +---------------+----------------+ | +-----------------------------+ | +----------------+ |+------------------|-------------------|-----------------------------------|-------------------------+ | | | v v |+--------------------------------------------------------------------------v-------------------------+| Next.js App Router API || +------------------------------------+ +------------------------------------+ || | POST /api/creator/upload-assets | | PATCH /api/creator/edit-assets | || | GET /api/creator/list-assets | | DELETE /api/creator/delete-assets | || | GET /api/creator/get-assets | | PATCH /api/creator/restore-assets | || | GET /api/creator/get-trash-assets| | DELETE /api/creator/purge-assets | || +------------------+-----------------+ +------------------+-----------------+ || | | || +------------------v----------------------------------------v-----------------+ || | Cron endpoints: | || | GET /api/cron/check-pending-deletion | || | GET /api/cron/purge-expired-archived | || +------------------+----------------------------------------+-----------------+ |+---------------------|----------------------------------------|-------------------------------------+ | | v v+----------------------------------------------------------------------------------------------------+| lib || uploadAssets/validate.ts uploadAssets/saveMetadataToDB.ts || uploadAssets/uploadFileToStorage.ts archive/purgeArchivedContent.ts || uploadAssets/probeMp4Duration.ts |+---------------------+----------------------------------------+-------------------------------------+ | | v v+----------------------------------------------------------------------------------------------------+| Supabase || +----------------------------+ +----------------------------+ +-------------------------------+ || | Postgres (asset_metadata) | | Storage (public-assets) | | Supabase Auth | || +----------------------------+ +----------------------------+ +-------------------------------+ |+----------------------------------------------------------------------------------------------------+
Technologies Used
Concern | Technology |
Framework | Next.js 16 App Router, React 19, TypeScript strict (route handlers under |
API client |
|
DB / Auth | Supabase Postgres + RLS, Supabase Auth (migrations; |
Validation | Zod v4 ( |
UI | Shadcn/UI + Tailwind v4 + |
Media probing | Hand-rolled MP4 box parser with no external dependencies ( |
Scheduling |
|
Tests | Jest for unit/API, Playwright for E2E ( |
Detailed Design & Implementation
Data Model / Schema
Table public.asset_metadata — created by supabase/migrations/20260424053322_create_asset_metadata_table.sql:9, amended by later migrations.
Column | Type | Nullable | Default | Notes / migration |
|
| NO |
| PK |
|
| NO | — | owner = |
|
| NO | — | user-visible name; editable |
|
| YES | — | Storage object path; |
|
| YES | — | public URL (uploaded or external) |
|
| YES | — | original third-party URL |
|
| YES | — | MIME type; |
|
| YES | — | bytes; |
|
| NO | — | constrained (see below) |
|
| YES | — | e.g. |
|
| NO |
|
|
|
| YES | — | preview, esp. for video/audio/document |
|
| YES |
| soft-delete timestamp |
|
| YES |
| written as |
|
| YES |
| added |
|
| NO |
| |
|
| NO |
| trigger-maintained |
+-----------------------------------+ +-----------------------------------+| APP_USERS | | ASSET_METADATA |+-----------------------------------+ +-----------------------------------+| id: uuid (PK) | 1 * | id: uuid (PK) || clerk_id: text +-------------+ creator_id: text / uuid (logical) || email: text | | file_name: text || agency_id: uuid | | file_path: text (nullable) |+-----------------------------------+ | file_url: text (nullable) | | external_url: text (nullable) | | file_type: text (nullable) | | file_size: integer (nullable) | | asset_type: text | | usage_context: text (nullable) | | source: text | | thumbnail_url: text (nullable) | | deleted_at: timestamptz (nullable)| | expires_at: timestamptz (nullable)| | deletion_warning_sent_at: tstz | | created_at: timestamptz | | updated_at: timestamptz | +-----------------------------------+
Composite keys: None. Primary key is
id(uuid).Foreign keys: There is no foreign key from
asset_metadata.creator_idtoapp_users.idin the migrations; the relationship is logical and enforced in application code.file_path/file_urlreference thepublic-assetsStorage bucket without a database foreign key.ON DELETE CASCADE: Not configured on
asset_metadatatables.Triggers:
trigger_update_asset_metadata_updated_atonpublic.asset_metadata(BEFORE UPDATE) executesupdate_asset_metadata_updated_at()to setNEW.updated_at = NOW()(20260424053322:46-57).RLS helper functions / policies: Policies in
20260911_supabase_rls_auth_uid.sql:175-204supersede theauth.jwt() ->> 'sub'policies in20260424053322:63-81. All four policies resolve the caller throughapp_usersbyclerk_id = auth.uid()::textand comparecreator_id::text:asset_metadata_select_own(SELECT):creator_id::text = (SELECT id::text FROM app_users WHERE clerk_id = auth.uid()::text)asset_metadata_insert_own(INSERT): sameasset_metadata_update_own(UPDATE): sameUSING+WITH CHECKasset_metadata_delete_own(DELETE): same
Note: Every API route uses the service-role client (lib/supabase.ts), which bypasses RLS. RLS protects against direct PostgREST access from a user JWT; the API's own ownership checks are the effective authorization boundary for the app.
Indexes:
asset_metadata_pkeyonididx_asset_metadata_creator_idoncreator_ididx_asset_metadata_deleted_atondeleted_atidx_asset_metadata_created_atoncreated_at DESCidx_asset_metadata_deletion_warningondeleted_atWHERE deleted_at IS NOT NULL AND deletion_warning_sent_at IS NULL
Asset Type Constraint:
The original inline constraint allowed only IMAGE, AUDIO, VIDEO, quest_profile, quest_mentor, adventure-profile. Migration 20260813_add_document_asset_type.sql:14-32 drops and re-adds it by name to also allow DOCUMENT, BADGE, adventure-cover, and any pattern matching quest_%.
API Specification
All endpoints are under the route group app/api/creator/(background-assets)/ — the parenthesised segment is omitted from the URL, so paths are /api/creator/<route>.
Every route calls getCreatorAuthContext() (lib/auth/agencyOwnership.ts:674), requiring an authenticated user whose platform role is CREATOR, AGENCY, or ADMIN (:610-661). Reads are scoped by agencyResourceFilter() (:270) — agency members see all agency users' assets, standalone users see only their own. Mutations additionally call verifyAgencyResourceOwnership(context, asset.creator_id) (:127) per selected asset.
Method & Path | Auth | Purpose | Body / Query |
|
| Active assets ( | Query: none |
|
| Legacy duplicate of | Query: none |
|
| Archived assets ( | Query: none |
|
| Upload a file or add an external link | Body: |
|
| Rename | Body: |
|
| Soft-delete (archive) | Body: |
|
| Un-archive | Body: |
|
| Permanent delete (DB + Storage) | Body: |
|
| Email 30-day warnings | Query: |
|
| Purge rows older than 120 days | Query: |
Note: When usage_context === "BADGE", the file is uploaded to Storage but no asset_metadata row is created (upload-assets/route.ts:194-218, :259-283), so badge images never appear in the library.
Logic & Workflows
File upload workflow:
getCreatorAuthContext()resolvescreator_id = authContext.userInternalId(upload-assets/route.ts:28-29).parseRequestDataparses multipart or JSON (parseRequestData.ts:14).validateAssetTypeAndUsagechecksasset_typeinALLOWED_ASSET_TYPESand validates the usage context (validate.ts:214).validateMimeTypeandvalidateFileTypeevaluate input: allow-listed MIME wins, extension allow-list acts as fallback, and SVG is hard-rejected first (validate.ts:52).Server-side size limits are enforced (
upload-assets/route.ts:70-94, mirroringlib/constants.ts:1-4).For
VIDEO,probeMp4Durationparsesmvhd/mdhdboxes; if duration exceeds 300 seconds, it returns400 "Video exceeds 5 minutes."(upload-assets/route.ts:96-108).uploadFileToStoragere-validates, reads bytes into aBuffer, and pins a server-derivedcontentType(uploadFileToStorage.ts:33-44).Optional video
thumbnail_fileis validated as a raster image and uploaded as aBufferwith pinned content type; on failure, the object is skipped and a placeholder is used (upload-assets/route.ts:133-176).Placeholder fallbacks are assigned for audio, video, or document types (
:179-187).saveMetadataToDBinserts the row intopublic.asset_metadataunlessusage_context === "BADGE"(:194-218).
Failure / rollback details: If
saveMetadataToDBfails after a successful Storage upload, the route returns500but the Storage object is not deleted, causing an orphaned object. There is no compensating delete in this route (unlikelib/uploadAssets/uploadMediaWithMetadata.ts:84-121, which performs best-effort cleanup). No retry or backoff is configured; failures are logged only.
External link workflow:
handleAddExternalLinkclassifies YouTube/Vimeo asVIDEO,.mp3/.wav/.oggasAUDIO, and all other URLs asIMAGE(uploadHandlers.ts:174-183).The route derives a name from
file_nameor the URL hostname (upload-assets/route.ts:241-243), picks a thumbnail viagetExternalThumbnailor a placeholder, and inserts a row defaultingsource = 'uploaded'withfile_type = 'external'andfile_path = null.
Archive (soft-delete) workflow:
DELETE /api/creator/delete-assetsvalidates the payload usingbulkAssetIdsSchema, queries matching rows, runsverifyAgencyResourceOwnershipper row, filters out already-archived rows, and setsdeleted_at = now()andexpires_at = now() + 120 daysin one.in("id", archivingIds)query (delete-assets/route.ts:37-71).Re-archiving returns
400 "Assets are already archived"; bulk calls reportarchivedIdsonly for rows that underwent a state transition.
Restore workflow:
PATCH /api/creator/restore-assetsvalidates payload IDs, checks ownership per row, updates rows wheredeleted_at IS NOT NULLby settingdeleted_at = nullandexpires_at = null, and reportsrestoredIds,missingIds, andskippedIds(restore-assets/route.ts:54-79).
Permanent purge workflow:
DELETE /api/creator/purge-assetsrequires the exact confirmation string"DELETE"validated bypurgeAssetsSchema(route.ts:39-45,components/shared/confirm-destructive-modal.tsx:53).Database rows are deleted first.
removeAssetFromStorageis called only for rows confirmed deleted in the database, removing the primary object and any matching<base>.jpgthumbnail for videos (purge-assets/route.ts:74-91,:7-30). Storage removal errors are logged ("Storage cleanup failed:") but do not fail the request.
Retention lifecycle (cron):
Warning notification:
check-pending-deletionqueries assets wheredeleted_at <= now() - 90 daysanddeletion_warning_sent_at IS NULL, groups them by creator, delivers emails viaEmailService, and marksdeletion_warning_sent_at = now()(check-pending-deletion/route.ts:7,:64-70,:211-219). If the email send throws, the timestamp is not stamped and the operation retries on the next run (:177-188).Automatic purge:
purge-expired-archivedcomputescutoff = now() - 120 days, invokespurgeExpiredAssetsin parallel with quests and adventures, deletes expired database rows, and removes corresponding Storage objects usingPromise.allSettled(purge-expired-archived/route.ts:36-44,purgeArchivedContent.ts:175-219).
Client display handling:
AssetListrenders image thumbnails with anonErrorfallback icon, a music placeholder for audio (isAudioAsset), and a document icon (AssetList.tsx:41-80).DisplayAssetswitches display mode byasset_type: image, document (<iframe sandbox="allow-same-origin allow-scripts">plus external link), and video/audio (YouTube embed, native<video>, or native<audio>) (DisplayAsset.tsx:96-184).
Infrastructure & Operations
Dependencies
Upstream:
Supabase Postgres: stores metadata in
public.asset_metadatavia the service-role client (lib/supabase.ts)Supabase Auth: provides user identity via
auth.uid()(legacy*_clerk_idcolumns remain)Environment configuration:
NEXT_PUBLIC_SUPABASE_URL,SUPABASE_SERVICE_ROLE_KEYVideo probe logic:
probeMp4Durationoperates with no external binary or package dependencies
Downstream:
Supabase Storage bucket
public-assets: stores raw media underbackground-assets/<creatorId>/{video,audio,document,image}/...EmailService: sends 30-day archive expiration notices (lib/email/emailService.ts)Scheduled tasks: requires
CRON_SECRETfor invoking/api/cron/check-pending-deletionand/api/cron/purge-expired-archived
Monitoring & Alerting
The feature relies on console log output; no dedicated metric dashboards or alarms are currently configured.
Symptom | Likely cause | Fix |
| Database connectivity failure or invalid query parameters | Check database health and Supabase connection pool. |
| Query execution failure on the legacy retrieval route | Migrate client to |
| Error querying archived asset records | Verify query filters on |
| Failed to update | Check for payload formatting or database write lock contention. |
| Failure during soft-delete archive update | Confirm asset ID exists and verify ownership permissions. |
| Failure during un-archive update | Ensure target IDs have |
| Failure during row deletion in database | Inspect database foreign key constraints or active transaction locks. |
| Storage object deletion call failed after DB deletion | Manually remove orphaned objects from the |
| General upload pipeline error | Inspect pipeline sub-step logs for validation or network errors. |
| Storage bucket rejected file upload | Verify bucket permissions, size quotas, and MIME type rules. |
| Metadata insertion failed after file upload | Check schema constraints; inspect Storage for orphaned files. |
| Error uploading generated or custom video thumbnail | Verify thumbnail format is an accepted raster image. |
| Video thumbnail failed raster image validation checks | Ensure thumbnail is a valid JPEG, PNG, or WebP file. |
| Warning email dispatch or automatic purge routine failed | Verify |
Circuit breaker: all DB calls funnel through lib/supabase.ts (checkCircuitBreaker / recordCircuitFailure) via getAgencyAuthContext and the shared client (lib/auth/agencyOwnership.ts:34, :58-68).
Deployment Plan
Migration order (fresh database):
supabase/migrations/20260424053322_create_asset_metadata_table.sqlsupabase/migrations/20260424052731_soft_delete_optional_purge_columns.sqlsupabase/migrations/20260729_backfill_archived_at_and_warning_tracking.sqlsupabase/migrations/20260813_add_document_asset_type.sqlsupabase/migrations/20260911_supabase_rls_auth_uid.sqlsupabase/migrations/20260923_set_public_assets_allowed_mime_types.sql
Note: The create migration timestamp (
...053322) sorts after...052731_soft_delete_optional_purge_columns.sql. On a strictly timestamp-ordered fresh apply, the soft-delete migration attempts to runALTER TABLEon a non-existent table. The create migration was added as a backfill (86d3a1bd) and already includesdeleted_at,expires_at, and indexing. Apply order must be manually reconciled or guarded with conditional column checks.
Bucket configuration: The Storage bucket allow-list in
20260923_set_public_assets_allowed_mime_types.sql:47-48must be applied by a maintainer viasupabase db pushor the Supabase dashboard to block SVG and XML uploads at the storage layer.Feature flags: No feature flags gate this functionality; it is enabled by default at
app/creator/background-assets/page.tsx.Environment variables: Ensure
CRON_SECRET,NEXT_PUBLIC_SUPABASE_URL,SUPABASE_SERVICE_ROLE_KEY, and email provider variables are set in production environments.Backfills:
deletion_warning_sent_atdefaults toNULLvia20260729:24; thearchived_atbackfill applies only to quests and adventures.
Testing & Quality Assurance
Test Strategy
Unit tests (Jest):
tests/unit/upload-file-to-storage.test.ts: ValidatesBufferrequest bodies, pinnedcontentTypeheaders acrossVIDEO,AUDIO,DOCUMENT, andIMAGE,.m4vextension fallbacks, and SVG rejections prior to storage.tests/unit/upload-validation.test.ts: VerifiesvalidateMimeTypeandvalidateFileTypehard rejection of SVG files, including extension-bypass attacks, and ensures raster and PDF files pass validation.tests/unit/resolve-upload-content-type.test.ts: Tests file extension to MIME type resolution inresolveUploadContentType.tests/unit/validate-raster-image.test.ts: Tests raster image validation invalidateRasterImageUpload.
API and route tests (Jest, Node environment):
tests/api/background-assets-thumbnail.test.ts: VerifiesPOST /api/creator/upload-assetsrejects SVG video thumbnails and stores valid PNG files as aBufferwithimage/png.tests/creator/api/background-assets/getAssets.test.ts: Tests retrieval of/api/creator/list-assets.tests/creator/api/background-assets/createAsset.test.ts: Tests file and external upload handlers.tests/creator/api/background-assets/deleteAsset.test.ts: Tests client calls toDELETE /api/creator/delete-assets.
E2E tests (Playwright):
tests/e2e/creator/archive-asset.spec.ts(QA-051): Verifies archiving an asset removes it from both the active library and the asset picker UI.tests/e2e/creator/secure-asset-delete.spec.ts(QA-045,QA-046): Tests permanent purge flows requiring the typedDELETEconfirmation string, ensuring submission remains disabled until typed correctly.
Known Limitations
Stale retention copy in migrations: Migration files
20260424053322:33,:90and comments in20260729state a 30-day retention window; application logic enforces 120 days (lib/archive/getDaysRemaining.ts:1).Stale image size error string:
lib/uploadAssets/uploadFileToStorage.ts:111returns"Image file size exceeds 5 MB limit.", butMAX_IMAGE_SIZE = 10 * 1024 * 1024(lib/constants.ts:3). The route error correctly states"Image size must not exceed 10MB."(upload-assets/route.ts:84).Orphaned Storage objects on database failure: If
saveMetadataToDBfails after file upload completes, no compensating deletion occurs, stranding the object in the Storage bucket (upload-assets/route.ts:194-218).Best-effort Storage purge:
purge-assetsandpurgeArchivedContentcatch and log Storage deletion errors without rolling back or retrying, potentially leaving unreferenced files in Storage.Write-only
expires_atcolumn: The column is written bydelete-assets/route.ts:61-67and cleared byrestore-assets, but never queried; purge logic determines eligibility usingdeleted_at + 120 days(purgeArchivedContent.ts:180-184).Layout-sensitive MP4 parser:
probeMp4Durationreturnsnullif boxes are ordered unexpectedly; the server passes parsing through silently, relying solely on client-side checks (uploadHandlers.ts:103).creator_idtype inconsistency: Database comments in20260911:178-179statecreator_idis defined asTEXTin migration files but deployed asUUIDin production. New raw SQL queries must explicitly castcreator_id::text.Duplicate listing endpoints:
get-assetsduplicateslist-assetslogic, and its type resolution falls back only toIMAGE,AUDIO, orVIDEO, resolving unpopulatedasset_typevalues onDOCUMENTrows tonull(get-assets/route.ts:29-38,list-assets/route.ts:30-35).Stale E2E test assertions:
archive-asset.spec.tslooks for UI string "Move to Trash" and "30d left", but current UI displays "Move to Archive" (DeleteAssetModal.tsx:39) with a 120-day retention period.secure-asset-delete.spec.ts:42-46also references "Move to Trash".Incomplete admin bypass on mutations: While
verifyCreatorAccessallowsADMINroles,verifyAgencyResourceOwnershiplacks a global admin bypass; administrators cannot edit assets belonging to independent creators outside their agency (lib/auth/agencyOwnership.ts:127-188).Test coverage gaps: There is no unit or route-level test coverage for
edit-assets,restore-assets, bulk operations, external link ingestion,DOCUMENTpreview rendering, or either cron endpoint.
Maintenance & Support
Troubleshooting
Symptom | Likely cause | Fix |
| Uploaded video exceeds | Compress the file below 25 MB ( |
| Video duration exceeds | Trim media length or re-encode video header tracks. |
| File container is not an MP4 format | Convert media container to MP4 ( |
| Uploaded audio exceeds | Compress audio bitrate below 10 MB limit. |
| Uploaded PDF exceeds | Optimize and compress PDF pages below 20 MB limit. |
| Uploaded image exceeds | Compress image file below 10 MB limit. |
| Stale validation string triggered in storage upload layer | Treat 10 MB as active limit; update string in |
| Upload attempted with SVG MIME type or | Reject file; SVG is blocked to prevent stored XSS attacks. |
| MIME type not present in allow-list | Validate MIME type against |
| Client omitted required multipart fields | Verify form submission includes both parameters. |
| Context string not present in | Provide |
| Asset IDs do not exist or were purged | Refresh client asset list to sync state. |
| Asset record already has | No-op; item is already in archive. |
| Attempted restore on asset with | No-op; item is already active. |
| Payload omitted | Provide at least one UUID in request body. |
| ID missing or user is outside creator/agency ownership scope | Verify ownership rights or matching |
| Malformed body failed Zod schema parsing | Ensure payload matches required UUID types and formats. |
| Confirmation token does not match literal | Pass |
| Database constraint or foreign key prevented deletion | Inspect PostgreSQL server logs for constraint violations. |
| Database failure during read query execution | Inspect |
| Insert into | Inspect DB logs; remove orphaned object from Storage manually. |
| Storage bucket rejected write operation | Verify bucket exists and file matches |
|
| Verify user authentication and database health. |
| Storage object deletion failed during asset purge | Manually delete object path in Supabase Storage bucket. |
| Server environment lacks | Set |
| Request header missing valid | Pass |
Changelog
September 28, 2026 — fix(security): de-combine remaining sandbox flags on
main(46a66e20).September 23, 2026 — fix(security): update allowed MIME types for
public-assetsto prevent XML uploads onmain(bd998ebb).September 23, 2026 — fix(security): pin remaining upload paths and reconcile MIME allow-list (M4) on
main(1510f8bb).September 23, 2026 — fix(security): close remaining SVG upload branches (M4) on
main(4b989c2d).September 11, 2026 — feat: update
asset_metadataRLS policies to canonicalauth.uid()convention onmain(4274ff28).September 11, 2026 — backfill script skips uuid
creator_idcolumns; clarifycreator_idhandling onmain(ca9059c1,d0377a97).September 04, 2026 — fix: defer badge image upload to badge save; address badge review findings on
main(7119e8ba,97c5fdb3).August 20, 2026 — feat: standardize destructive actions on shared
ConfirmDestructiveModalonmain(13373adb).August 18, 2026 — Merge PR
#770feature/3.6-archive-assetintomain(5bb0e374).August 17, 2026 — feat: sorting for archive/background assets; clear selection on re-sort, sync rename on
feature/3.6-archive-asset(5d3eeea3,70411755).August 17, 2026 — feat: purge expired archived content after 120 days on
feature/3.6-archive-asset(b813aaa8).August 14, 2026 — fix: rewrite
asset_typemigration by name; enforce upload limits server-side; harden migration onmain(3dcb3518,10a5bc93).August 13, 2026 — feat: add PDF (
DOCUMENT) asset support + video duration warning; PR review fixes onmain(6bf936f0,034c96c7).August 06, 2026 — fix: refine audio asset detection; align settings error response on
main(08c9512b).August 04, 2026 — feat: bulk asset operations (new schema for deletion/restoration/archiving) + selection action bar on
feature/3.6-archive-asset(22e7434c,9dab82ad,7de73df0).August 04, 2026 — fix: render music placeholder for audio; image load fallback on
main(7aed1333).August 03, 2026 — Merge PRs
#726,#725,#723feature/3.6-archive-assetintomain(600a2e3e,79d882ce,b0b3fe9f).July 29, 2026 — Merge PR
#711feature/3.6-archive-assetintomain(6743848a).July 28, 2026 — feat: archive functionality with 120-day auto-purge for quests, adventures, and background assets on
feature/3.6-archive-asset(ae533e9b).July 23, 2026 — Merge PR
#695fix/audio-bg-asset-uploadintomain(0b65b045).July 22, 2026 — fix: resolve server-side MIME rejection and silent overwrite on
main(e7225c6c).July 02, 2026 — feat: agency-scoped reviewer/creator roles with RLS policies on
main(ac5a4209).July 01, 2026 — fix: migrate remaining 13 routes from old auth; fix 2 frontend API paths on
main(3b6970f3).May 18, 2026 — perf: replace
SELECT *with explicit columns across all 36 API route instances onmain(ba4ca7b6).May 12, 2026 — refactor: use
Buttoncomponent and improve type safety onmain(d348c0e9).April 24, 2026 — chore(db): add missing initial
asset_metadatamigration; Sushi Standard fixes onmain(86d3a1bd,f536c340).April 23, 2026 — fix/refactor: soft-delete API, trash fetch, trash-bin UI, permanent delete, image preview on
main(c52d52a8,f394e2cd,d5fa6998,766c2847,ae9b1b8f,08c5fd56).March 27, 2026 — feat(3.7): secure asset delete with confirmation modal; mobile view support on
main(47cdbed6,6ddf1e52).March 04, 2026 — feat(W1.2.5): Visual Canvas import integration on
main(c089d735).January 20, 2026 — feature/W1.4 API standardization (
#139) onmain(fda6a09d).October 19, 2025 — color-theme application on
main(487775a8,bbc826c2).August 26, 2025 — integrated background images/audio select for question cards on
main(f5e51f2b).August 15, 2025 — video thumbnail deletion; external audio/video thumbnails; upload limits + Edit Asset on
main(13391773,6c7da958,9d0c9d1a).August 12, 2025 — Delete Asset; modularized Background Assets; video/audio player; image asset display on
main(f742ec73,95f2833c,efc762ee,bb15524f).August 08, 2025 — media type/size safety; modularized
upload-assetsAPI + preview cards onmain(c3306f63,f5277757).August 07, 2025 — media preview cards + custom titles; Background Assets frontend; external media links on
main(c8854098,543e7e36,3e9b06f6).August 04, 2025 — CRUD operations for Background Assets; file-metadata schema; file structure on
main(84bf3557,a8040ef2,7bbb777c).
Document version:
1.0 - Draft, Initial Technical Guide, 07/29/2026
1.1 - Published, Asset Library Technical Guide, 07/29/2026