Storage
Manage S3-compatible storage buckets for files, images, and assets. Backed by Garage (self-hosted S3) and accessed via any S3 SDK.
ghayma storage create
Create a new storage bucket in the current project (reads project_id from
.ghayma.json).
ghayma storage create my-assets
# Set a storage quota in GB up front
ghayma storage create my-assets --quota-gb 50
# Connect it to the 'main' site as it is created
ghayma storage create my-assets --site mainOptions:
| Flag | Description | Default |
|---|---|---|
--quota-gb | Per-bucket storage quota in GB, priced in points. Interactive picker when omitted; plan default if the catalog is unavailable. | picker / plan default |
--site | Connect the new bucket to this site (slug); repeat for several. | asked on a terminal |
--no-connect | Connect it to no site, without asking. | — |
A new bucket is connected only to the sites you choose. With neither flag the
CLI asks on a terminal, while a script connects nothing and prints the
ghayma connect bucket … command — exactly as ghayma db create does.
Sizing and points
A bucket’s quota is priced in points (see
Pricing by Service). Pass
--quota-gb to set it non-interactively; omit it and storage create opens a
picker:
Storage quota in GB — steps of 1 (blank for plan default): 50When you choose a quota, the CLI previews the points it will reserve before the bucket is created:
This bucket will reserve 5 pts · 45 remaining afterThe · N remaining after tail appears only when your project has a points budget.
Leaving the quota blank uses the plan’s per-bucket default and prints no preview.
The quota steps and points come from the live catalog — the figures above are only examples.
Interactive picker: Ctrl-C cancels without creating anything — the CLI
prints ❌ Cancelled. and exits.
Output:
✅ Created storage bucket 'my-assets' (linked to project)
ID: <uuid>
Bucket: st-myassets-a1b2c3d4
Limit: 10.0 GB
Status: provisioning
🔗 Connected to 'main'A bucket belongs to one project for its whole life. It is created inside
the project in your .ghayma.json and cannot be moved to another one later.
Every site connected to it receives STORAGE_ENDPOINT, STORAGE_ACCESS_KEY,
STORAGE_SECRET_KEY, STORAGE_BUCKET and STORAGE_REGION — the access key
being one minted for that connection — so there is no
attach step to run afterwards. See When a connection
applies.
ghayma storage list
List all storage buckets.
ghayma storage listOutput (columns: name, status, used/limit, the project the bucket
belongs to, 🌐 for public buckets):
📦 Your storage buckets (3):
my-assets active 12.4 MB/10.0 GB my-app
user-uploads active 2.1 GB/50.0 GB my-app 🌐
backups stopped 0 B/10.0 GB my-appghayma storage info
Show details about a bucket.
ghayma storage info my-assetsOutput:
📦 Bucket: my-assets
S3 Bucket: st-myassets-a1b2c3d4
Status: active
Storage: 12.4 MB / 10.0 GB
Public: false
Project: <project-id>
Endpoint: https://s3.ghayma.techWhen the bucket has expose enabled, an extra Public URL: line appears
(see storage expose below).
ghayma storage credentials
Show where a bucket answers and which apps receive its keys — without any key.
ghayma storage credentials my-assetsOutput:
🔌 Connection details for bucket 'my-assets'
Endpoint: https://s3.ghayma.tech
Bucket: st-myassets-a1b2c3d4
Connected sites and the variables they receive:
main read-write STORAGE_ENDPOINT, STORAGE_ACCESS_KEY, STORAGE_SECRET_KEY, STORAGE_BUCKET, STORAGE_REGION, STORAGE_MY_ASSETS_ENDPOINT, STORAGE_MY_ASSETS_ACCESS_KEY, STORAGE_MY_ASSETS_SECRET_KEY, STORAGE_MY_ASSETS_BUCKET, STORAGE_MY_ASSETS_REGION
No key is shown: each site has its own key, delivered in these variables.
From your laptop: ghayma env pull
From outside Ghayma: ghayma access add bucket my-assets --name <principal>Endpoint is the one the platform reports for the bucket; a platform too old to
report it leaves the row out. Bucket is the S3 bucket name to pass to a client.
Each connected site comes with its
level and the exact variable names
it receives; the values hold that site’s own key, and
ghayma env pull writes them for one site. A
bucket no site is connected to says so in place of the list:
Not connected to any site. Connect one with: ghayma connect bucket my-assets --site <slug>A bucket’s own key is never shown, to anyone. Every app connected to it
holds a key of its own, able to reach that bucket and nothing else — see Each
app gets its own storage
key. From your laptop,
ghayma env pull writes your site’s
STORAGE_* variables: the S3 endpoint is public, so they work from your
machine as they are. For a tool or person outside Ghayma, add a named
principal: its own key pair, read or read-write,
an optional source allowlist, revocable on its own.
This output needs CLI v0.14.0 or later. An older CLI asks the platform for the bucket’s own key, which is refused now: it prints the platform’s sentence, which names the same ways in.
Rotating: a bucket’s own key is never handed out, and only an operator
rotates it. To give one app a new key, run ghayma connections rotate bucket <name> --site <slug>.
ghayma storage expose / storage unexpose
Enable or disable a public website for the bucket. When exposed, the bucket
is served read-only at https://<bucket-name>.web.ghayma.tech (useful
for static assets, public image hosting, etc.).
ghayma storage expose my-assets
ghayma storage unexpose my-assetsExpose output:
✅ Bucket 'my-assets' is now publicly accessible
🌐 Public URL: https://st-myassets-a1b2c3d4.web.ghayma.techThat ghayma host is what the platform serves and hands out everywhere — the expose response, every bucket read and this command all name it.
Exposed buckets are fully public — any path under the bucket is readable. Don’t expose buckets that contain private user data.
ghayma storage delete
Permanently delete a bucket and all its files. Requires typing the bucket name to confirm:
⚠️ This will permanently delete 'my-assets' and all its files.
Type the bucket name to confirm:ghayma storage delete my-assetsThis permanently deletes the bucket and every file inside. This action cannot be undone.
Using storage in your app
Read the bucket’s injected env vars from any S3-compatible SDK:
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({
endpoint: process.env.STORAGE_ENDPOINT,
region: process.env.STORAGE_REGION,
credentials: {
accessKeyId: process.env.STORAGE_ACCESS_KEY!,
secretAccessKey: process.env.STORAGE_SECRET_KEY!,
},
forcePathStyle: true, // required for Garage
// Required for AWS SDK v3 >= 3.729.0: Garage rejects the SDK's new
// default CRC32 checksums (400 InvalidDigest), especially on presigned
// PUTs. See the guide linked below.
requestChecksumCalculation: "WHEN_REQUIRED",
responseChecksumValidation: "WHEN_REQUIRED",
});
await s3.send(new PutObjectCommand({
Bucket: process.env.STORAGE_BUCKET,
Key: "images/photo.jpg",
Body: fileBuffer,
}));The two WHEN_REQUIRED options are required with @aws-sdk/client-s3
v3.729.0+. Without them, uploads (especially presigned PUTs) fail with
400 InvalidDigest. See S3 Checksums for the full
explanation and examples for boto3, Go, and the AWS CLI.
The key in STORAGE_ACCESS_KEY belongs to this app’s connection: it reaches
this one bucket, and never deletes it or changes its settings. Connect the app
at level read and the same code still runs, but PutObjectCommand is refused
— see Read-only connections.
Or use the typed helpers from the SDK for a higher-level interface.