Storage
Stack ships a Supabase Storage API deployment so your app can serve and store files without wiring S3 by hand. By default the controller deploys MinIO in your namespace, generates credentials, and injects them plus a JWT secret into the storage pod.
This page continues the demo flow from the Database and REST guides.
Quick local test (demo manifest)
With the demo manifest, you can hit the Storage API via the nginx gateway at /storage.
First export the service role JWT from stack status:
export SERVICE_ROLE_JWT="$(stack status --manifest demo.stack.yaml | awk -F'Service role: ' '/Service role: /{print $2; exit}')"
Then create a bucket:
curl --location --request POST 'http://localhost:30090/storage/v1/bucket' \ --header "Authorization: Bearer ${SERVICE_ROLE_JWT}" \ --header 'Content-Type: application/json' \ --data-raw '{"name": "avatars"}'
Verify in Postgres:
kubectl -n stack-demo exec -it stack-demo-db-cluster-1 -- psql -d stack-demo \ -c 'select * from storage.buckets;'
Defaulted container "postgres" out of: postgres, bootstrap-controller (init) id | name | owner | created_at | updated_at | public | avif_autodetection | file_size_limit | allowed_mime_types | owner_id | type ---------+---------+-------+-------------------------------+-------------------------------+--------+--------------------+-----------------+--------------------+----------+---------- avatars | avatars | | 2026-01-09 06:47:35.646183+00 | 2026-01-09 06:47:35.646183+00 | f | f | | | | STANDARD (1 row)
Upload a file
Create a file
echo "hello storage" > hello.txt
and now the upload
curl -X POST 'http://localhost:30090/storage/v1/object/avatars/hello.txt' \ -H "Authorization: Bearer ${SERVICE_ROLE_JWT}" \ -H 'Content-Type: text/plain' \ --data-binary @hello.txt
You should see something like...
{"Key":"avatars/hello.txt","Id":"25e4259e-2f80-43d4-9ab1-1d433b4b2165"}
Then check the DB:
kubectl -n stack-demo exec -it stack-demo-db-cluster-1 -- psql -d stack-demo \ -c 'select id, name, bucket_id, version from storage.objects;'
And our file meta data is in the database.
Defaulted container "postgres" out of: postgres, bootstrap-controller (init) id | name | bucket_id | version --------------------------------------+-----------+-----------+-------------------------------------- 25e4259e-2f80-43d4-9ab1-1d433b4b2165 | hello.txt | avatars | 329061dc-dacf-4150-9cc9-66bd9db14525 (1 row)
What the controller creates
- A
storageDeployment usingsupabase/storage-apion port5000. - A
storage-s3Secret with generated AWS-compat credentials and endpoint/bucket settings. - A
jwt-authSecret with a random JWT secret (shared with REST and Realtime). - A MinIO Deployment/Service when you have not provided your own S3 secret.
Using the default MinIO
If you omit s3_secret_name, the controller:
- Creates
storage-s3with defaults (bucketsupa-storage-bucket, endpointhttp://minio:9000, regionus-east-1, path-style forced). - Generates random access keys and S3 protocol keys.
- Deploys MinIO with those credentials.
Using an external S3/MinIO
Create a secret (any name) in your app namespace with these keys:
STORAGE_S3_BUCKETSTORAGE_S3_ENDPOINT(e.g.https://s3.amazonaws.comorhttp://minio.example:9000)STORAGE_S3_REGIONSTORAGE_S3_FORCE_PATH_STYLE("true"or"false")AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYS3_PROTOCOL_ACCESS_KEY_IDS3_PROTOCOL_ACCESS_KEY_SECRET
Then set s3_secret_name to that secret and install_minio: false if you don’t want the bundled MinIO.
Database wiring
- Storage uses the
migrations-urlfrom thedatabase-urlssecret so it can run migrations. - JWT auth uses the shared
jwt-authsecret and HS256. - Supabase Storage keeps metadata in your app’s Postgres database under the
storageschema (tables likebuckets,objects, and policies). Plan migrations/backups accordingly—metadata and object store must stay in sync.
Customising with the CRD
Add a storage block to your StackApp:
spec: components: storage: # Optional: point at your own S3 secret and skip MinIO s3_secret_name: my-s3-secret install_minio: false