---
topic: files
connector: "Not in the connector yet: use the HTTP calls below with the deploy token"
api: "PUT https://ship.embarko.ai/storage/apps/{app-name}/files/{filename}"
requires: [deploy, claim]
---

[← Guide home](https://embarko.ai/docs)

# File storage

**On this page:** Over HTTP, From inside the app, Examples, Limits, Errors, What to tell the person

> Plain words unless the person is a developer. Never invent a link, name or code. [Rules](https://embarko.ai/docs/prerequisites) Needs first: [deploy](https://embarko.ai/docs/deploy), [claim](https://embarko.ai/docs/claim) (over HTTP only).

Every app has its own file store. Put a file in and get a public link back; the link works until the file is deleted. Two ways in: you upload with the deploy token (a logo, seed images, an export the person asked for), or the app uploads while it runs, using two env vars Embarko sets on every server app: STORAGE_URL (where to send) and STORAGE_TOKEN (its own key, good only for its own files). Files count against a per-app and a per-workspace quota set by the plan. For records (rows, settings) use SQLite under DATA_DIR instead; this store is for files people open or download.

**Connector:** Not in the connector yet: use the HTTP calls below with the deploy token

**Done when:** The url in the reply opens the file. If the app uploaded it, the app also saved that url (and the id) with its record.

**Over HTTP**

1. Decide who uploads. A file you have now: you, with the deploy token (the app must be claimed). Files that arrive or are made while the app runs: the app itself, see "From inside the app" below.
2. Upload: PUT https://ship.embarko.ai/storage/apps/{app-name}/files/{filename} with the raw bytes as the body (not a form, not JSON) and a Content-Length header. Reply 201 { id, url, sizeBytes, contentType }.
3. Use the url as-is: it is public and never changes. Every upload makes a new file with a new id and url, even with the same filename, so delete the old one if it should go.
4. List: GET https://ship.embarko.ai/storage/apps/{app-name}/files. Reply 200 { files: [ { id, filename, sizeBytes, contentType, url, createdBy, createdAt } ] }, newest first.
5. Room left: GET https://ship.embarko.ai/storage/apps/{app-name}/usage. Reply 200 { used, quota, workspaceUsed, workspaceQuota, maxFileBytes }, all bytes. Check it before a big upload, and to answer "how much space is left".
6. Delete: DELETE https://ship.embarko.ai/storage/apps/{app-name}/files/{id}, the id from the upload or list reply (not the filename). Reply 200 { ok: true }; the url stops working at once and the space is freed.

**From inside the app**

1. Read process.env.STORAGE_URL and process.env.STORAGE_TOKEN (or os.environ in Python). Never hardcode them, never print them, never send the token to the browser. STORAGE_TOKEN is not a deploy token: it can only read and write this app's files.
2. The app's own name is in APP_NAME, also set by Embarko. Upload = PUT {STORAGE_URL}/apps/{APP_NAME}/files/{filename} with Authorization: Bearer {STORAGE_TOKEN}. List, usage and delete are the same paths as above, on STORAGE_URL.
3. Typical upload handler: receive the file from the browser, PUT it to storage, save the returned id and url in SQLite under DATA_DIR next to the record, render the url in <img src> or as a download link. When the record is deleted, delete the file by id too.
4. A file the app generates (a PDF, a CSV export): build it in memory or in a temp file, PUT it, give the person the url. Do not keep it under DATA_DIR as well.
5. On the developer's machine the two env vars are absent: fall back to a local folder, or skip the upload, so the app still runs there.
6. A full quota (409 storage_quota_reached) is a normal condition, not a crash: show the person a clear message.

**Example: Upload an image with the deploy token → 201**

```bash
curl -sS -X PUT 'https://ship.embarko.ai/storage/apps/my-app/files/hero.png' --oauth2-bearer $DEPLOY_TOKEN --data-binary @hero.png
```

Reply 201:

```json
{"id":"cmh1q2w3e0001abcdfile","url":"https://files.embarko.ai/cmh1q2w3e0001abcdproj/cmh1q2w3e0001abcdfile/hero.png","sizeBytes":48213,"contentType":"image/png"}
```

**Example: Upload from inside the app (Node)**

```js
async function storeFile(name, buffer) {
  const res = await fetch(`${process.env.STORAGE_URL}/apps/${process.env.APP_NAME}/files/${encodeURIComponent(name)}`, {
    method: "PUT",
    headers: { Authorization: `Bearer ${process.env.STORAGE_TOKEN}`, "Content-Length": String(buffer.length) },
    body: buffer,
  });
  if (!res.ok) throw new Error((await res.json()).error); // storage_quota_reached, file_too_large, ...
  return res.json(); // { id, url, sizeBytes, contentType }
}
```

Python: requests.put(f"{os.environ['STORAGE_URL']}/apps/{os.environ['APP_NAME']}/files/{name}", data=data, headers={"Authorization": f"Bearer {os.environ['STORAGE_TOKEN']}"}).json()["url"]

**Example: List, usage, delete**

```bash
curl -sS 'https://ship.embarko.ai/storage/apps/my-app/files' --oauth2-bearer $DEPLOY_TOKEN
curl -sS 'https://ship.embarko.ai/storage/apps/my-app/usage' --oauth2-bearer $DEPLOY_TOKEN
curl -sS -X DELETE 'https://ship.embarko.ai/storage/apps/my-app/files/cmh1q2w3e0001abcdfile' --oauth2-bearer $DEPLOY_TOKEN
```

The reply shown is the usage one. List answers { files: [...] }; delete answers { ok: true }.

Reply 200:

```json
{"used":48213,"quota":524288000,"workspaceUsed":48213,"workspaceQuota":524288000,"maxFileBytes":26214400}
```

**Limits**

- Filename: 1-200 characters, no slashes, extension one of png jpg jpeg gif webp avif ico pdf txt md csv json zip gz mp3 mp4 webm wav ogg woff woff2 ttf. No html, js or svg: files are served as-is from a shared host.
- Per-file, per-app and per-workspace limits depend on the plan; the usage reply names the current ones. 409 storage_quota_reached when one is full, with scope (app or workspace), limit and used.
- Every file is public: anyone with the link can open it, and links are not listed anywhere. Do not store secrets or private records there; for private data use SQLite under DATA_DIR.
- A file belongs to one app. The app's STORAGE_TOKEN cannot touch another app's files; the deploy token follows the person's access to each app.
- Files survive redeploys, renames and rollbacks. Deleting the app deletes its files. The old name keeps working for these calls after a rename.
- Static sites get STORAGE_TOKEN too but nothing runs to use it: upload for them with the deploy token or the dashboard, and reference the url from the HTML.
- Rate limit: 60 calls a minute per token.

**Errors**

| Code | Status | Retry | What to do |
| --- | --- | --- | --- |
| `invalid_filename / file_type_not_allowed` | 400 | No | Rename the file: allowed extension, no slashes, under 200 characters. |
| `length_required` | 411 | No | Send Content-Length with the raw body; chunked uploads are not accepted. |
| `file_too_large` | 400 | No | Over the per-file limit (maxFileBytes in the reply). Shrink it, or tell the person the plan's limit. |
| `storage_quota_reached` | 409 | No | The app or workspace (scope in the reply) is full. Delete files, or tell the person their plan's limit is reached. |
| `size_mismatch` | 400 | Yes | The body was shorter than Content-Length (connection dropped). Nothing was stored; send it again. |
| `file_not_found` | 404 | No | No file with that id on this app. Use the id from the list reply, not the filename. |
| `upload_failed / delete_failed / storage_unavailable` | 502 | Yes | The store did not answer. Nothing changed; retry once after a minute. |
| `storage_not_configured` | 503 | No | This Embarko environment has no file store. Tell the person and report it (see feedback). |
| `invalid_token` | 401 | No | Bad or revoked token, or STORAGE_TOKEN of another app. |
| `permission_denied` | 403 | No | The person who made this token is not allowed to do this. The reply names the permission and their access to the app (or their role). Nothing changed. Tell the person (say_to_user.no_permission); do not try another way round it. |

**What to tell the person**

- done: "Your file is online: {url}. Anyone with the link can open it."
- full: "Your app's file storage is full. Delete some files from its Files tab, or upgrade the plan."

**Next:** Decide who uploads, upload one file, open its url.

**Related:** [data](https://embarko.ai/docs/data) · [env-vars](https://embarko.ai/docs/env-vars) · [delete](https://embarko.ai/docs/delete)

---

[Guide home](https://embarko.ai/docs) · JSON: add .json