Appearance
Asset Library — Backend Spec
Frontend is done (mock data). This spec describes what the backend needs to provide so we can swap mock data for real API calls.
What the frontend does NOT need from backend
Folder tree. The folder hierarchy (Enterprise → Brand → Campaign → Creative Group) is already built in the frontend from existing Vuex data (brands, campaigns, creative groups). No folder endpoints needed.
Data model
assets table
| Column | Type | Notes |
|---|---|---|
id | bigint / uuid | Primary key |
name | string | Original filename or user-defined name |
type | enum | image, font, video, url |
url | text, nullable | CDN URL. Null for url-type assets (those store just an alias) |
alias | string, nullable | Display name for url-type assets (e.g. "Google Sheets - Leads 2026") |
thumbnail | text, nullable | Thumbnail URL for images/videos |
workspace_id | bigint | FK to workspaces — all queries scoped to workspace |
scope_level | enum | enterprise, brand, campaign, creativeGroup |
scope_id | string | ID of the entity at that scope level |
metadata | json, nullable | See below |
created_at | timestamp | |
updated_at | timestamp |
metadata JSON structure
json
// Image
{ "width": 1280, "height": 720, "fileSize": 85000 }
// Font
{ "fontFamily": "Kristiania Display", "fileSize": 34000 }
// Video
{ "width": 1920, "height": 1080, "duration": 45, "fileSize": 8500000 }
// URL
{ "fileSize": null }Endpoints
1. List assets
GET /api/v1/workspaces/:workspaceId/assetsQuery params (all optional):
type— filter by asset type (image,font,video,url)scope_level— filter by scope levelscope_id— filter by specific scope entity
Response:
json
{
"data": [
{
"id": "asset-123",
"name": "campus-photo.jpg",
"type": "image",
"url": "https://delivery-6.cavai.com/assets/...",
"alias": null,
"thumbnail": "https://delivery-6.cavai.com/assets/.../thumb.jpg",
"scopeLevel": "brand",
"scopeId": "42",
"metadata": { "width": 1280, "height": 720, "fileSize": 85000 },
"createdAt": "2026-03-01T12:00:00Z",
"updatedAt": "2026-03-01T12:00:00Z"
}
]
}Frontend maps scopeLevel+scopeId to full scope info (name, level) using its own Vuex data.
2. Upload asset
POST /api/v1/workspaces/:workspaceId/assets
Content-Type: multipart/form-dataBody:
file— the file (image, font, video)type—image|font|videoscope_level—enterprise|brand|campaign|creativeGroupscope_id— ID of entity at that scope level
Response: single asset object (same shape as list items)
Notes:
- Existing
CreativeAssetsController.store()already handles image/font validation and Drive upload — this can be extended - Thumbnail generation for images: resize to ~200px width at upload time
- Video uploads: can use existing Bunny integration (
getBunnyVideoUploadToken)
3. Create URL-type asset
POST /api/v1/workspaces/:workspaceId/assets/url
Content-Type: application/jsonBody:
json
{
"alias": "Leads spreadsheet 2026",
"url": "https://docs.google.com/spreadsheets/d/...",
"scope_level": "campaign",
"scope_id": "15"
}Response: single asset object
4. Update asset (rename / move)
PATCH /api/v1/assets/:assetId
Content-Type: application/jsonBody (all optional):
json
{
"name": "new-name.jpg",
"scope_level": "campaign",
"scope_id": "15"
}Response: updated asset object
5. Delete asset
DELETE /api/v1/assets/:assetIdResponse: 204 No Content
Should also delete the file from storage (Drive / Bunny).
What already exists that can be reused
| Existing | Reuse for |
|---|---|
CreativeAssetsController.store() | File validation (types, max size), Drive upload logic |
Drive config (local + S3 disks) | Same storage backend |
DELIVERY_URL | Same base URL for serving assets |
Bunny.getUploadToken() | Video asset uploads |
@adonisjs/attachment-lite | Optional: thumbnail handling |
| Auth middleware | Same auth on all new routes |
workspace_id scoping | Same pattern as brands, campaigns, etc. |
Frontend integration
Only 3 Vuex actions need to change (in store/modules/assetLibrary.ts):
fetchAssetLibrary— swap mock data forGET /workspaces/:id/assetsuploadAssetToLibrary— swapURL.createObjectURL()forPOST /workspaces/:id/assetsdeleteAssetFromLibrary— addDELETE /assets/:idcall
Plus two new actions for rename (PATCH) and move (PATCH).
The folder tree, filtering, breadcrumb nav, grid/list views, and picker mode are all done and don't depend on backend.
Storage considerations
Assets should go to Bunny CDN (aligning with the ongoing migration). The url field in the response should be the final CDN URL that the creative engine can use directly — same pattern as existing image uploads.
Not needed for v1
- Asset usage tracking (which creatives use an asset)
- Bulk operations (multi-select, bulk move/delete)
- Tagging / search by metadata
- Asset versioning
These can be added later without schema changes (usage tracking would need a join table).