# Objects

> Upload files of up to 10 GiB from your backend, hand out download links that last 15 minutes, list, delete and restore files, and decide who may reach each one.
> Source: https://mdrn.io/docs/objects/

After this page an environment of your project stores files, your backend uploads them and hands out download links that work for 15 minutes, lists and deletes them, and brings a deleted one back within 30 days. Objects is called from your app's backend, never from a browser; a browser only ever sees a link ([Apps with a backend](/docs/apps-with-a-backend/)).

## Turn it on

A new project starts with Objects on. It is switched per project, in Sandbox and Production at once, and each environment has its own files. If it is off, in the console open the project, then **Objects** under Storage in the sidebar, and press **Turn on Objects**. Only an Admin can turn a product on. From the CLI:

```sh
mdrn products enable objects --idempotency-key <key>
```

Every release built after that receives two variables:

| Variable | What it is |
| --- | --- |
| `MDRN_OBJECTS_URL` | The address your backend sends file calls to. |
| `MDRN_OBJECTS_TOKEN` | The credential it sends with them, as `Authorization: Bearer`. |

Send every call through `env.MDRN_PRODUCTS.fetch`, with the `Idempotency-Key` every change needs ([what your backend receives](/docs/apps-with-a-backend/#what-your-backend-receives)). The bytes themselves go to and from a **grant** address with a plain `fetch`, never through `MDRN_PRODUCTS` and never with your token. A release that was already serving when you turned Objects on does not have the variables: deploy again.

## Upload a file

A file goes up in three steps: open an upload with the file's name, type, size and SHA-256; send its bytes in parts of 16 MiB, each to a part grant; complete the upload with each part's `etag`. MDRN then reads the file back, checks it against the SHA-256 you gave, and the file becomes `ready`. The JavaScript SDK ([SDKs](/docs/sdk/)) does all three and waits for the check:

```javascript
import { Mdrn } from "@mdrnio/sdk";

const mdrn = Mdrn.fromEnv(env);
const file = await mdrn.objects.upload({ name: "report.pdf", type: "application/pdf", bytes });
// file.id, file.state === "ready", file.bytes, file.sha256
```

Raw, with paths relative to `MDRN_OBJECTS_URL` (your app's origin followed by `/v1/objects`):

```http
POST /uploads
Authorization: Bearer <MDRN_OBJECTS_TOKEN>
Idempotency-Key: <new key for this upload>
Content-Type: application/json

{"filename": "report.pdf", "contentType": "application/pdf", "bytes": 1048576, "sha256": "<hex sha-256 of the whole file>"}
```

The answer is `202` with the upload: its `id`, the `objectId` the file will have, `state` and `partBytes` (16777216). MDRN prepares the upload before it answers, so `state` is almost always `uploading` already; if it is still `pending`, read `GET /uploads/<id>` a little later until it is. Then for each part, numbered from 1:

```http
POST /uploads/<id>/parts
Authorization: Bearer <MDRN_OBJECTS_TOKEN>
Idempotency-Key: <new key for this part>
Content-Type: application/json

{"partNumber": 1, "bytes": 1048576}
```

The answer is `202` with a grant: `url`, `method` (`PUT`), `headers` and `expiresAt`. Send the part's bytes with exactly that method and those headers to that address; the answer carries the part's `etag`. Every part but the last is 16 MiB. Then:

```http
POST /uploads/<id>/complete
Authorization: Bearer <MDRN_OBJECTS_TOKEN>
Idempotency-Key: <new key>
Content-Type: application/json

{"parts": [{"partNumber": 1, "etag": "<etag>"}]}
```

The answer is `202` with the object's `state` and an `operationId`. MDRN checks the bytes for up to about 5 seconds before it answers, so a small file is usually `ready` already; a large one may still be `verifying`, and then you read `GET /<objectId>` until `state` is `ready`. Reading an upload that is still `pending` or an object that is still `verifying` is not counted on your usage; a file whose bytes do not match the SHA-256 or the size is `rejected` and never served (the SDK throws `OBJECT_REJECTED`). A zero-byte file is completed with an empty `parts` array. `DELETE /uploads/<id>` abandons an upload; one not completed within 24 hours expires on its own.

## Give a user a download link

```javascript
const { url, expiresAt } = await mdrn.objects.downloadUrl(file.id);
```

```http
POST /<id>/download-grants
Authorization: Bearer <MDRN_OBJECTS_TOKEN>
Idempotency-Key: <new key>
Content-Type: application/json

{}
```

The answer is `200` with a grant whose `url` anyone holding it can `GET` for 15 minutes, as often as they like, with a `Range` header if they want part of the file. It is for a `ready` file only, and stops working when the file is deleted or replaced. The link is on `files.mdrn.page`, or on your own host such as `files.example.com` once you give a domain's host the environment's downloads ([Domains](/docs/deploy/domains/)). Send the link to the browser, as a redirect or in a page; never the file through your backend.

A download is served with the file's `contentType` and name, with `Cache-Control: private, no-store`, and inside a sandbox: `Content-Security-Policy: sandbox; default-src 'none'` and `X-Content-Type-Options: nosniff`, so an HTML or SVG file someone uploaded runs no script and reaches no cookie when opened. PNG, JPEG, GIF, WebP and AVIF images, MP3, Ogg and WAV audio, and MP4, WebM and Ogg video open in the browser; everything else, SVG and PDF included, is offered as a download.

## List and delete files

```javascript
const page = await mdrn.objects.list({ limit: 50 });   // { items, nextCursor }
const info = await mdrn.objects.get(file.id);          // null when there is none
await mdrn.objects.delete(file.id);
```

```http
GET ?limit=50&cursor=<nextCursor>
GET /<id>
DELETE /<id>
Authorization: Bearer <MDRN_OBJECTS_TOKEN>
```

A list answers the `ready` files of the environment, ordered by `id`, with `items` and `nextCursor`; a cursor works for 24 hours. `DELETE` answers `204`: every link to the file stops working at once, the file leaves the list, and it is kept for 30 days, counting as stored until then. Deleting it again does not extend the 30 days.

## Keep one user's files from another's

Objects does not know who your users are. Every file belongs to the project and environment it was uploaded to, and `MDRN_OBJECTS_TOKEN` lets your backend list, download and delete every one of them. Nothing in Objects stops one user's request from reaching another user's file: your backend does.

- Store each file's `id` with the record of who owns it, and compare the signed-in user's ID ([`x-mdrn-user-id`](/docs/auth/#read-who-is-signed-in)) with that record before you create a download link, delete a file or show it in a list.
- A download or upload grant works for whoever holds it, signed in or not, until it expires. Give a link only to the user it is for, and do not store it or put it in a page others can see.
- Nothing a browser sends names a file to Objects directly: a path like `/files/<id>` on your app is answered by your backend, which checks the owner and then asks for a grant.

The [app template](https://github.com/mdrn-io/app-template) does this: a record per file with its owner, and a route that checks the owner before every grant.

## Restore a deleted file

A deleted file is kept 30 days. In the console, the environment's **Objects** page lists them under **Deleted objects**, each with **Restore**; with a management token, `POST /v1/platform/projects/{projectId}/environments/{environmentId}/objects/{objectId}/restore` does the same. The file keeps its `id`, is `ready` again and counts as stored as before. Links issued before the deletion stay dead: issue new ones. After 30 days the file is permanently removed and cannot be restored. Your app's own token cannot restore, and cannot list deleted files.

## Exporting your data

Your files are yours to take out at any time, as the bytes you uploaded, under the names you gave them.

- **Every file at once:** `mdrn objects pull <dir>` pages through the environment's ready files and downloads each one into `<dir>`, its name as the path inside it (folders are made as needed). It streams each file to disk, so a large file never sits in memory.

  ```bash
  mdrn objects pull ./files-backup --project project_id --environment production
  ```

  A file already in `<dir>` with the same size is skipped, so running it again finishes an interrupted pull; `--force` downloads everything again. A name that would land outside `<dir>` (an absolute path, `..`, a drive letter) is skipped and named in the output, and the command then ends with exit code 1, as it does when a download fails (each file is checked against its size and SHA-256). When two files share a name, the later one is saved with its file `id` before the extension, such as `report.obj_123.pdf`.
- **One file:** on the environment's **Objects** page in the console, **Copy link** makes a download link for that file.
- **From your own tools:** with a management token, `GET /v1/platform/projects/{projectId}/environments/{environmentId}/objects` lists the files and `POST .../objects/{objectId}/download-grants` makes a link ([management reference](/docs/reference/management/objects/)).

Every download link is signed, read-only and lasts 15 minutes, like the links your app makes.

Exporting keeps working when your app's calls do not: while Objects is turned off, while the workspace has no current plan or an unpaid bill, and while its files are archived (the 90 days before archived data is deleted). Workspace members who can read the environment's files, and management tokens with `objects:read`, can list and download them; your app's own token stays refused, and nothing can be uploaded, deleted or restored until Objects is on and paid for again. Reading does not restore anything or move it back.

While Objects is on, a download this way counts on your usage like any other download (one request and one object read). While Objects is off or archived, exporting is not charged.

## Limits

| What | Limit | Answer when exceeded |
| --- | --- | --- |
| A file | 10 GiB | `400 INVALID_INPUT` |
| A part | 16 MiB, every part but the last exactly that | `400 INVALID_INPUT` |
| Parts per upload | 640 | `400 INVALID_INPUT` |
| An upload must complete within | 24 hours | The upload expires |
| A download or part grant lasts | 15 minutes | `403` at the grant address |
| Uploads open at once, per environment | 100 | `429 TOO_MANY_OPEN_UPLOADS` |
| Stored per environment, open uploads and deleted files included | 1 TiB | `409 OBJECTS_QUOTA_EXCEEDED` |
| A deleted file is kept | 30 days | Permanently removed |
| A file name | 255 bytes, no control characters and no invisible direction or zero-width characters (the zero-width joiner is allowed) | `400 INVALID_INPUT` |
| A content type | 128 bytes | `400 INVALID_INPUT` |
| Files per page | 50 by default, 200 at most | `400 INVALID_INPUT` |
| A list cursor works for | 24 hours | `400 INVALID_INPUT` |
| A file, in build mode | 100 MB | `402 BUILD_LIMIT_REACHED` |
| Stored, in build mode | 1 GB | `402 BUILD_LIMIT_REACHED` |

The same limits, beside the rest of the platform's, are on [Limits](/docs/limits/#objects). What each upload, part, download and stored gigabyte costs is on [Billing](/docs/billing/#how-each-meter-is-measured).

## Reference

- [Objects from your app](/docs/reference/app/objects/): uploads, parts, grants, objects and operations, their requests and answers.
- [Objects management](/docs/reference/management/objects/): deleted files, restores and deleting all data.
- [Error codes](/docs/errors/#every-product): every code a file call can answer.
