> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wellapp.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Media

> Media is an atomic, reusable binary-asset record that stores logos, avatars, and banners for companies and people in the Well platform

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/&lt;workspace_id&gt;/media/&lt;media_id&gt;.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 MikroORM `onCreate: () => new Date()` lifecycle hook. Always populated.
* updated\_at: set by MikroORM `onCreate` and `onUpdate` lifecycle hooks. No TypeScript default initializer — the field is `updated_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 `url` attribute in the JSON:API response is NOT the raw `media.url` column. The formatter calls `resolveMediaUrl(media)` which returns `buildMediaCdnUrl(media.path)` when `path` is present, and falls back to `media.url` otherwise. Consumers must never read `media.url` or `media.path` directly — always use the resolved `url` from 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 `url` column 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 leave `url` NULL.
* Composite records-table field `composite_media_list`: built from `company_media.media.media_id`, `company_media.media.label`, and `company_media.media.media_type` (for companies) and the analogous `person_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 including `media_id`, `media_type`, `label`, `url`, and `path`. The function was updated by Migration20260527120000 to include `path` alongside the legacy `url` so 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

```json theme={null}
{"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"}}}}
```

<sub>Source: `apps/api/src/database/entities/Media.ts` · domain: financial-graph · tier: Supporting</sub>
