Files live in buckets; each bucket belongs to a project and is its own bucket in object storage. A file's key is its path (avatars/42.png); folders are just the part of the key before a slash.
Files are private. To upload or download, your server asks Harakumo for a signed URL for one key, and the client sends or fetches the bytes directly. There are no public bucket URLs and no S3-style access keys yet.
Upload from the browser
- Your server signs a PUT for a key, naming the Content-Type.
- The browser PUTs the file to that URL with the same Content-Type header. Buckets accept browser uploads from any origin.
const { url, expiresAt } = await hk.storage.presign(bucketId, {
key: 'avatars/' + userId + '.png', method: 'put', contentType: 'image/png',
});
// hand url to the browserawait fetch(url, { method: 'PUT', headers: { 'content-type': 'image/png' }, body: file });Download links
Sign a GET for a key and send the person (or an img tag) to the URL. Links last 15 minutes by default; pass expiresIn from 60 seconds to 7 days (604800). For files shown to many people, sign a longer link once and reuse it instead of signing on every view.
curl -X POST https://harakumo.com/api/storage/7 \
-H "Authorization: Bearer $HARAKUMO_API_KEY" -H "content-type: application/json" \
-d '{ "key": "reports/q3.pdf", "method": "get", "expiresIn": 86400 }'
# → { "url": "…", "expiresIn": 86400, "expiresAt": "…" }Listing and deleting
GET /api/storage/:idlists objects:prefixnarrows to a path,delimiter=/groups keys intofolders, andlimit(up to 1,000, default 100) withcursorpages through;nextCursoris set while more remain.POST { key, method: "delete" }removes one object.DELETE /api/storage/:idempties and deletes the bucket.- Sizes and file counts are measured daily (or on
?measure=1) and shown as a dash until a full count exists.
Who can do what
- Viewers and read-only keys can list and get download links.
- Uploading and deleting need the developer role (a full key).
- A key limited to the project works for all of it.
Limits and scaling
- One upload is at most 5 GB (one signed PUT; multipart is not offered yet).
- Keys are up to 1,024 characters.
- Buckets per plan: 2 hobby, 20 pro, 500 enterprise.
- File storage per plan, all buckets together: 1 GB hobby, 100 GB pro, 1,000 GB enterprise, measured daily (on Hobby, across all your free workspaces). On Hobby, new uploads are refused once the plan's file storage allowance is used up (402
usage_limit). On Pro and Enterprise, use past it is charged daily from the Balance at $0.05 per GB a month, and new uploads pause (402usage_paused) only while a charge cannot be paid. Deleting files frees room once the bucket is measured again: daily, or right away withGET /api/storage/:id?measure=1(for a bucket of up to 10,000 files). - The bytes never pass through Harakumo; each signed URL is one API call.
Reference
const { bucket } = await hk.storage.create(project.id, { name: 'uploads' });
const { url } = await hk.storage.presign(bucket.id, { key: 'file.png', method: 'put', contentType: 'image/png' });
const { url: download } = await hk.storage.presign(bucket.id, { key: 'file.png', method: 'get', expiresIn: 3600 });
const { objects, folders, nextCursor } = await hk.storage.objects(bucket.id); // first page
await hk.storage.deleteObject(bucket.id, 'file.png');harakumo storage 12 # list buckets
harakumo storage create 12 uploadsPOST /api/projects/:id/storage { "name": "uploads" }
POST /api/storage/:id { "key": "file.png", "method": "put"|"get"|"delete", "contentType"?, "expiresIn"? }
GET /api/storage/:id?prefix=&delimiter=/&limit=&cursor= → { objects, folders, nextCursor, truncated }
DELETE /api/storage/:id # empties and deletes the bucket