# Assets (/docs/guides/assets)

Assets are uploaded files: images, video, documents. They belong to a space and are shared by all its environments, so promoting content never copies files.

## Uploading [#uploading]

Upload in the **Media** library (media.obelism.studio, under Products in the sidebar), or from an asset field's picker in the entry editor. Files live in **folders** there; the picker opens the Space's folders too and uploads into the one it shows. Both list files newest first, a page at a time, and a search by file name looks in every folder. Agents upload with `POST …/assets` (multipart with a `file` field, or base64 JSON) through the [agent API](/docs/api/agents) or the `uploadAsset` MCP tool; a file too big for the call goes to `POST …/uploads/assets`, whose `uploadUrl` takes the bytes with a `PUT`, then `POST …/assets/from-upload`.

- Files uploaded from Content or by an agent are at most 50 MB; over 25 MB, Content warns that the file adds to your storage quota. The Media library uploads larger files in parts ([uploads](https://media.obelism.studio/docs/library/uploads)).
- The type is read from the file's bytes, not from its name or what the upload claims. A file claiming to be a browser-renderable image (PNG, JPEG, GIF, WebP, AVIF) whose bytes prove nothing image-like is refused.
- Images get their `width` and `height` measured — inline, multipart and signed uploads alike — for the `width` and `height` attributes on the site.
- Each asset has an **alt text** and a **title** per [locale](/docs/guides/locales). The delivery API answers them in the requested locale.

## Tags [#tags]

Give files **tags** in the **Media** library (comma separated, up to 20 per file) to find them again: the tags above the library filter it, with how many files have each. Tags are only for finding files; they aren't part of delivery responses. Agents set `tags` with `PATCH …/assets/{id}` and filter with `GET …/assets?tag=…`.

## Replacing a file [#replacing-a-file]

**Media → Replace file** puts a new file under an existing asset, so every entry using it shows the new one without being edited: a new logo, a corrected PDF. The new file must be the same kind (an image for an image, a PDF stays an application file), since asset fields may only allow some types. Alt text, title, focal point and tags stay; size and dimensions follow the new file.

The asset's `url` changes (`/assets/{id}/v2/{filename}`), because files are cached for a year: environments whose content uses the asset get a new content version and an `asset.updated` [webhook](/docs/guides/webhooks), so sites pick up the new URL. Old URLs keep working and serve the new file. Agents use `PUT …/assets/{id}/file` with the same body as uploading (multipart or base64 JSON; a `filename` field names the file).

## Using them [#using-them]

An `asset` field stores an asset's id (a list with `multiple`), optionally limited to some types with `mimeTypes` (`["image/"]`). Rich text embeds assets too. In delivery responses the id is replaced by the asset:

```json
{
	"id": "asset_…",
	"url": "https://content.obelism.studio/assets/asset_…/photo.jpg?w=1600&q=80",
	"filename": "photo.jpg",
	"mimeType": "image/jpeg",
	"size": 482113,
	"width": 1600,
	"height": 900,
	"title": "Our office",
	"alt": "The office from the canal side",
	"focalPoint": { "x": 0.42, "y": 0.3 }
}
```

`focalPoint` is where crops should keep the image (fractions of its width and height from the top left), or `null`. Set it by clicking the image in its details: open the file in the **Media** library, or click a picked file in the entry form, and crop with it as `focus` (see [images](/docs/api/images)).

### Alt text and focal point for one use [#alt-text-and-focal-point-for-one-use]

A file's alt text and focal point are its own, wherever it is used. One use can have its own instead: **For this use** on a picked file in the entry form, or in an asset field's value `{ "asset": "asset_…", "alt": { "en": "…" }, "focalPoint": { "x": 0.2, "y": 0.8 } }` (either key may be left out, and `alt` falls back to the file's in a language it doesn't name). In rich text, select an embedded file and choose **Alt** for its alt text there, or send `alt` and `focalPoint` on the `embeddedAsset` node. In Markdown, an image's alt text is the use's: `![Two people on a beach](asset:asset_…)`.

Under the alt text, in the media picker's pane, **For this use** and a file's details, **Suggest alt text** asks Obelism for one plain sentence describing the image in the language shown. **Keep it** puts it in the field, still unsaved; **Try another** asks again; **Dismiss** drops it. It takes an editor role and counts towards the same per-person limit as [prompt editing](/docs/guides/prompt-editing) (20 requests a minute). Media's library has the same button: see [a file's details](https://media.obelism.studio/docs/library/details#suggest-alt-text).

Delivery answers the same asset object with the use's alt text and focal point in place, and its `url` carries the focal point as `focus`, so the crop follows it. The file and its other uses keep their own.

### Products in a file [#products-in-a-file]

With a Shopify store connected to the space, files in **Media** can be tagged with its products: **Products** on a file adds them, and clicking the image places each one where it is. The asset picker's product filter finds a product's files.

A use shows them only when you ask: **Show its products** in **For this use**, 🛍 on a file selected in rich text, or `products: true` in the value (`{ "asset": "asset_…", "products": true }`, or the `embeddedAsset` node's attribute). Delivery then adds them to that asset, in the order they were tagged:

```json
"products": [
	{ "id": "gid://shopify/Product/1001", "handle": "denim-jacket", "position": { "x": 0.42, "y": 0.6 } }
]
```

`position` is the product's point in the picture (fractions of its width and height from the top left, as `focalPoint`), or `null` when it was tagged without one. How products are shown is up to your site: hotspots on the points, a list next to the image, or both. Read prices and stock from the Storefront API by `id`, as for a `shopify` field. Other uses of the file, and uses without the switch, have no `products`; while the space has no store connected, the list is empty.

## Private files [#private-files]

A file only some visitors should get (a members' PDF, a price list) can be made private under **Media → Make private**. Its file is then only served with a **signed URL**: the delivery API, the agent API and the dashboard hand out its `url` with `?expires=…&signature=…`, valid until the end of the current hour plus a day. Without a valid signature, or after it expires, the file answers `404`. So keep the delivery token on your server, and only put the URL in pages the right visitors see.

- Signed URLs work with [image transforms](/docs/api/images): add the query to them (the SDK's `imageUrl` does).
- Private files are never kept in the shared edge cache; browsers may keep one privately until its link expires.
- **Revoke links** gives the file a new key: every signed URL handed out so far stops working at once, and new ones are signed with the new key.
- Making a file private moves it to a new URL and announces it like an edit (`asset.updated`). The old public URL stops within a day, once the edge's copy expires, like a deleted file. **Make public** serves it without a signature again.
- Agents use `PATCH …/assets/{id}` with `{ "private": true }`, or `{ "revokeLinks": true }`.

## Serving [#serving]

Sites consume **`/assets/{id}/{filename}`**: the URL the delivery API hands out. An image's URL names its width and quality (`?w=1600&q=80`), which the route forces to what Content decides a site should get: a modern format chosen from `Accept`, metadata stripped, the asset's focal point for crops. Both are optional: without `w` (or `h`) an image gets its own width, and without `q` a quality of 80. `w` is never past 3840 pixels and nothing is enlarged past the original. An AVIF upload is served as it is, already small. Files that aren't images answer as they are, so they can be linked and seeked like a raw file would be. The image query parameters [images](/docs/api/images) documents (`h`, `fit`, `format`, `focus`) work here too.

The other files are answered from the same place as they always were:

- **`/files/{id}/{filename}`** (and `/files/{id}/v{n}/{filename}`) serves the raw file, public and immutable, cached for a year. Files that existed before media took over keep answering raw here exactly as they did; files uploaded after get the forced optimizations above, on this host too. **`/files` is the raw file: don't link it for images — the delivery URL is what Content decides a site should consume.**
- Both carry an `ETag`; a request with a matching `If-None-Match` gets `304 Not Modified`.
- A `Range: bytes=…` request for one part of a file gets `206 Partial Content`, so video can be seeked in Safari and on iOS. Optimized images ignore `Range`.
- Files are kept at Cloudflare's edge for a day; `X-Cache: HIT | MISS` shows whether a response came from there.

Only types a browser shows passively are served inline when opened directly: PNG, JPEG, GIF, WebP and AVIF images, PDF, and common video and audio. Everything else (SVG included) downloads, so nothing uploaded can run as a page. Embedding is unaffected: an SVG still shows in an `<img>`.

## In a storefront [#in-a-storefront]

Delivery hands out absolute URLs on `https://content.obelism.studio`, so any host uses them as they come: no proxy, no client SDK, and no image service of your own. Content does the resizing and re-encoding, on its own edge.

- **Images:** use the `url` as it comes, and build a `srcset` from it with [`imageSrcSet`](/docs/api/images) or by replacing its `w` and `q` (the first `w` in the URL wins). In a React Router or Hydrogen storefront that is a plain `<img>`: Hydrogen's `<Image>` only transforms Shopify CDN URLs with its default loader, so a Content URL gains nothing from it.
- **Allow the host** in the CSP: `img-src https://content.obelism.studio`. Only add `connect-src` for it if a browser reads the delivery API ([the headers on Oxygen](/docs/getting-started/oxygen#3-send-the-headers-the-editor-needs)).
- **Private files:** the signed URL lasts until the end of the current hour plus a day, so don't cache a page that carries one for longer, and keep the delivery token on the server ([private files](#private-files)).
- **Products:** prices and stock come from the Storefront API by `id`, as for a `shopify` field; the points are in the asset when the use asks for them with `products: true` ([products in a file](#products-in-a-file)).

## Deleting [#deleting]

An asset can be deleted while no entry uses it, in any environment, as a draft or published. The dashboard says which entries still do. Its URL stops working within a day, once the edge's copy expires; browsers that already loaded it may keep their own copy.
