Skip to content
Docs · Data

Storage

Private object storage with no egress fees. Upload and download with signed URLs, so file bytes go straight between the client and storage.

View as Markdown
All topics
On this page

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.

presignsigned URLbytes go direct — no egress feesyour app12Harakumo APIobject storagepresign once — uploads never touch your servers

Upload from the browser

  1. Your server signs a PUT for a key, naming the Content-Type.
  2. The browser PUTs the file to that URL with the same Content-Type header. Buckets accept browser uploads from any origin.
Server
const { url, expiresAt } = await hk.storage.presign(bucketId, {
  key: 'avatars/' + userId + '.png', method: 'put', contentType: 'image/png',
});
// hand url to the browser
Browser
await fetch(url, { method: 'PUT', headers: { 'content-type': 'image/png' }, body: file });

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
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/:id lists objects: prefix narrows to a path, delimiter=/ groups keys into folders, and limit (up to 1,000, default 100) with cursor pages through; nextCursor is set while more remain.
  • POST { key, method: "delete" } removes one object. DELETE /api/storage/:id empties 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 (402 usage_paused) only while a charge cannot be paid. Deleting files frees room once the bucket is measured again: daily, or right away with GET /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

SDK
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');