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 realmc admincalls againstof-live-platform-minio. The MCP surface, env keys, andboto3usage 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
- Provision -
add_app_bucketasks the platform for a fresh bucket. The platform generates a globally-unique bucket name (slug + suffix), an access key, and a secret key. - 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 thebucket_idto the app’sbucket_refs. - Deliver - the next
deploy_applands those vars in the app VM at/etc/openfactory/app.env, where your process reads them on boot. - 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
| Tool | Use |
|---|---|
add_app_bucket | Provision a bucket for an app and publish OF_S3_* env vars |
list_app_buckets | List 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).
| Method | Path | Purpose | Auth |
|---|---|---|---|
POST | /api/app-infra/buckets | Provision a bucket + access/secret key pair for an app | Bearer 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/buckets | List buckets, optionally filtered by app_id query param | Bearer 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
| Key | What it holds |
|---|---|
OF_S3_ENDPOINT | Endpoint URL of the bucket (use as endpoint_url) |
OF_S3_BUCKET | The bucket name |
OF_S3_ACCESS_KEY | Access key for the bucket |
OF_S3_SECRET_KEY | Secret key for the bucket |
OF_S3_REGION | AWS-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:
- 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). - Publish. The same call publishes the five
OF_S3_*keys into the app’s env via the secrets producer API (whenOPENFACTORY_STORAGE_PRODUCER_TOKENis configured), and appendsbucket_idto the app’sbucket_refs. - Deploy. The next
deploy_appwrites the env vars into the app VM at/etc/openfactory/app.env. Your process picks them up on (re)start. - Use. App code builds a
boto3client with explicitendpoint_url/aws_access_key_id/aws_secret_access_keyand reads or writes objects inOF_S3_BUCKET. - 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_bucketwhen env publishing is skipped, and never again. Lose it, provision a new bucket. - One app, many buckets.
add_app_bucketis additive. Each call provisions a new bucket and appends tobucket_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_tokenfield 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.
Related
- App Deployment -
deploy_appis what actually lands theOF_S3_*env vars in the app VM. - App Secrets and Environment Variables - the producer API that publishes bucket credentials into the app’s env store.
- Managed Databases - same libvirt REST + producer pattern, for Postgres/MySQL instead of object storage.
- MCP Integration - set up the OpenFactory MCP server.