Skip to main content

Use Shared Storage

The platform exposes object storage through obstore, a thin abstraction that lets the same code run against AWS S3, GCS, Azure Blob, and local MinIO. Solutions use it for artifacts, intermediate data, files served to the UI, and anything else that doesn’t belong in the relational database. For the platform-side abstraction model, see Platform Overview. For data-source connections (databases + storage credentials registered by customers), see Platform › Data Connections.

Why obstore

Three things you get for free:
  1. One code path, all clouds. No if AWS: ... elif GCP: ... in your service.
  2. Credential rotation handled by the platform. Connections fetch fresh credentials per request via the secrets pipeline.
  3. Per-tenant path isolation enforced by convention. Every operation is scoped by org_id + project_id (passed via X-Project-ID).
Read left-to-right: your code calls obs.put / obs.get with a path scoped by org_id/project_id/solution; the obstore client picks the actual backend from env config; the same code runs against any of S3/GCS/Azure/MinIO.

Path conventions

Stick to this prefix layout for everything you write:
Example:
Predictable paths make backup, garbage collection, and audit trivial.

Provision a connection

Customers register a Data Connection (type s3, gcs, or minio) via Assets. Your solution looks it up by name when it needs to read or write.
The customer registers the connection in the UI or via the Assets API, then grants your solution permission. Your code:

Code patterns

The obstore Python package gives you a single API across providers. Install with uv add obstore.

Initialize a client from env

packages/api/src/api/storage.py

Put / get / list / delete

Stream a large object (avoid loading into memory)

For multi-GB objects, prefer streaming over bytes().

Per-tenant isolation

X-Project-ID is mandatory on every storage operation. Build the prefix from request context:
Now every read/write is scoped — there is no path a request can construct that crosses tenants. See Platform › Multi-tenancy for the org/project model.

Local development with MinIO

The Local Development page shows the docker-compose snippet. Pre-create the bucket once:
Then set OBSTORE_BACKEND=s3, OBSTORE_ENDPOINT=http://localhost:9000, OBSTORE_PATH_STYLE=true. The same code that talks to AWS S3 in prod talks to MinIO in dev.

Common errors

Set OBSTORE_PATH_STYLE=true. MinIO doesn’t support virtual-hosted-style addressing without DNS configuration.
Check that the bucket policy / Workload Identity SA grants the required action. For GCS Workload Identity, verify the K8s ServiceAccount is annotated with iam.gke.io/gcp-service-account=<gsa> and the GSA has roles/storage.objectAdmin on the bucket.
AWS S3 SDKs require a region; set OBSTORE_REGION even if you’re targeting a non-AWS endpoint via OBSTORE_ENDPOINT.
Your storage_prefix helper isn’t being used everywhere, OR your route accepts a user-controlled key without prefixing. Audit by grepping for obs.get_async\|obs.put_async\|obs.delete_async in your service and confirming every call site composes the path through storage_prefix(...).

Verification

Next steps

Data connections

How customers register storage and DB connections.

Multi-tenancy

Org/project isolation — what enforces it where.

Manage secrets

Storage credentials follow the same secret-injection pattern.

Troubleshooting

More storage error scenarios.