Media
Media is an atomic, reusable binary-asset record that stores logos, avatars, and banners for companies and people in the Well platform. Each row carries a media_type enum, an optional human label, and the GCS asset location expressed either as a legacy url (deprecated) or a modern GCS path (preferred). Media rows are workspace-scoped via a nullable ManyToOne to Workspace. CompanyMedia and PersonMedia are independent pivot entities that each hold a ManyToOne reference to Media โ the Media entity itself declares no back-reference collections.
| Naming | Value |
|---|---|
| Object | Media |
Resource type (JSON:API type) | media |
| Collection / records root | media |
| REST base | /v1/medias |
| Entity class | Media |
API operations
| Operation | Method & path | Status |
|---|---|---|
| List | GET /v1/medias | โ Implemented |
| Retrieve | GET /v1/medias/{id} | โ Implemented |
| Create | POST /v1/medias | ๐ก Planned |
| Update | PATCH /v1/medias/{id} | ๐ก Planned |
| Delete | DELETE /v1/medias/{id} | ๐ก Planned |
Data model
Attributes
| Field | Type | Required | Constraints | Allowed values | Description |
|---|---|---|---|---|---|
| media_id | string, UUID, ๐ system | โ Yes | unique; default gen_random_uuid() | โ | Public identifier for the media asset. Generated by PostgreSQL on insert via gen_random_uuid(). This is the value exposed in the JSON:API id field. |
| media_type | string (enum) | โ Yes | NOT NULL; native PG enum media_type_enum in core_api schema | avatar, logo, banner | Category of the binary asset. avatar is used for person profile pictures; logo for company brand marks; banner for wide header images. Controls how the asset is rendered by cell components in the records table. |
| label | string | โช No | varchar(255) by MikroORM default, nullable | โ | Human-readable label for the asset, e.g. "Acme Corp primary logo" or "Profile photo โ Q1 2026". Optional free-text annotation; not used programmatically. |
| url | string | โช No | text, nullable; DEPRECATED โ NULL for all assets stored after the GCS migration | โ | Legacy absolute URL to the asset. Kept for backward compatibility with records created before the GCS storage migration. For all new assets this column is NULL; the asset is stored at path instead. The formatter's resolveMediaUrl() falls through to this field only when path is absent. |
| path | string | โช No | text, nullable | โ | GCS object path for the asset, e.g. workspaces/<workspace_id>/media/<media_id>.png. The API formatter (resolveMediaUrl() in media.formatter.ts) converts this path to a CDN URL via buildMediaCdnUrl(). Takes priority over the legacy url field when both are present. NULL on pre-migration records. |
| created_at | Date, ๐ system | โ Yes | timestamptz NOT NULL; initialized to new Date() as TypeScript default and also set by MikroORM onCreate lifecycle hook | โ | Timestamp when the media record was created. Immutable after insert. |
| updated_at | Date, ๐ system | โช No | timestamptz, nullable; set by MikroORM onCreate hook on insert AND onUpdate hook on every subsequent write; no TypeScript default initializer (unlike created_at) | โ | Timestamp of the last attribute update on this record. Set automatically by MikroORM on both create and update operations. |
| deleted_at | Date | โช No | timestamptz, nullable; NULL = active record | โ | Soft-delete timestamp. When non-NULL the record is considered logically deleted. All queries must filter deleted_at IS NULL. The composite index idx_media_workspace_deleted on (workspace_pk, deleted_at) is the hot path for Hasura permission filters. |
Relationships
| Name | Type | Required | Description |
|---|---|---|---|
| workspace | to-one (workspace) | โช No | The workspace this media asset belongs to. Nullable โ set to NULL on historic records predating the workspace-scoping migration. The FK carries ON DELETE SET NULL so workspace deletion orphans the asset rather than cascading a hard delete. Composite index idx_media_workspace_deleted on (workspace, deleted_at) accelerates the Hasura permission filter (workspace_pk = $1 AND deleted_at IS NULL). |
System-computed
- media_id: generated by PostgreSQL
gen_random_uuid()at INSERT time via@Property({ defaultRaw: 'gen_random_uuid()' }). Never set by application code. - created_at: initialized to
new Date()as a TypeScript class field default AND set by MikroORMonCreate: () => new Date()lifecycle hook. Always populated. - updated_at: set by MikroORM
onCreateandonUpdatelifecycle hooks. No TypeScript default initializer โ the field isupdated_at?: Date(optional). NULL on records that have never been updated after initial creation. - deleted_at: soft-delete sentinel. NULL = active. Application must never issue hard DELETEs against the media table; set
deleted_at = NOW()instead. - URL resolution: the public-facing
urlattribute in the JSON:API response is NOT the rawmedia.urlcolumn. The formatter callsresolveMediaUrl(media)which returnsbuildMediaCdnUrl(media.path)whenpathis present, and falls back tomedia.urlotherwise. Consumers must never readmedia.urlormedia.pathdirectly โ always use the resolvedurlfrom the formatted response. - Workspace scoping: added retroactively by Migration20260105120000. Existing unscoped media was reassigned to workspaces via a data migration that de-duplicated assets shared across multiple workspaces by inserting new media rows per workspace.
- Deprecated url column: the
urlcolumn was originally NOT NULL (see Migration20250919154301 DDL). It was later made nullable by the GCS migration; all new records store the asset in GCS and leaveurlNULL. - Composite records-table field
composite_media_list: built fromcompany_media.media.media_id,company_media.media.label, andcompany_media.media.media_type(for companies) and the analogousperson_media.*paths (for people). Defined in composites.yml; rendered by the records table as a media-list composite cell. - Primary media computed field: the PostgreSQL function
core_api.company_primary_media(company_row, hasura_session)selects the most-recently-created non-deleted CompanyMedia row for a company and returns a JSONB projection includingmedia_id,media_type,label,url, andpath. The function was updated by Migration20260527120000 to includepathalongside the legacyurlso the data-views layer can derive a CDN URL for logos stored in GCS. - Pivot relationships: CompanyMedia and PersonMedia each declare a @ManyToOne to Media on their own entity classes. The Media entity itself declares NO @OneToMany back-references โ navigation from Media to its pivot rows is done via Hasura array relationships (metadata-level), not via MikroORM collections.
Example
{"type":"media","id":"b2f7c3d1-09e4-4a7b-8f56-3c2d1e0a9b7f","attributes":{"media_type":"logo","label":"Acme Corp primary logo","url":null,"path":"workspaces/4e9d7c2b-11aa-4b38-9e1f-7a3f5d6c8b20/media/b2f7c3d1-09e4-4a7b-8f56-3c2d1e0a9b7f.png","created_at":"2026-03-14T10:22:00.000Z","updated_at":"2026-04-01T08:45:00.000Z","deleted_at":null},"relationships":{"workspace":{"data":{"type":"workspace","id":"4e9d7c2b-11aa-4b38-9e1f-7a3f5d6c8b20"}}}}apps/api/src/database/entities/Media.ts ยท domain: financial-graph ยท tier: Supporting