> For the complete documentation index, see [llms.txt](https://docs.emseapea.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.emseapea.ai/builder-guide/storing-files-and-data.md).

# Storing files and data in your app

[The two kinds of access](/builder-guide/kinds-of-access.md) is about systems your app **reaches** — Salesforce, Microsoft 365, your company ERP. This page is about the other thing entirely: somewhere for your app to keep **its own** stuff. The receipt somebody uploaded. The generated PDF. The bookings table.

These are not the same problem and they do not work the same way, so it is worth being blunt about the difference before anything else:

|                        | Reaching a system                           | Storing your own data                         |
| ---------------------- | ------------------------------------------- | --------------------------------------------- |
| Whose data is it       | The company's, already there                | Your app's, created by your app               |
| Who signs in           | A person, or a prepared account IT set up   | **Nobody.** There is no person and no account |
| What you're asking for | Permission to read something that exists    | Somewhere to put something that doesn't yet   |
| What you get           | A route through your gateway to that vendor | A place, and a route to it                    |

The most common confusion is expecting the second to behave like the first — looking for "whose permissions does the storage use?" There is no such question. Nobody signs in to your app's storage. There is no person whose account could be missing from it, and it holds what your app puts there and nothing else.

## Asking for it

You ask on the same request form as everything else, at **step 4 · Does it need somewhere to keep its own data?** Two checkboxes, in plain words:

* **Does your app need somewhere to put files?** — uploads, documents, images, anything it keeps as a whole file.
* **Does your app need somewhere to keep structured data?** — records, lists, anything you would look things up in.

Tick either, both, or neither. If you tick files, you are asked one more question, and it is the only answer on the form that something actually carries out:

> **Can this app change the files, or only read them?**
>
> A reporting or lookup app usually only needs to read. Choose that where it is true — it is the smaller thing to agree to.

Answer honestly rather than defensively. "Only read them" means your app can open and list its files and cannot add, overwrite or delete one — and your gateway turns away the attempt if it tries. If you pick it for an app that genuinely needs to write, you will find out at the first `PUT`.

### What decides where you're offered

You do not choose a provider, a region or a bucket. Your admin has already set up a list of approved places (see [Somewhere for apps to keep things](/admin-guide/storage.md) for their side of it), and the form narrows that list for you:

* **Exactly one place fits** — it is chosen for you and named. The common case, and it should feel like no decision at all.
* **Several fit** — you pick, with the region and the shutdown rule shown for each, because those are the differences that matter.
* **Nothing fits** — you're told so plainly, and told what an admin would have to add. **Send the request anyway** — it is never a dead end.

What narrows it is what you said in step 2 about the kind of information involved, and (if your org has defined templates) what your app is built from. And here is the honest limit, which appears under the choice on the form itself:

> This is based on what you told us about the information involved. It decides where your data is kept — it does not stop your app storing other kinds of data.

Read that as it is written. Being given "Everyday files" does not mean something will stop you writing a customer's home address into it. **Nothing inspects what your app writes** — your gateway passes bodies through untouched. What you have is a recorded agreement about where your app's data lives, and if what your app handles later stops matching where it keeps it, that shows up on the app's page for a human to look at. Nothing gets moved or switched off behind your back.

## What you get when it's approved

Approval provisions your repo as usual (see [Your workspace](/builder-guide/your-workspace.md)), and storage adds three things to it:

1. **A section in `CONNECTORS.md`** headed **"Where this app keeps its own data"**, with a worked example per operation — the same examples that are on this page. This is the file your AI tool reads, so it does not invent an S3 client and credentials that do not exist.
2. **`emseapea.json`**, committed at the root, recording the grant as data rather than prose: the storage key, the kind, and the **names** of the environment variables to read. It contains no credentials and never will.
3. **Environment variables at deploy time** — the values themselves, injected when your app is deployed, never committed:
   * `GATEWAY_URL` — your organization's gateway. Everything goes here.
   * `CREDENTIAL_HANDLE` — your app's revocable `emcp_…` handle.
   * `EMSEAPEA_STORAGE` — JSON describing every place your app holds right now: the key, the name, the kind, the provider, the size limit, and the gateway path for files. No secrets; you may log it.
   * `DATABASE_URL` — **only** if you were granted a database your app connects to itself. See [that section](#a-database-your-app-connects-to-itself) — it is the one credential on this list that is different from the rest.

**Believe the environment over the file.** An admin can rename a class, raise its size limit, or move a database from one route to the other long after your repo was written, and nothing can reach back into a committed file to correct it. `emseapea.json` therefore carries only what cannot change; `EMSEAPEA_STORAGE` carries the current answer, read fresh at every deploy. If they ever disagree, the variable is right.

## Files, through the gateway

Same shape as every other call your app makes: your gateway, your handle, never a vendor SDK and never a vendor credential. There is nothing new to learn and no emseapea client library to adopt.

> How this app reaches it: through your own gateway. It asks the gateway for a file by name over the web, and the only key it holds is the handle you can revoke.

The address is `{GATEWAY_URL}/u/<the storage key>/…`, and the key is in `EMSEAPEA_STORAGE`. The examples below use `everyday-files`.

**Write a file**

```http
PUT {GATEWAY_URL}/u/everyday-files/invoices/2026-01.csv
Authorization: Bearer {CREDENTIAL_HANDLE}

<the bytes>
```

Stored inside this app's own folder. The path you send is a name inside that folder, so `invoices/2026-01.csv` cannot collide with another app's file of the same name.

**Read it back**

```http
GET {GATEWAY_URL}/u/everyday-files/invoices/2026-01.csv
Authorization: Bearer {CREDENTIAL_HANDLE}
```

The same bytes back.

**List everything this app has stored**

```http
GET {GATEWAY_URL}/u/everyday-files/
Authorization: Bearer {CREDENTIAL_HANDLE}
```

Addressing the storage itself rather than a file in it is how a file store spells "list". The gateway turns it into a listing of this app's own folder and nothing else. Do not send a prefix of your own — the gateway supplies it, and one reaching outside this app's folder is refused.

**Delete a file**

```http
DELETE {GATEWAY_URL}/u/everyday-files/invoices/2026-01.csv
Authorization: Bearer {CREDENTIAL_HANDLE}
```

Gone from this app's folder.

That is the whole API. Nested paths work — `invoices/2026-01.csv` is a name inside your folder, not a directory you have to create first.

### Your folder, and the boundary around it

> This app has its own folder inside that storage and can reach nothing else in it. Paths are taken as names inside that folder, and one that tries to climb out of it is refused outright rather than quietly tidied up — so another app's files are not reachable from here at all.

Your organization's file store is shared between apps, and your gateway puts `<your org>/<your app>/` in front of every path before the request reaches it. Two things follow, and both are worth knowing:

* **You cannot collide with another app.** Two apps can both write `invoices/2026-01.csv` and neither will see the other's.
* **A path that climbs out is refused, not corrected.** `../` games get a refusal, and so does a listing with a `prefix` of your own that reaches wider than your folder. Do not build anything on the assumption that you can walk up a level.

### If your app was granted read-only files

If the request said this app only reads its files, the examples generated into your repo are different — no working write, no delete — and the `PUT` that remains is labelled as the refusal it is:

```http
PUT {GATEWAY_URL}/u/reference-files/invoices/2026-01.csv
Authorization: Bearer {CREDENTIAL_HANDLE}

<the bytes>
```

Refused with 403. Whoever asked for this app said it only needs to read these files, so the gateway turns away anything that would add, overwrite or delete one, before it reaches the storage. That is the expected answer and not something to debug. Reading and listing work normally. **If this app genuinely needs to change files, ask an administrator rather than working around it.**

Your read-only grant is carried out at your gateway, before the store is reached, and the refusal lands in your app's audit trail as its own outcome. What it does **not** do is look at what is in a file:

> Your gateway turns away an attempt to change a file by an app that may only read, before it reaches the storage, and records that it did. It does not look at what is in a file — an app allowed to change its files can put anything it likes in them.

## A database

Two very different routes, and which one you're on was decided by your admin when they set the class up. `EMSEAPEA_STORAGE` tells you which; so does the app's page and the `CONNECTORS.md` in your repo.

### A database your app connects to itself

This is the route that works end to end today.

> How this app reaches it: straight to the database. It connects itself, using a connection string kept as one of its settings, and that string contains a database password this app holds.

Read the connection string from `DATABASE_URL`, with whatever client or ORM your language already has:

```js
// DATABASE_URL is set for you when the app is deployed.
// It is never in this repository and must never be committed to it.
const db = new Client(process.env.DATABASE_URL);
```

A normal database connection. There is no emseapea client to adopt and no gateway in the way — which is also why none of these queries appear anywhere in emseapea:

> What the graph sees: nothing. These calls do not go through your gateway, so this page cannot show you what this app did with this database. You can see what it was given; you cannot see what it used.

`DATABASE_URL` is unsuffixed because an app can be granted exactly one database, which is also why every ORM finds it without configuration.

#### "This app holds a database password" — why this one is different

You will see that sentence on your app's page, on the class in Settings, and on the approver's card. It is there because this credential is genuinely unlike every other one you have been handed, and the difference is worth understanding rather than skimming:

**Every other credential in emseapea is a handle.** Your `emcp_…` handle is not a password to anything. It is redeemed at your organization's gateway, which checks it on every single call, records what you did with it, and can be revoked the moment somebody wants it gone. It expires on its own and is re-issued for you. If it leaks, somebody revokes it and the leak is over — and the audit trail shows exactly what was done with it in the meantime.

**A connection string is not that.** It contains a real database password to a real database. Concretely:

* **It is a key, not a ticket.** Anything holding it can connect. There is no check at your gateway, because your gateway is not in the path.
* **Nothing is recorded.** Not by emseapea. Your app's database activity is invisible to the dependency graph — you can see the app was **given** the database; nobody can see what it **did** with it. If you need to answer "what did this app read last March", the answer has to come from your database's own logs, and only if somebody turned them on.
* **Revoking is not one click.** It means changing the password on the database and getting the new one to every app that uses it. There is no emseapea button that undoes a leak here.
* **It outlives everything.** Your handle expires; a database password does not.

So, in practice: **never log it, never print it in an error, never commit it, never put it in a client-side bundle, and do not pass it anywhere it is not needed.** Load it from `process.env.DATABASE_URL` at the point of use. If your framework dumps environment variables into a debug page, turn that off before you deploy. This is the one place in a governed app where ordinary secret hygiene is entirely on you, because the machinery that usually covers you is not in the path.

It is not a mistake that this route exists — it is the only way to reach a database that cannot answer over the web, and it works with any database and any ORM. It just costs more than the other one, and the cost is worth knowing.

### A database behind your gateway

If your class says "through your gateway," **do not write this code yet.**

> Not set up yet: Emseapea creates a shared file store and routes apps to it, but it does not yet put a database behind your gateway. This records how you have decided apps should reach it.

The only example generated into your repository for this route is the call that is refused, labelled as one, because a plausible-looking example for a path nothing serves is worse than no example:

```http
GET {GATEWAY_URL}/u/everyday-database/anything
Authorization: Bearer {CREDENTIAL_HANDLE}
```

Refused with 403 today. Nothing is behind that path: emseapea does not yet put a database behind your gateway, so this class records how your organization has decided apps should reach it and not something you can call. Ask an administrator before you build against it.

The upside, when it arrives, is the one on the other side of the trade above: your app would hold no database password, only its revocable handle, and every call would be on the graph.

## Size limits, and what happens if you pass one

Your class may record a size limit, and you can see it in `EMSEAPEA_STORAGE` and on your app's page. Two things are true about it:

**Nothing turns your writes away at that number.** emseapea is not between your app and its storage. Passing the limit produces a warning for a human — starting at four fifths of the way there — and a conversation, not an outage. Your data is not silently dropped and your app does not start failing.

**And today nobody can read how much you are using.** Every figure shows as "Not available", with the reason on your app's page. That is not a fault and it is deliberately not shown as a zero: emseapea created your organization's file store and kept no key to it, so it cannot open it to measure. If you need to know how much you are holding, count it in your own app.

## Things it is easy to assume, and shouldn't

* **Nothing looks at what you write.** Not the gateway, not the deploy gate, not the storage class. Being granted a place ticked for "everyday work information" does not stop you writing personal details into it — it means somebody recorded an agreement that you wouldn't. If your app starts handling something different, say so, and the app's page will show that its storage and its declaration no longer agree.
* **Two classes may be one place.** If your org has two Cloudflare R2 file classes, they are the same bucket under two names. Your own folder is still yours alone, and no other app can read it — but do not treat "Sensitive files" as a different building from "Everyday files" unless your admin has put it on a different provider.
* **The region on your class may not be doing anything yet.** It is recorded for your organization's records; it is not currently turned into a provider location. If your app has a hard jurisdiction requirement, ask your admin where the storage physically is rather than reading the label.
* **Retention is not automatic deletion.** "Keep for 90 days, then delete" means the deletion becomes a dated task for an administrator on day 90. emseapea holds no key to your storage and removes no bytes itself.
* **Your storage does not disappear when your handle rotates.** The handle is re-issued automatically; your files stay exactly where they are.

## Where to go next

* [Your workspace: repo, credential, connectors](/builder-guide/your-workspace.md) — the repo, the handle, and `CONNECTORS.md`.
* [The two kinds of access](/builder-guide/kinds-of-access.md) — the other half, for systems your app reaches rather than data it keeps.
* [Deploying through the gate](/builder-guide/deploying.md) — where the environment variables above are injected.
* [Somewhere for apps to keep things](/admin-guide/storage.md) — your admin's side of the list you are picking from, if you want to know why you were offered what you were offered.
