Skip to main content
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:
marina.json
Export a portable handler. Marina supplies the declared runtime services as the second argument:
src/app.js
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

put accepts a string, Uint8Array, ArrayBuffer, or ReadableStream, with these options: 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:
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:
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:

Byte ranges

Pass range to get or getStream to read part of an object: 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.
  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 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.
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.

Limits and usage guidelines

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:
Do not retry INVALID_INPUT, UNDECLARED, or a non-retryable QUOTA_EXCEEDED error without changing the request.