Asset Library

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_metadata with 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_metadata plus its optional Storage object.

Asset Library / Background Assets

The feature and /creator/background-assets UI.

Archive / Trash

Soft-delete state where deleted_at IS NOT NULL; UI route /creator/archive.

Purge

Permanent delete of the DB row and its Storage object.

Usage context

Free-ish string classifying where an asset is used, e.g. BACKGROUND, BADGE, quest:profile (lib/constants.ts:51-60).

Internal user id

app_users.id (a UUID) written into asset_metadata.creator_id — not the auth UID (lib/auth/agencyOwnership.ts:74-76).


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 app/api/**/route.ts)

API client

@supabase/supabase-js service-role client server-side (lib/supabase.ts uses SUPABASE_SERVICE_ROLE_KEY)

DB / Auth

Supabase Postgres + RLS, Supabase Auth (migrations; lib/auth/supabase-server.ts referenced by lib/auth/agencyOwnership.ts:2)

Validation

Zod v4 (shared/schemas/contentManagementSchema.ts:76, :96)

UI

Shadcn/UI + Tailwind v4 + lucide-react + sonner (components/creator/background-assets/*, components/ui/dialog.tsx)

Media probing

Hand-rolled MP4 box parser with no external dependencies (lib/uploadAssets/probeMp4Duration.ts:35)

Scheduling

CRON_SECRET-gated routes + external scheduled workflow (app/api/cron/*/route.ts)

Tests

Jest for unit/API, Playwright for E2E (tests/unit/*, tests/api/*, tests/e2e/creator/*)

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

id

uuid

NO

gen_random_uuid()

PK

creator_id

text (repo) / uuid (deployed per RLS comment)

NO

—

owner = app_users.id; CHECK (creator_id != '')

file_name

text

NO

—

user-visible name; editable

file_path

text

YES

—

Storage object path; NULL for external

file_url

text

YES

—

public URL (uploaded or external)

external_url

text

YES

—

original third-party URL

file_type

text

YES

—

MIME type; "external" for links

file_size

integer

YES

—

bytes; NULL for external

asset_type

text

NO

—

constrained (see below)

usage_context

text

YES

—

e.g. BACKGROUND, BADGE, quest:profile

source

text

NO

'uploaded'

CHECK (source IN ('uploaded','external'))

thumbnail_url

text

YES

—

preview, esp. for video/audio/document

deleted_at

timestamptz

YES

NULL

soft-delete timestamp

expires_at

timestamptz

YES

NULL

written as deleted_at + 120d by the delete route; not read by purge

deletion_warning_sent_at

timestamptz

YES

NULL

added 20260729_backfill_archived_at_and_warning_tracking.sql:24

created_at

timestamptz

NO

NOW()

updated_at

timestamptz

NO

NOW()

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_id to app_users.id in the migrations; the relationship is logical and enforced in application code. file_path/file_url reference the public-assets Storage bucket without a database foreign key.

  • ON DELETE CASCADE: Not configured on asset_metadata tables.

  • Triggers: trigger_update_asset_metadata_updated_at on public.asset_metadata (BEFORE UPDATE) executes update_asset_metadata_updated_at() to set NEW.updated_at = NOW() (20260424053322:46-57).

  • RLS helper functions / policies: Policies in 20260911_supabase_rls_auth_uid.sql:175-204 supersede the auth.jwt() ->> 'sub' policies in 20260424053322:63-81. All four policies resolve the caller through app_users by clerk_id = auth.uid()::text and compare creator_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): same

    • asset_metadata_update_own (UPDATE): same USING + WITH CHECK

    • asset_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_pkey on id

  • idx_asset_metadata_creator_id on creator_id

  • idx_asset_metadata_deleted_at on deleted_at

  • idx_asset_metadata_created_at on created_at DESC

  • idx_asset_metadata_deletion_warning on deleted_at WHERE 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

GET /api/creator/list-assets

creator / agency / admin

Active assets (deleted_at IS NULL)

Query: none

GET /api/creator/get-assets

creator / agency / admin

Legacy duplicate of list-assets

Query: none

GET /api/creator/get-trash-assets

creator / agency / admin

Archived assets (deleted_at IS NOT NULL)

Query: none

POST /api/creator/upload-assets

creator / agency / admin

Upload a file or add an external link

Body: multipart/form-data or JSON payload

PATCH /api/creator/edit-assets

creator / agency / admin (owner or same agency)

Rename file_name

Body: { "asset_id": "<uuid>", "file_name": "<string>" }

DELETE /api/creator/delete-assets

creator / agency / admin (owner or same agency)

Soft-delete (archive)

Body: { "asset_id": "<uuid>" } or { "asset_ids": ["<uuid>"] }

PATCH /api/creator/restore-assets

creator / agency / admin (owner or same agency)

Un-archive

Body: { "asset_id": "<uuid>" } or { "asset_ids": ["<uuid>"] }

DELETE /api/creator/purge-assets

creator / agency / admin (owner or same agency)

Permanent delete (DB + Storage)

Body: { "asset_id": "<uuid>", "confirm_text": "DELETE" }

GET /api/cron/check-pending-deletion

Bearer $CRON_SECRET or ?secret=

Email 30-day warnings

Query: secret=<CRON_SECRET> (optional if header present)

GET /api/cron/purge-expired-archived

Bearer $CRON_SECRET or ?secret=

Purge rows older than 120 days

Query: secret=<CRON_SECRET> (optional if header present)

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

  1. File upload workflow:

    1. getCreatorAuthContext() resolves creator_id = authContext.userInternalId (upload-assets/route.ts:28-29).

    2. parseRequestData parses multipart or JSON (parseRequestData.ts:14).

    3. validateAssetTypeAndUsage checks asset_type in ALLOWED_ASSET_TYPES and validates the usage context (validate.ts:214).

    4. validateMimeType and validateFileType evaluate input: allow-listed MIME wins, extension allow-list acts as fallback, and SVG is hard-rejected first (validate.ts:52).

    5. Server-side size limits are enforced (upload-assets/route.ts:70-94, mirroring lib/constants.ts:1-4).

    6. For VIDEO, probeMp4Duration parses mvhd/mdhd boxes; if duration exceeds 300 seconds, it returns 400 "Video exceeds 5 minutes." (upload-assets/route.ts:96-108).

    7. uploadFileToStorage re-validates, reads bytes into a Buffer, and pins a server-derived contentType (uploadFileToStorage.ts:33-44).

    8. Optional video thumbnail_file is validated as a raster image and uploaded as a Buffer with pinned content type; on failure, the object is skipped and a placeholder is used (upload-assets/route.ts:133-176).

    9. Placeholder fallbacks are assigned for audio, video, or document types (:179-187).

    10. saveMetadataToDB inserts the row into public.asset_metadata unless usage_context === "BADGE" (:194-218).

    • Failure / rollback details: If saveMetadataToDB fails after a successful Storage upload, the route returns 500 but the Storage object is not deleted, causing an orphaned object. There is no compensating delete in this route (unlike lib/uploadAssets/uploadMediaWithMetadata.ts:84-121, which performs best-effort cleanup). No retry or backoff is configured; failures are logged only.

  2. External link workflow:

    • handleAddExternalLink classifies YouTube/Vimeo as VIDEO, .mp3/.wav/.ogg as AUDIO, and all other URLs as IMAGE (uploadHandlers.ts:174-183).

    • The route derives a name from file_name or the URL hostname (upload-assets/route.ts:241-243), picks a thumbnail via getExternalThumbnail or a placeholder, and inserts a row defaulting source = 'uploaded' with file_type = 'external' and file_path = null.

  3. Archive (soft-delete) workflow:

    • DELETE /api/creator/delete-assets validates the payload using bulkAssetIdsSchema, queries matching rows, runs verifyAgencyResourceOwnership per row, filters out already-archived rows, and sets deleted_at = now() and expires_at = now() + 120 days in one .in("id", archivingIds) query (delete-assets/route.ts:37-71).

    • Re-archiving returns 400 "Assets are already archived"; bulk calls report archivedIds only for rows that underwent a state transition.

  4. Restore workflow:

    • PATCH /api/creator/restore-assets validates payload IDs, checks ownership per row, updates rows where deleted_at IS NOT NULL by setting deleted_at = null and expires_at = null, and reports restoredIds, missingIds, and skippedIds (restore-assets/route.ts:54-79).

  5. Permanent purge workflow:

    • DELETE /api/creator/purge-assets requires the exact confirmation string "DELETE" validated by purgeAssetsSchema (route.ts:39-45, components/shared/confirm-destructive-modal.tsx:53).

    • Database rows are deleted first.

    • removeAssetFromStorage is called only for rows confirmed deleted in the database, removing the primary object and any matching <base>.jpg thumbnail for videos (purge-assets/route.ts:74-91, :7-30). Storage removal errors are logged ("Storage cleanup failed:") but do not fail the request.

  6. Retention lifecycle (cron):

    • Warning notification: check-pending-deletion queries assets where deleted_at <= now() - 90 days and deletion_warning_sent_at IS NULL, groups them by creator, delivers emails via EmailService, and marks deletion_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-archived computes cutoff = now() - 120 days, invokes purgeExpiredAssets in parallel with quests and adventures, deletes expired database rows, and removes corresponding Storage objects using Promise.allSettled (purge-expired-archived/route.ts:36-44, purgeArchivedContent.ts:175-219).

  7. Client display handling:

    • AssetList renders image thumbnails with an onError fallback icon, a music placeholder for audio (isAudioAsset), and a document icon (AssetList.tsx:41-80).

    • DisplayAsset switches display mode by asset_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_metadata via the service-role client (lib/supabase.ts)

  • Supabase Auth: provides user identity via auth.uid() (legacy *_clerk_id columns remain)

  • Environment configuration: NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY

  • Video probe logic: probeMp4Duration operates with no external binary or package dependencies

Downstream:

  • Supabase Storage bucket public-assets: stores raw media under background-assets/<creatorId>/{video,audio,document,image}/...

  • EmailService: sends 30-day archive expiration notices (lib/email/emailService.ts)

  • Scheduled tasks: requires CRON_SECRET for invoking /api/cron/check-pending-deletion and /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

GET /list-assets Error:

Database connectivity failure or invalid query parameters

Check database health and Supabase connection pool.

GET /api/get-assets Error:

Query execution failure on the legacy retrieval route

Migrate client to list-assets and verify database health.

GET /get-trash-assets Error:

Error querying archived asset records

Verify query filters on deleted_at and connection state.

PATCH /edit-assets Error:

Failed to update file_name in database

Check for payload formatting or database write lock contention.

Soft Delete Error:

Failure during soft-delete archive update

Confirm asset ID exists and verify ownership permissions.

Restore Asset Error:

Failure during un-archive update

Ensure target IDs have deleted_at populated.

Purge Asset Error:

Failure during row deletion in database

Inspect database foreign key constraints or active transaction locks.

Storage cleanup failed:

Storage object deletion call failed after DB deletion

Manually remove orphaned objects from the public-assets bucket.

POST /upload-assets error:

General upload pipeline error

Inspect pipeline sub-step logs for validation or network errors.

File upload error:

Storage bucket rejected file upload

Verify bucket permissions, size quotas, and MIME type rules.

DB insert error: / DB insert error (external link):

Metadata insertion failed after file upload

Check schema constraints; inspect Storage for orphaned files.

Failed to upload thumbnail:

Error uploading generated or custom video thumbnail

Verify thumbnail format is an accepted raster image.

Rejected video thumbnail:

Video thumbnail failed raster image validation checks

Ensure thumbnail is a valid JPEG, PNG, or WebP file.

[cron] ...

Warning email dispatch or automatic purge routine failed

Verify CRON_SECRET, EmailService status, and cron schedule.

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

  1. Migration order (fresh database):

    1. supabase/migrations/20260424053322_create_asset_metadata_table.sql

    2. supabase/migrations/20260424052731_soft_delete_optional_purge_columns.sql

    3. supabase/migrations/20260729_backfill_archived_at_and_warning_tracking.sql

    4. supabase/migrations/20260813_add_document_asset_type.sql

    5. supabase/migrations/20260911_supabase_rls_auth_uid.sql

    6. supabase/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 run ALTER TABLE on a non-existent table. The create migration was added as a backfill (86d3a1bd) and already includes deleted_at, expires_at, and indexing. Apply order must be manually reconciled or guarded with conditional column checks.

  2. Bucket configuration: The Storage bucket allow-list in 20260923_set_public_assets_allowed_mime_types.sql:47-48 must be applied by a maintainer via supabase db push or the Supabase dashboard to block SVG and XML uploads at the storage layer.

  3. Feature flags: No feature flags gate this functionality; it is enabled by default at app/creator/background-assets/page.tsx.

  4. Environment variables: Ensure CRON_SECRET, NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, and email provider variables are set in production environments.

  5. Backfills: deletion_warning_sent_at defaults to NULL via 20260729:24; the archived_at backfill applies only to quests and adventures.


Testing & Quality Assurance

Test Strategy

  • Unit tests (Jest):

    • tests/unit/upload-file-to-storage.test.ts: Validates Buffer request bodies, pinned contentType headers across VIDEO, AUDIO, DOCUMENT, and IMAGE, .m4v extension fallbacks, and SVG rejections prior to storage.

    • tests/unit/upload-validation.test.ts: Verifies validateMimeType and validateFileType hard 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 in resolveUploadContentType.

    • tests/unit/validate-raster-image.test.ts: Tests raster image validation in validateRasterImageUpload.

  • API and route tests (Jest, Node environment):

    • tests/api/background-assets-thumbnail.test.ts: Verifies POST /api/creator/upload-assets rejects SVG video thumbnails and stores valid PNG files as a Buffer with image/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 to DELETE /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 typed DELETE confirmation string, ensuring submission remains disabled until typed correctly.

Known Limitations

  • Stale retention copy in migrations: Migration files 20260424053322:33, :90 and comments in 20260729 state a 30-day retention window; application logic enforces 120 days (lib/archive/getDaysRemaining.ts:1).

  • Stale image size error string: lib/uploadAssets/uploadFileToStorage.ts:111 returns "Image file size exceeds 5 MB limit.", but MAX_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 saveMetadataToDB fails 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-assets and purgeArchivedContent catch and log Storage deletion errors without rolling back or retrying, potentially leaving unreferenced files in Storage.

  • Write-only expires_at column: The column is written by delete-assets/route.ts:61-67 and cleared by restore-assets, but never queried; purge logic determines eligibility using deleted_at + 120 days (purgeArchivedContent.ts:180-184).

  • Layout-sensitive MP4 parser: probeMp4Duration returns null if boxes are ordered unexpectedly; the server passes parsing through silently, relying solely on client-side checks (uploadHandlers.ts:103).

  • creator_id type inconsistency: Database comments in 20260911:178-179 state creator_id is defined as TEXT in migration files but deployed as UUID in production. New raw SQL queries must explicitly cast creator_id::text.

  • Duplicate listing endpoints: get-assets duplicates list-assets logic, and its type resolution falls back only to IMAGE, AUDIO, or VIDEO, resolving unpopulated asset_type values on DOCUMENT rows to null (get-assets/route.ts:29-38, list-assets/route.ts:30-35).

  • Stale E2E test assertions: archive-asset.spec.ts looks 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-46 also references "Move to Trash".

  • Incomplete admin bypass on mutations: While verifyCreatorAccess allows ADMIN roles, verifyAgencyResourceOwnership lacks 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, DOCUMENT preview rendering, or either cron endpoint.


Maintenance & Support

Troubleshooting

Symptom

Likely cause

Fix

Video size must not exceed 25MB.

Uploaded video exceeds MAX_VIDEO_SIZE (25 MB)

Compress the file below 25 MB (lib/constants.ts:1).

Video exceeds 5 minutes.

Video duration exceeds MAX_VIDEO_DURATION_SECONDS (300 s)

Trim media length or re-encode video header tracks.

Only MP4 video files are allowed.

File container is not an MP4 format

Convert media container to MP4 (video/mp4).

Audio size must not exceed 10MB.

Uploaded audio exceeds MAX_AUDIO_SIZE (10 MB)

Compress audio bitrate below 10 MB limit.

Document size must not exceed 20MB.

Uploaded PDF exceeds MAX_DOCUMENT_SIZE (20 MB)

Optimize and compress PDF pages below 20 MB limit.

Image size must not exceed 10MB.

Uploaded image exceeds MAX_IMAGE_SIZE (10 MB)

Compress image file below 10 MB limit.

Image file size exceeds 5 MB limit.

Stale validation string triggered in storage upload layer

Treat 10 MB as active limit; update string in uploadFileToStorage.ts:111.

SVG files are not allowed

Upload attempted with SVG MIME type or .svg extension

Reject file; SVG is blocked to prevent stored XSS attacks.

Invalid MIME type for <type>: <mime>

MIME type not present in allow-list

Validate MIME type against lib/constants.ts definitions.

Missing asset_type / Missing usage_context

Client omitted required multipart fields

Verify form submission includes both parameters.

Invalid usage_context: <v>

Context string not present in BASIC_USAGE_CONTEXTS

Provide BACKGROUND or prefix with quest: / adventure:.

Assets not found

Asset IDs do not exist or were purged

Refresh client asset list to sync state.

Assets are already archived

Asset record already has deleted_at IS NOT NULL

No-op; item is already in archive.

Assets are not archived

Attempted restore on asset with deleted_at IS NULL

No-op; item is already active.

No asset IDs provided

Payload omitted asset_id and asset_ids

Provide at least one UUID in request body.

Asset not found or not authorized

ID missing or user is outside creator/agency ownership scope

Verify ownership rights or matching agency_id.

Invalid request payload

Malformed body failed Zod schema parsing

Ensure payload matches required UUID types and formats.

Invalid permanent purge request payload

Confirmation token does not match literal "DELETE"

Pass "confirm_text": "DELETE" in JSON request body.

Failed to delete from database

Database constraint or foreign key prevented deletion

Inspect PostgreSQL server logs for constraint violations.

Failed to retrieve asset metadata

Database failure during read query execution

Inspect lib/supabase.ts circuit breaker status and retry.

Failed to save metadata

Insert into asset_metadata failed after file upload

Inspect DB logs; remove orphaned object from Storage manually.

Failed to upload asset

Storage bucket rejected write operation

Verify bucket exists and file matches allowed_mime_types.

Failed to load archive

GET /api/creator/get-trash-assets returned failure

Verify user authentication and database health.

Storage cleanup failed:

Storage object deletion failed during asset purge

Manually delete object path in Supabase Storage bucket.

CRON_SECRET not configured

Server environment lacks CRON_SECRET variable

Set CRON_SECRET in environment variables.

Unauthorized

Request header missing valid CRON_SECRET bearer token

Pass Authorization: Bearer $CRON_SECRET with request.

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-assets to prevent XML uploads on main (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_metadata RLS policies to canonical auth.uid() convention on main (4274ff28).

  • September 11, 2026 — backfill script skips uuid creator_id columns; clarify creator_id handling on main (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 ConfirmDestructiveModal on main (13373adb).

  • August 18, 2026 — Merge PR #770 feature/3.6-archive-asset into main (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_type migration by name; enforce upload limits server-side; harden migration on main (3dcb3518, 10a5bc93).

  • August 13, 2026 — feat: add PDF (DOCUMENT) asset support + video duration warning; PR review fixes on main (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, #723 feature/3.6-archive-asset into main (600a2e3e, 79d882ce, b0b3fe9f).

  • July 29, 2026 — Merge PR #711 feature/3.6-archive-asset into main (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 #695 fix/audio-bg-asset-upload into main (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 on main (ba4ca7b6).

  • May 12, 2026 — refactor: use Button component and improve type safety on main (d348c0e9).

  • April 24, 2026 — chore(db): add missing initial asset_metadata migration; Sushi Standard fixes on main (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) on main (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-assets API + preview cards on main (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


Was this article helpful?