Skip to Content
TestingApp Object Storage - S3-compatible buckets per app

App Object Storage - S3-compatible buckets per app

Provision a per-app object storage bucket and have its S3-compatible credentials delivered straight into your app’s environment. Files uploaded through the bucket survive VM rebuilds, redeploys, and slug changes, because the bucket is managed by the platform, not by the app VM’s filesystem.

The bucket is reachable with any standard S3 client (boto3, aws-sdk, minio-js, s3cmd) using the published OF_S3_* env vars.

Today (M1): the bucket data plane (naming, credential generation, env publication, and the libvirt REST routes) is fully wired, but buckets are currently fabricated against a stub MinIO endpoint (stub-minio-platform.local:9000) rather than a live MinIO VM. M1.5 swaps in real mc admin calls against of-live-platform-minio. The MCP surface, env keys, and boto3 usage shown below do not change when that flip happens.

When to use it

  • Your app needs to persist user uploads (images, PDFs, exports) across redeploys.
  • You want a shared artifact store between build/deploy jobs and the running app.
  • You want to avoid baking secrets into the repo. Credentials are injected via the env at deploy time, just like managed databases.

For ephemeral scratch space that does not need to survive a rebuild, the app VM’s local disk is fine. Use a bucket when the data is part of the product.

How it works

  1. Provision - add_app_bucket asks the platform for a fresh bucket. The platform generates a globally-unique bucket name (slug + suffix), an access key, and a secret key.
  2. Publish env - the same call publishes five env keys (OF_S3_ENDPOINT, OF_S3_BUCKET, OF_S3_ACCESS_KEY, OF_S3_SECRET_KEY, OF_S3_REGION) into the app’s environment via the secrets producer API, and appends the bucket_id to the app’s bucket_refs.
  3. Deliver - the next deploy_app lands those vars in the app VM at /etc/openfactory/app.env, where your process reads them on boot.
  4. Use it - your app code constructs an S3 client from the env vars and reads/writes the bucket like any other S3 endpoint.

The secret key is returned once, on the provisioning call, and is never returned by subsequent list_app_buckets or GET /buckets/{id} reads. If you lose it, rotate by adding a new bucket.

MCP tools

ToolUse
add_app_bucketProvision a bucket for an app and publish OF_S3_* env vars
list_app_bucketsList buckets for an app (metadata only; no secrets)

add_app_bucket

Provisions a MinIO bucket for the app and publishes OF_S3_* credentials into the app’s env via the producer API.

add_app_bucket(app_id='550e8400-e29b-41d4-a716-446655440000')

Response when env_published=true (producer token configured and publish succeeded):

{ "bucket_id": "buk-a7f3b2c1d9e4f", "bucket_name": "my-app-cool-thing-2026-9357b4", "endpoint_url": "http://stub-minio-platform.local:9000", "access_key": "AKIAJY4T7ZQXMN2LQWPS", "region": "us-east-1", "status": "stub", "env_keys": ["OF_S3_ENDPOINT", "OF_S3_BUCKET", "OF_S3_ACCESS_KEY", "OF_S3_SECRET_KEY", "OF_S3_REGION"], "env_published": true, "next": "deploy_app(app_id='550e8400...', vm_name=...) - OF_S3_* env vars are already in the app's env. App code: boto3.client('s3', endpoint_url=os.environ['OF_S3_ENDPOINT'], aws_access_key_id=os.environ['OF_S3_ACCESS_KEY'], aws_secret_access_key=os.environ['OF_S3_SECRET_KEY']).", "session_token": "user@example.com" }

Response when env_published=false (producer token not configured or publish failed):

{ "bucket_id": "buk-a7f3b2c1d9e4f", "bucket_name": "my-app-cool-thing-2026-9357b4", "endpoint_url": "http://stub-minio-platform.local:9000", "access_key": "AKIAJY4T7ZQXMN2LQWPS", "region": "us-east-1", "status": "stub", "env_keys": [], "env_published": false, "secret_key": "Zy6vd8xqL2pjH5mK9nR4sT7wY+a3bCdEfG0h", "env_payload": { "OF_S3_ENDPOINT": "http://stub-minio-platform.local:9000", "OF_S3_BUCKET": "my-app-cool-thing-2026-9357b4", "OF_S3_ACCESS_KEY": "AKIAJY4T7ZQXMN2LQWPS", "OF_S3_SECRET_KEY": "Zy6vd8xqL2pjH5mK9nR4sT7wY+a3bCdEfG0h", "OF_S3_REGION": "us-east-1" }, "publish_skipped_reason": "OPENFACTORY_STORAGE_PRODUCER_TOKEN is not configured; call set_app_env(app_id, set=<env_payload from this response>) to inject manually.", "next": "set_app_env(app_id='550e8400...', set=<env_payload from this response>) then deploy_app(...) - OF_S3_* will land in /etc/openfactory/app.env and boto3 / @aws-sdk/client-s3 will pick it up automatically.", "session_token": "user@example.com" }

The secret_key field is only present when env_published=false and is shown exactly once. When env vars are published via the producer API, the secret is never returned again. env_keys reflects which keys were published; it is empty when env_published=false. The session_token field is appended by the MCP wrapper and is not present in direct REST calls.

status: "stub" reflects the M1 caveat above; in M1.5 this becomes "ready" and endpoint_url points at the real MinIO VM.

list_app_buckets

Lists all buckets provisioned for an app. Metadata only - access_key is returned for reference, but secret_key is never returned here.

list_app_buckets(app_id='550e8400-e29b-41d4-a716-446655440000')
{ "buckets": [ { "bucket_id": "buk-a7f3b2c1d9e4f", "app_id": "550e8400-e29b-41d4-a716-446655440000", "bucket_name": "my-app-cool-thing-2026-9357b4", "access_key": "AKIAJY4T7ZQXMN2LQWPS", "endpoint_url": "http://stub-minio-platform.local:9000", "region": "us-east-1", "shape": "shared-minio", "shared_host": "stub-minio-platform.local", "status": "stub", "notes": null, "created_at": "2026-06-25T14:30:22Z" } ], "session_token": "user@example.com" }

The session_token field is appended by the MCP wrapper and is not present in direct REST calls to /api/app-infra/buckets.

REST endpoints

The MCP tools call these under the hood. Most users will not need to hit them directly, but they are the integration surface for non-MCP clients (CI pipelines, custom dashboards).

MethodPathPurposeAuth
POST/api/app-infra/bucketsProvision a bucket + access/secret key pair for an appBearer token, or X-User-Email header (loopback only) or X-Guest-Id header
GET/api/app-infra/buckets/{bucket_id}Fetch bucket metadata (secret_key is never returned)Bearer token, or X-User-Email header (loopback only) or X-Guest-Id header
GET/api/app-infra/bucketsList buckets, optionally filtered by app_id query paramBearer token, or X-User-Email header (loopback only) or X-Guest-Id header

REST responses do not include the session_token wrapper field that appears in MCP responses.

Env vars delivered to your app

KeyWhat it holds
OF_S3_ENDPOINTEndpoint URL of the bucket (use as endpoint_url)
OF_S3_BUCKETThe bucket name
OF_S3_ACCESS_KEYAccess key for the bucket
OF_S3_SECRET_KEYSecret key for the bucket
OF_S3_REGIONAWS-style region string (defaults to us-east-1)

Use them with explicit kwargs - do not rely on ~/.aws/credentials or ambient AWS env vars:

import os, boto3 s3 = boto3.client( "s3", endpoint_url=os.environ["OF_S3_ENDPOINT"], aws_access_key_id=os.environ["OF_S3_ACCESS_KEY"], aws_secret_access_key=os.environ["OF_S3_SECRET_KEY"], region_name=os.environ["OF_S3_REGION"], ) s3.upload_file("logo.png", os.environ["OF_S3_BUCKET"], "logo.png")

Putting it together

End-to-end from an empty app to a durable upload:

  1. Provision. Call add_app_bucket(app_id). The MCP tool POSTs to /api/app-infra/buckets, which returns bucket metadata plus the one-time-visible secret key (if env publish is skipped).
  2. Publish. The same call publishes the five OF_S3_* keys into the app’s env via the secrets producer API (when OPENFACTORY_STORAGE_PRODUCER_TOKEN is configured), and appends bucket_id to the app’s bucket_refs.
  3. Deploy. The next deploy_app writes the env vars into the app VM at /etc/openfactory/app.env. Your process picks them up on (re)start.
  4. Use. App code builds a boto3 client with explicit endpoint_url / aws_access_key_id / aws_secret_access_key and reads or writes objects in OF_S3_BUCKET.
  5. Survive. Rebuild or redeploy the app VM as often as you like - the bucket and its contents are platform-managed, so uploaded files (logo.png, user PDFs, exported CSVs) are still there after the rebuild.

Notes

  • Secret key is one-shot. It is returned once by add_app_bucket when env publishing is skipped, and never again. Lose it, provision a new bucket.
  • One app, many buckets. add_app_bucket is additive. Each call provisions a new bucket and appends to bucket_refs.
  • Bucket names are globally unique. The platform appends a short random suffix to the slug so two apps cannot collide.
  • Stub mode is honest. In M1 the bucket is fabricated against stub-minio-platform.local:9000. Code written against the env vars works unchanged when M1.5 promotes the endpoint to the real MinIO VM.
  • MCP vs. REST response shape. MCP responses include a session_token field for session continuity. REST responses do not. Both include the same data fields otherwise.
  • Anonymous public reads (a /_storage/... Caddy gateway) are deferred to feature 07. For now, all access is authenticated via the access/secret key pair.
Last updated on