Storage

Fast persistent storage for your projects.

Introduction

A storage is fast block storage: a fixed-size disk on local NVMe SSD, persisting independently of any serverlet instance — no network hop, no shared filesystem, just local-disk speed.

It belongs to a project, like a serverlet or a cron, and holds data that needs to outlive an instance — instances are routinely replaced during rolling updates and scaling, so anything written to an instance’s own filesystem doesn’t persist.

Underneath, it’s one disk image, however you access it — mounted as a block device or reached over one of the protocols below. There’s no separate “block” and “object” product, just one piece of capacity accessed different ways.

Features

  • Resizing — fixed capacity, set at creation and changeable later, up to 256 GiB. Growing extends the disk online, no restart needed; shrinking needs a restart and can only go down to your current usage.
  • Public vs. privateprivate by default: reachable only from the cloud console or a serverlet it’s mounted into, never over plain HTTP. Public allows anonymous reads over HTTP; writes and listing always require authentication. See Serving over HTTP.
  • Mounting into serverlets — attaches as a block device at a path you choose, local-NVMe speed, no network hop. The same storage can be mounted by multiple serverlets and instances at once, on the same or different servers in the region; each server gets its own local replica.
  • Replication and consistency — always replicated, minimum two instances, one per server it’s mounted on, every instance active-active.
    • Multi-writer, multi-reader — master-master, no leader.
    • Atomic locally — a write completes fully or not at all on its own server.
    • Async across the cluster — other instances catch up shortly after.
    • Eventually consistent — near-simultaneous writes resolve deterministically (later wins); a lagging instance briefly serves stale data.
    • Self-healing — conflicts and lag resolve automatically, no manual intervention.
  • Smart offloading — idle storage moves from NVMe to cheaper HDD in the background, still keeping two copies; hot files stay cached. Mounting it again brings it back to NVMe seamlessly.

Protocols

The same disk is reachable through several protocols — a file written through one is visible through the others immediately, subject to replication above.

  • Mounted block device — a normal filesystem from your application code. See Mounting into serverlets.
  • Web UI — browse, upload, download, and manage files from the dashboard.
  • Futrou Storage API — Futrou’s own file API.
  • Futrou S3 API — an S3-compatible endpoint for existing S3 tooling and SDKs.

Futrou Storage API

Futrou’s own file API, at /v2/storages/:name/files/..., is what the dashboard’s file browser calls, and it’s available to call directly:

  • List and search files and folders, with pagination and nesting-depth control.
  • Download a file, or fetch its metadata as JSON.
  • Upload or overwrite a file — raw body, multipart/form-data, or chunked via Content-Range.
  • Create a folder, move or rename a file, delete a file or folder (recursively, on request).

S3-compatible API

The same disk is also reachable as an S3 bucket — point any standard S3 SDK or tool at it using the storage’s endpoint, an API token as the access key, and its secret as the secret key. AWS Signature Version 4 is supported for both header-signed requests and presigned URLs, so tools built against S3 generally work without modification.

Supported actions:

  • ListObjectsV2 — list objects at the bucket root.
  • GetBucketLocation
  • GetBucketVersioning — always reports versioning as not enabled; see below.
  • PutBucketVersioning — succeeds as a no-op for Suspended; rejects Enabled (see Not supported).
  • ListObjectVersions — every object appears once, as its only (“null”) version.
  • HeadObject, GetObject (including Range and conditional headers If-Match, If-None-Match, If-Modified-Since, If-Unmodified-Since)
  • PutObject, CopyObject, DeleteObject, DeleteObjects (batch delete)
  • GetObjectAttributes
  • CreateMultipartUpload, UploadPart, UploadPartCopy, ListParts, ListMultipartUploads, CompleteMultipartUpload
  • A Futrou-specific rename action (PUT with ?renameObject) — not part of the standard S3 API, for moving an object without a copy-then-delete round trip.
  • ETags computed from object content, and Content-MD5 upload verification.
  • Both signed (AWS4-HMAC-SHA256) and presigned-URL authentication.

Not supported:

  • PutBucketAcl / GetBucketAcl, PutObjectAcl / GetObjectAcl — bucket and object ACLs (NotImplemented).
  • PutBucketPolicy / GetBucketPolicy — bucket policies (NotImplemented).
  • PutBucketEncryption / GetBucketEncryption — server-side encryption configuration (NotImplemented).
  • PutBucketLifecycleConfiguration and object lifecycle rules (expiration, transitions) — there’s nothing to transition to, since underneath a storage is block storage, not tiered object storage.
  • PutBucketVersioning with Status=Enabled — every PUT replaces the current version; there’s no version history to turn on, list, or restore (NotImplemented).

Metadata and headers

Every object carries metadata alongside its content — a fixed set of system headers: Content-Type, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, and Expires, set at upload time and echoed back on every subsequent GET/HEAD. Arbitrary user-defined x-amz-meta-* headers aren’t captured or stored.

This metadata, plus each object’s cached ETag and size, is stored as small sidecar files in a .metadata folder at the storage root, mirroring the object tree — mounting the storage as a block device or browsing it in the web UI, you’ll see this folder alongside your files.

If .metadata is deleted — directly on a mounted volume, for instance — nothing is lost structurally: every entry is recreated fresh, on demand, the next time each object is read. ETag and size are always recomputed correctly from the object’s actual content. Custom headers set at upload time can’t be recovered this way, since they aren’t derivable from the file itself — deleting .metadata means objects fall back to a guessed Content-Type and no custom headers until they’re re-uploaded or their metadata is set again.

Serving over HTTP

A public storage (see Public vs. private) can be connected directly to the proxy and served over HTTP straight from the storage — no serverlet in the request path. The proxy serves it like a static file host, with the same conventions as S3 static website hosting:

  • index.html / index.json — requesting a directory path serves its index.html if present, falling back to index.json.
  • 404.html — a custom error page at the storage root is served (with a 404 status) for any missing object, instead of a raw API error.
  • Custom headers from metadata — the system headers above are sent back on every response, so assets can carry their own cache and content headers.

This makes a storage a natural fit for hosting a static site or serving static assets (images, downloads, built frontend bundles) directly, without a serverlet running just to serve files.

Use cases

Application data

Mount a storage into your serverlet and use it like a normal local disk for anything your application needs to persist — uploads, generated files, session data, caches, queues. This is the common case, and the default replicated mode (two or more instances, async replication) is the right fit: writes hit local NVMe immediately, and the data converges across replicas shortly after, with no manual replication to manage.

Static sites and assets

Make the storage public and connect it to the proxy to serve a static site or static assets (images, downloads, frontend bundles) directly over HTTP — no serverlet needed in the request path at all. See Serving over HTTP.

Databases

Don’t use Futrou’s storage replication for a database’s own data directory. A storage’s replication is asynchronous and eventually consistent at the block level — a database engine writing its files expects its own storage to be immediately, strictly consistent, and two independently-writing replicas of the same database files will corrupt each other rather than merge safely.

For a database, mount a storage in local mode instead: a single instance, no replication at the storage level at all. If you need database redundancy, handle it at the database level — run the database’s own primary/replica (master-slave) replication across two serverlets, each with its own separate local storage, and let the database engine own consistency for its own data.

Permissions

Creating, resizing, and deleting a storage requires at least the Developer role in the workspace; viewing and using an existing storage’s data is available to any role that can access the project. See Roles for what each role can do.

Last updated