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

# Storage

> Store files and JSON in an isolated, durable application namespace.

Marina storage is durable object storage isolated to an app, with S3-like
semantics: whole objects addressed by key, written and replaced atomically,
with last-write-wins on concurrent writes and no partial updates or appends.
Your code reads and writes objects through the runtime Marina supplies to the
app handler. No package or separate storage setup is required.

## Enable storage

Declare the versioned service in `marina.json`:

```json marina.json theme={null}
{
  "schema": 1,
  "entrypoint": "src/app.js",
  "runtime": {
    "storage": "v1"
  }
}
```

Export a portable handler. Marina supplies the declared runtime services as
the second argument:

```js src/app.js theme={null}
export default {
  async fetch(request, marina) {
    const url = new URL(request.url);

    if (request.method === "PUT" && url.pathname === "/report") {
      const body = JSON.stringify({ ready: true, savedAt: new Date().toISOString() });
      const object = await marina.storage.put("reports/latest.json", body, {
        contentType: "application/json",
        metadata: { kind: "report" },
      });
      return Response.json(object, { status: 201 });
    }

    const object = await marina.storage.getStream("reports/latest.json");
    if (!object) return new Response("Not found", { status: 404 });

    return new Response(object.body, {
      headers: { "content-type": object.contentType ?? "application/octet-stream" },
    });
  },
};
```

For local unit tests, import this object and call `fetch` with a small test
double for the services your handler uses. Marina injects the real services
when it runs the deployed app; there is no runtime package to install or cloud
credential to copy into the project.

Adding storage to an existing app is a new runtime capability and may require a
publisher to approve the deployment before it becomes live.

## Operations

| Operation | Result |
| - | - |
| `put(key, body, options?)` | Writes or replaces an object and returns its metadata. |
| `get(key, options?)` | Returns the object, or a byte range of it, with `body` as a `Uint8Array`, or `null`. |
| `getStream(key, options?)` | Returns the object, or a byte range of it, with `body` as a `ReadableStream`, or `null`. |
| `head(key)` | Returns the object's metadata without its body, or `null`. |
| `delete(key)` | Deletes the object. Deleting a missing key also succeeds. |
| `list(options?)` | Returns object metadata, an opaque cursor, and `truncated`. |

`put` accepts a string, `Uint8Array`, `ArrayBuffer`, or `ReadableStream`, with
these options:

| Option | Description |
| - | - |
| `contentType` | The object's content type. |
| `metadata` | Custom string metadata. |
| `size` | The body's exact length in bytes. Required when `body` is a `ReadableStream`. |
| `sha256` | The body's SHA-256 digest as 64 hexadecimal characters. The write fails if the stored bytes differ. |

Object metadata contains the logical key, byte size, ETag, upload time, content
type when set, and custom string metadata. `list` returns metadata only; call
`head` for one object's metadata, or `get` or `getStream` for its body.

Use the returned cursor until `truncated` is false:

```ts theme={null}
let cursor: string | undefined;

do {
  const page = await marina.storage.list({
    prefix: "reports/",
    limit: 100,
    ...(cursor ? { cursor } : {}),
  });

  for (const object of page.objects) {
    console.log(object.key, object.size);
  }

  cursor = page.cursor;
} while (cursor);
```

Treat cursors as opaque and short-lived pagination values. Do not parse them or
store them as object identity.

## Large objects and streams

`get` holds the whole object in memory. `getStream` does not, so use it to
serve large objects:

```js theme={null}
const object = await marina.storage.getStream(key);
if (!object) return new Response("Not found", { status: 404 });
return new Response(object.body, {
  headers: { "content-type": object.contentType ?? "application/octet-stream" },
});
```

To store a request body without buffering it, pass the stream with its exact
length. A body shorter or longer than `size` fails without storing anything:

```js theme={null}
const size = Number(request.headers.get("content-length"));
const sha256 = request.headers.get("x-content-sha256") ?? undefined;
const object = await marina.storage.put(key, request.body, { size, sha256 });
```

### Byte ranges

Pass `range` to `get` or `getStream` to read part of an object:

| `range` | Bytes returned |
| - | - |
| `{ offset }` | From `offset` to the end of the object. |
| `{ offset, length }` | `length` bytes starting at `offset`. |
| `{ suffix }` | The last `suffix` bytes. |

A ranged read also returns `range: { offset, length }`, the bytes actually
returned. A range that runs past the end of the object is shortened. A range
that starts past the end of a non-empty object fails with `INVALID_INPUT`.
The bytes and `range` always come from one version of the object; if the
object keeps being replaced during the read, the read fails with a retryable
`CONFLICT` error.

### Checksums

When `put` includes `sha256`, storage computes the digest of the bytes it
received and fails with `INVALID_INPUT` if it differs. A corrupted or
truncated upload never replaces the existing object.

### Files larger than one request

A request body sent to an app can be at most 100 MB. To store a larger file,
such as a 1 GB build:

1. Upload it in fixed-size parts (8 MiB works well), one request per part,
   each stored with `put(key, request.body, { size, sha256 })`. Retry a failed
   part on its own.
2. Record the list of parts in your own data, such as another object or the
   [database](/guides/database).
3. Serve the file from a route that reads each part with `getStream` and writes
   the parts to the response in order. For an HTTP `Range` request, read only
   the parts the range covers, using `range` on the first and last part.

Memory use stays at about one part at a time. To let a caller without a
browser session download the file, create a
[temporary link](/guides/temporary-links) to the route.

## Keys and isolation

Keys are relative logical names such as `reports/2026/summary.json`. Marina
isolates every key to the current app. Storage v1 enforces these rules:

* Keys are at most 900 UTF-8 bytes and use `/` as the separator.
* Object keys cannot be empty, absolute, or end in `/`.
* Empty, `.`, and `..` path segments, backslashes, and control characters are
  rejected.
* List prefixes may be empty or end in `/`, but otherwise follow the same
  rules.

An app cannot select or escape its Marina-assigned namespace. Another app in
the same workspace cannot read, list, overwrite, or delete its objects.

<Note>
  Namespace isolation does not replace application authorization. If an object belongs to a
  particular user, verify the authenticated user before reading or writing it. Avoid using an
  untrusted URL path directly as an object key even though Marina blocks traversal patterns.
</Note>

## Limits and usage guidelines

| Limit | Storage v1 |
| - | - |
| `put` with a string or bytes, and `get` result | 64 MiB |
| `put` with a `ReadableStream` | 1 GiB |
| `getStream` result | Any object size |
| List page | 1–1,000 objects |
| Content type | 200 characters |
| Custom metadata | 32 entries |
| Metadata key | 1–128 characters |
| Metadata value | Up to 1,024 characters |

Each request to an app can carry a body of at most 100 MB, and an app has
128 MB of memory shared by the requests it is handling. Stream large bodies
instead of buffering them: several buffered objects at once can exceed the
memory limit even when each is under 64 MiB.

Keep secrets and sensitive user data out of object keys and custom metadata.
Treat names and metadata as identifiers rather than an encryption boundary.

Storage belongs to the app, not to one artifact version. Redeploying, rolling
back, or renaming the app keeps the same data. Deleting an app does not
automatically erase its objects in storage v1, so delete objects explicitly
before deleting the app when removal is required.

## Handle runtime errors

Runtime failures have the name `MarinaRuntimeError`, a stable `code`, and a
`retryable` hint:

```js theme={null}
try {
  await marina.storage.put(key, body);
} catch (error) {
  if (error?.name === "MarinaRuntimeError" && error.retryable) {
    // Retry with bounded exponential backoff.
  }
  throw error;
}
```

Do not retry `INVALID_INPUT`, `UNDECLARED`, or a non-retryable
`QUOTA_EXCEEDED` error without changing the request.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.