The complete Dbrij Storage reference: buckets, objects, presigned uploads, the S3 endpoint, access keys, versioning, lifecycle, public links and plans.
Dbrij Storage keeps files. Two faces, one allowance: Buckets, which your code reaches through the REST API below or through the S3 protocol with any tool that speaks it, and Files, the drive people use from the app with folders, sharing and editors. A person's storage is theirs; a company's is the company's, and every meter and cap is by that principal.
Bytes live on an S3 compatible object store. Bucket uploads go straight to the store through presigned URLs, so a large file never streams through the API; Drive files go through the Dbrij API. Every plan includes transfer each month (10 GB on Free). Past it, Free stops serving until the next period and paid plans bill ₦15 a GB at renewal.
https://api.dbrij.com/api/storagehttps://s3.dbrij.comhttps://files.dbrij.comREST calls carry Authorization: Bearer <accessKeyId>:<secret>. S3 calls sign with the same id and secret as AWS credentials. Keys are minted on the Access keys page; the secret is shown once.
Three calls put a file in a bucket and read it back. Every step also works with the AWS CLI, shown afterwards.
curl https://api.dbrij.com/api/storage/buckets \
-H "Authorization: Bearer dbsk_…:dbss_…" \
-H "Content-Type: application/json" \
-d '{ "name": "my-assets" }'curl https://api.dbrij.com/api/storage/buckets/<bucketId>/upload-url \
-H "Authorization: Bearer dbsk_…:dbss_…" \
-H "Content-Type: application/json" \
-d '{ "key": "images/logo.png", "contentType": "image/png" }'
# → { "url": "https://…", "headers": { "Content-Type": "image/png" } }
curl -X PUT "<url from the response>" -H "Content-Type: image/png" --data-binary @logo.pngcurl https://api.dbrij.com/api/storage/buckets/<bucketId>/confirm \
-H "Authorization: Bearer dbsk_…:dbss_…" \
-H "Content-Type: application/json" \
-d '{ "key": "images/logo.png" }'export AWS_ACCESS_KEY_ID=dbsk_…
export AWS_SECRET_ACCESS_KEY=dbss_…
aws --endpoint-url https://s3.dbrij.com s3 mb s3://my-assets
aws --endpoint-url https://s3.dbrij.com s3 cp logo.png s3://my-assets/images/logo.png
aws --endpoint-url https://s3.dbrij.com s3 ls s3://my-assets/images/Over S3 there is no confirm step: the store tells Dbrij when the bytes land. Use whichever surface your stack already speaks. Most frameworks have an S3 storage driver, and pointing it here is a two line change.
An access key is an id (dbsk_…) and a secret (dbss_…) with a scope of read, write or both. A key reaches only the buckets of the space it was made in, over REST and S3 alike: a key made in your personal space reaches your buckets, and a key made while working in a company reaches the company's buckets and bills to the company. A company key stops working when the person who made it loses Storage or Hosting access in that company; make a new one from someone who still has it. The secret is shown once, at creation.
WHAT EACH SCOPE ALLOWS
| read | scope | List buckets and objects, download, and every S3 GET, HEAD and LIST. |
| write | scope | Create buckets, upload, delete, copy, multipart, and bucket configuration over S3. |
Both address forms work: path style (https://s3.dbrij.com/my-assets/key) and virtual host style (my-assets.s3.dbrij.com/key). The snippets below use path style, which every S3 tool supports. Presigned S3 URLs from any SDK verify the same way.
DBRIJ_KEY_ID=dbsk_…
DBRIJ_KEY_SECRET=dbss_…
DBRIJ_S3_ENDPOINT=https://s3.dbrij.com
DBRIJ_S3_REGION=auto
DBRIJ_BUCKET=my-assetsThese are the names the snippets read, and the names Dbrij writes into a hosted app when an AI assistant connects Storage to it. Any region name works: it is part of the signature, not a location.
rclone config create dbrij s3 provider=Other access_key_id=dbsk_… secret_access_key=dbss_… endpoint=https://s3.dbrij.com force_path_style=true
rclone sync ./public dbrij:my-assets/publicimport { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({
endpoint: process.env.DBRIJ_S3_ENDPOINT,
region: process.env.DBRIJ_S3_REGION,
forcePathStyle: true,
credentials: { accessKeyId: process.env.DBRIJ_KEY_ID!, secretAccessKey: process.env.DBRIJ_KEY_SECRET! },
});
await s3.send(new PutObjectCommand({ Bucket: process.env.DBRIJ_BUCKET, Key: 'images/logo.png', Body: bytes, ContentType: 'image/png' }));'dbrij' => [
'driver' => 's3',
'key' => env('DBRIJ_KEY_ID'),
'secret' => env('DBRIJ_KEY_SECRET'),
'region' => env('DBRIJ_S3_REGION', 'auto'),
'bucket' => env('DBRIJ_BUCKET'),
'endpoint' => env('DBRIJ_S3_ENDPOINT'),
'use_path_style_endpoint' => true,
],
// Storage::disk('dbrij')->put('images/logo.png', $bytes);import os
AWS_ACCESS_KEY_ID = os.environ["DBRIJ_KEY_ID"]
AWS_SECRET_ACCESS_KEY = os.environ["DBRIJ_KEY_SECRET"]
AWS_S3_ENDPOINT_URL = os.environ["DBRIJ_S3_ENDPOINT"]
AWS_S3_REGION_NAME = os.environ.get("DBRIJ_S3_REGION", "auto")
AWS_STORAGE_BUCKET_NAME = os.environ["DBRIJ_BUCKET"]
AWS_S3_ADDRESSING_STYLE = "path"
STORAGES = {
"default": {"BACKEND": "storages.backends.s3.S3Storage"},
"staticfiles": {"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"},
}Rotation. Mint the new key, move your deployments to it, then revoke the old one. A revoked key fails every call immediately with 401. Keys made before the S3 endpoint existed show as REST only on the keys page and cannot sign S3 requests; mint a new one.
Scoped keys (Pro plan and up) are narrowed at creation: one bucket, a prefix under it, an end date, an IP allowlist. Out of scope calls fail with 403 on REST and AccessDenied over S3; an expired key fails with 401. A key for one service that can only write uploads/ in one bucket and stops working next quarter is the key to put in that service's environment. An AI assistant connecting an app to one bucket needs a scoped key too: below Pro it is told to connect a key for the whole space instead, or to move up a plan.
@dbrij/storage is a zero dependency client for Node 18+ and browsers. It covers what S3 does not: delivery links, transformations, presets, and the confirm step REST uploads need. Keep using an AWS SDK for everything else if you already do.
npm install @dbrij/storageimport { DbrijStorage, transform } from '@dbrij/storage';
const storage = new DbrijStorage({ keyId: process.env.DBRIJ_KEY_ID, secret: process.env.DBRIJ_KEY_SECRET });
const bucket = await storage.buckets.create('my-assets');
await storage.objects.upload(bucket.id, 'images/logo.png', bytes, { contentType: 'image/png' });
const link = await storage.delivery.url(bucket.id, 'images/logo.png', { transform: transform({ width: 800, format: 'auto' }) });
// link.url → https://files.dbrij.com/t/w_800,f_auto/<bucketId>/images/logo.pngimport { DbrijStorage } from '@dbrij/storage';
const file = await DbrijStorage.presetUpload('dbup_…', input.files[0], { onProgress: (f) => bar.style.width = f * 100 + '%' });
img.src = file.deliveryUrl;METHODS
| buckets.list / create | key | The buckets the key can reach; create a private or public one. |
| objects.list / upload / delete / downloadUrl | key | upload presigns, PUTs the bytes and confirms in one call, with progress in browsers. |
| delivery.url | key | A delivery link, signed for private objects, transformed for images. |
| DbrijStorage.presetUpload | no key | A keyless upload through a preset, for browsers and mobile. |
| transform(...) | helper | Named options → the token string, so code reads width: 800 rather than w_800. |
Python, Go and PHP: point the AWS SDK for that language at https://s3.dbrij.com and the key id and secret, and call the REST endpoints in this reference for delivery links and presets. Dedicated packages follow.
A bucket is a named space for objects. It is private by default: nothing in it can be read without a signed URL or a key. Switching it public makes every object's stable link readable by anyone. A public bucket can also be put on signed, expiring links instead of permanent ones, for downloads you want to be able to withdraw.
/storage/bucketsreadList buckets
Every bucket the key can reach: a personal key sees the person's buckets, a company key the company's. Counts and bytes ride along.
curl -X GET https://api.dbrij.com/api/storage/buckets \
-H "Authorization: Bearer $DBRIJ_API_KEY"{
"success": true,
"data": [{ "id": "b1…", "name": "my-assets", "visibility": "private", "versioning": null, "objectCount": 128, "totalBytes": 91827364, "createdAt": "…" }]
}/storage/bucketswriteCreate a bucket
Names are 3 to 63 lowercase letters, numbers and hyphens, unique within the space (your personal space, or one company), so another space can use the same name. A public bucket serves permanent links from the moment it exists.
PATH / BODY PARAMETERS
| name* | string | The bucket name, used in every S3 path. |
| visibility | "private" | "public" | Private by default: objects are reachable only through signed URLs. |
curl -X POST https://api.dbrij.com/api/storage/buckets \
-H "Authorization: Bearer $DBRIJ_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "my-assets", "visibility": "private" }'{ "name": "my-assets", "visibility": "private" }{
"success": true,
"data": { "id": "b1…", "name": "my-assets", "visibility": "private", "objectCount": 0, "totalBytes": 0 }
}Over S3, CreateBucket, ListBuckets, DeleteBucket, PutBucketVersioning, PutBucketLifecycleConfiguration and PutBucketPolicy are all served. Deleting a bucket needs it empty, as everywhere.
Keys are paths: slashes make folders, and a key ending in a slash is a folder marker with no bytes. Objects can be up to five gigabytes each, subject to the plan's allowance. Uploads never stream through the API: REST hands you a presigned PUT, S3 uploads go to the endpoint directly, multipart included.
/storage/buckets/:bucketId/objectsreadList objects
Objects in a bucket, optionally under a key prefix. Folder markers (keys ending in a slash) come back with isFolder true and no bytes.
PATH / BODY PARAMETERS
| prefix | string | Query parameter. Only keys starting with this, e.g. images/. |
curl -X GET https://api.dbrij.com/api/storage/buckets/:bucketId/objects \
-H "Authorization: Bearer $DBRIJ_API_KEY"{
"success": true,
"data": [{ "key": "images/logo.png", "size": 18233, "contentType": "image/png", "isFolder": false, "url": null, "updatedAt": "…" }]
}/storage/buckets/:bucketId/upload-urlwriteGet a presigned upload URL
Step one of an upload. The URL points at the object store, not the API: PUT the raw bytes there with the headers returned, with no Authorization header and no JSON envelope. It expires after fifteen minutes.
PATH / BODY PARAMETERS
| key* | string | The object key, up to 1,024 characters. Slashes make folders. |
| contentType | string | Sent as the object's Content-Type. Defaults to application/octet-stream. |
curl -X POST https://api.dbrij.com/api/storage/buckets/:bucketId/upload-url \
-H "Authorization: Bearer $DBRIJ_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "key": "images/logo.png", "contentType": "image/png" }'{ "key": "images/logo.png", "contentType": "image/png" }{
"success": true,
"data": { "url": "https://…", "method": "PUT", "headers": { "Content-Type": "image/png" }, "key": "images/logo.png", "expiresIn": 900 }
}/storage/buckets/:bucketId/confirmwriteConfirm an upload
Step two. Dbrij reads the object's real size from the store and records it, and the object shows from then on. An upload left unconfirmed is settled after a day: kept and counted as if confirmed when its bytes landed, dropped when nothing did.
PATH / BODY PARAMETERS
| key* | string | The key you uploaded to. |
curl -X POST https://api.dbrij.com/api/storage/buckets/:bucketId/confirm \
-H "Authorization: Bearer $DBRIJ_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "key": "images/logo.png" }'{ "key": "images/logo.png" }{
"success": true,
"data": { "key": "images/logo.png", "size": 18233, "contentType": "image/png", "url": null }
}/storage/buckets/:bucketId/object?key=…readDownload an object
Redirects (302) to a signed URL for the bytes, good for one hour. Follow the redirect, or read the Location header to hand the URL to a browser.
curl -X GET https://api.dbrij.com/api/storage/buckets/:bucketId/object?key=… \
-H "Authorization: Bearer $DBRIJ_API_KEY"/storage/buckets/:bucketId/object?key=…writeDelete an object
On a versioned bucket this writes a delete marker and keeps the previous version, which you restore over S3; on an unversioned bucket the bytes are gone.
curl -X DELETE https://api.dbrij.com/api/storage/buckets/:bucketId/object?key=… \
-H "Authorization: Bearer $DBRIJ_API_KEY"{
"success": true,
"data": { "deleted": true }
}Big files. Use S3 multipart for anything over a hundred megabytes: CreateMultipartUpload, UploadPart, ListParts, CompleteMultipartUpload and UploadPartCopy are served, and every SDK's high level upload helper uses them for you.
Copy. CopyObject copies within and between your buckets on the server, no download.
Metadata. x-amz-meta-*, Cache-Control, Content-Disposition and Content-Encoding sent on PutObject or CreateMultipartUpload are kept and returned on GET and HEAD; CopyObject with x-amz-metadata-directive: REPLACE replaces them. Listings carry ETags. Object tagging, object lock, retention and legal holds are not available and answer NotImplemented.
Browser uploads. A presigned URL is safe to hand to a browser: it carries a signature, never your key, and it expires. Both kinds work from any origin, localhost included: the URL the REST call returns, and one you presign yourself with an S3 SDK against the S3 endpoint. CORS is answered for every origin, because the signature, not the origin, is what authorizes an upload, so there is no CORS configuration to set: PutBucketCors is refused with that reason and GetBucketCors shows the policy in force. For uploads with no backend at all, use an upload preset.
Three kinds of link, depending on what the bucket allows.
LINK TYPES
| Stable public link | public bucket, or a public prefix | Permanent, cacheable, no signature. The object's url field in every listing. Overwrite the key and the link serves the new bytes. |
| Signed read | any bucket | A URL good for one hour from the download endpoint, or for any lifetime you sign with the S3 SDK (GetObject presign). Shareable, expiring, withdrawable by rotating the key. |
| Signed public link | public bucket with signed reads on | For public buckets that should not be hotlinkable forever: links carry a signature and expire, but no key is needed to mint them. |
https://files.dbrij.com/<bucketId>/images/logo.png # a public object
https://files.dbrij.com/<bucketId>/images/logo.png?dl=1 # served as a download
https://files.dbrij.com/<bucketId>/private.pdf?exp=1760000000&sig=… # a private object, signed
https://files.dbrij.com/t/w_800,c_fill,f_auto/<bucketId>/images/logo.png # transformed/storage/buckets/:bucketId/delivery-urlreadMake a delivery link
Public objects get a permanent link. Private objects, and public buckets set to signed reads, get a signed link that expires. Add a transformation for images.
PATH / BODY PARAMETERS
| key* | string | The object key. |
| transform | string | Transformation tokens, e.g. w_800,c_fill,f_auto. Images only. |
| expiresIn | number | Seconds a signed link lives. Default 3600, max thirty days. |
| download | boolean | Serve with Content-Disposition: attachment. |
curl -X POST https://api.dbrij.com/api/storage/buckets/:bucketId/delivery-url \
-H "Authorization: Bearer $DBRIJ_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "key": "images/hero.jpg", "transform": "w_1200,f_auto,q_80", "expiresIn": 86400 }'{ "key": "images/hero.jpg", "transform": "w_1200,f_auto,q_80", "expiresIn": 86400 }{
"success": true,
"data": { "url": "https://…/t/w_1200,f_auto,q_80/<bucketId>/images/hero.jpg?exp=…&sig=…", "expiresAt": "…", "signed": true }
}The delivery host streams the bytes with range support, ETag and Last-Modified, so browsers revalidate cheaply and video seeks. Every byte served counts against the plan's monthly transfer allowance: delivery links and custom domains, S3 downloads, and download links from the dashboard or REST. Past the allowance Free stops serving (429) until the next period and is never billed; paid plans keep serving and bill ₦15 a GB at renewal.
Put tokens in the path and the delivery host resizes, crops and re-encodes the image on first request, caches the result, and serves it from the cache after that. Only images transform; anything else is served as is. A transformation counts against the plan's allowance the first time it runs, never on a cache hit.
https://files.dbrij.com/t/<tokens>/<bucketId>/<key>
https://cdn.example.com/t/<tokens>/<key> # on a custom domainTOKENS
| w_800, h_600 | size | Width and height in pixels, up to 4096. One alone keeps the aspect ratio. |
| c_fill | fit | scale | pad | crop | crop | fill covers the box and crops; fit sits inside it without enlarging; scale stretches; pad letterboxes with the background; crop cuts to the box. |
| g_auto | face | center | north | south | east | west | northeast … | gravity | Where fill and crop keep. auto and face both crop to the most striking region, which is often a face; there is no face detection. |
| f_auto | webp | avif | jpg | png | gif | format | auto serves AVIF or WebP when the browser accepts it, else the source format, and never drops transparency into JPEG. |
| q_80 | quality | 1 to 100. Defaults suit each format. |
| dpr_2 | density | Multiplies w and h for high density screens, up to 3. |
| r_20 | r_max | corners | Rounded corners in pixels, or max for a circle. |
| b_ffffff | background | Hex colour behind pad and under transparency flattened into JPEG. |
| a_90 | rotate | Quarter turns clockwise. EXIF orientation is honoured on its own. |
| e_blur:200, e_grayscale, e_sharpen | effects | Blur strength up to 2000, greyscale, and a sharpen pass. |
| flip, flop | mirror | Vertical and horizontal mirror. |
Unknown tokens are refused with the token named, never ignored. Sources up to 40 MB transform; SVG and icons are served untouched. Media CDN style paths (/image/upload/w_300,c_fill/v1712/folder/file.jpg) are read too, on the delivery host after the bucket id and on custom domains directly, so URLs copied from an existing site keep working after a switch.
An upload preset opens one bucket prefix to keyless uploads from a browser or a mobile app, inside limits you set: accepted types, a size cap, and the origins allowed. The preset token (dbup_…) is safe in page source. Create presets on the bucket page. Preset uploads answer browser requests from any site; the preset's own allowed origins still decide which ones are let through.
<script src="https://storage.dbrij.com/storage-widget.js"></script>
<button id="upload">Upload files</button>
<script>
DbrijStorage.attach(document.querySelector('#upload'), {
preset: 'dbup_…',
onUploaded: (file) => console.log(file.key, file.deliveryUrl),
});
// or without UI: DbrijStorage.upload('dbup_…', fileInput.files[0]).then(console.log)
</script>/storage/uploads/:presetTokennoneStart a preset upload
No key. Returns a presigned PUT for a key under the preset prefix; PUT the bytes there with the headers given, then confirm.
PATH / BODY PARAMETERS
| filename* | string | Used for the key's last segment, sanitised. |
| contentType | string | Checked against the preset's allowed types. |
| size | number | Checked against the cap up front; the real size is checked again at confirm. |
curl -X POST https://api.dbrij.com/api/storage/uploads/:presetToken \
-H "Authorization: Bearer $DBRIJ_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "filename": "avatar.png", "contentType": "image/png", "size": 18233 }'{ "filename": "avatar.png", "contentType": "image/png", "size": 18233 }{
"success": true,
"data": { "uploadUrl": "https://…", "method": "PUT", "headers": { "Content-Type": "image/png" }, "key": "uploads/2026/09/1a2b3c4d-avatar.png", "expiresIn": 900, "confirmUrl": "…" }
}/storage/uploads/:presetToken/confirmnoneConfirm a preset upload
Reads what actually landed. Past the size cap or outside the allowed types, the bytes are deleted and the call fails; otherwise the object exists and a delivery link good for a day comes back.
curl -X POST https://api.dbrij.com/api/storage/uploads/:presetToken/confirm \
-H "Authorization: Bearer $DBRIJ_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "key": "uploads/2026/09/1a2b3c4d-avatar.png" }'{ "key": "uploads/2026/09/1a2b3c4d-avatar.png" }{
"success": true,
"data": { "key": "…", "size": 18233, "contentType": "image/png", "url": null, "deliveryUrl": "https://…?exp=…&sig=…" }
}Put your own hostname in front of a bucket. cdn.example.com/images/hero.jpg serves the object, cdn.example.com/t/w_800/images/hero.jpg the transformation, and media CDN style paths are read as well. The plan sets how many domains a space may attach.
DNS
| CNAME | recommended | Point the hostname at files.dbrij.com. Verification passes on the CNAME alone and a certificate is issued on first request. |
| TXT | ownership only | Put the token from the bucket page at _dbrij.<hostname>. Use it when the hostname must stay pointed elsewhere while you prove ownership. |
Add the domain on the bucket page, add the record, press Check. Signed links work on custom domains too; the signature does not depend on the host.
Turn versioning on for a bucket and every write keeps history, whichever way it comes in: the dashboard, the REST API, the SDK, upload presets, imports and S3. An overwrite keeps the previous bytes as a version, a delete writes a delete marker instead of removing anything, and deleting the marker brings the object back. Versions count against the allowance until a lifecycle rule or an explicit delete removes them. Versioning, restores and lifecycle rules are set over S3; the dashboard has no screen for them yet.
aws --endpoint-url https://s3.dbrij.com s3api put-bucket-versioning \
--bucket my-assets --versioning-configuration Status=Enabledaws --endpoint-url https://s3.dbrij.com s3api list-object-versions --bucket my-assets --prefix images/logo.png
aws --endpoint-url https://s3.dbrij.com s3api delete-object --bucket my-assets --key images/logo.png --version-id <marker id>Lifecycle rules run every hour. The applied subset is Expiration.Days (current objects older than this are deleted, a delete marker on a versioned bucket), NoncurrentVersionExpiration.NoncurrentDays (previous versions removed for good) and ExpiredObjectDeleteMarker. Transitions, tag filters and date based rules are refused with a reason rather than silently ignored.
aws --endpoint-url https://s3.dbrij.com s3api put-bucket-lifecycle-configuration --bucket my-assets --lifecycle-configuration '{
"Rules": [{ "ID": "prune-versions", "Status": "Enabled", "Filter": { "Prefix": "" },
"NoncurrentVersionExpiration": { "NoncurrentDays": 30 } }]
}'A whole bucket can be public, or just a prefix of it: public/ readable by anyone while everything else stays private. Set it from the bucket page, or with a bucket policy over S3. Dbrij reads the policy and maps it to what it enforces; anything finer than "public read on this prefix" is refused with a reason, never accepted and silently unenforced. Once a prefix is public, a plain unsigned GET or HEAD on the S3 endpoint serves the object, the same as the delivery link; lists, writes and version reads still need a signed request.
aws --endpoint-url https://s3.dbrij.com s3api put-bucket-policy --bucket my-assets --policy '{
"Version": "2012-10-17",
"Statement": [{ "Effect": "Allow", "Principal": "*", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::my-assets/public/*" }]
}'Object level ACLs (x-amz-acl: public-read) on a private bucket are refused with guidance, because an ACL that quietly did nothing is the kind of thing that leaks a file a year later.
A bucket can seal every new object with a key you hold in Dbrij Secrets Key management. Each object gets its own data key, wrapped by your key for that bucket alone; the store only ever holds sealed bytes, and the wrapped key is kept with the object's record, never beside its bytes. Disable or destroy the key in Key management and the bucket's sealed objects cannot be read by anyone, Dbrij included.
Object events delivered to your own https endpoints (Starter plan and up), signed the way every Dbrij webhook is signed. Add endpoints on the Webhooks page; each one gets a secret shown once and a delivery log you can replay from.
EVENTS
| object.created | event | An object was uploaded, overwritten, copied or imported. data: bucketId, key, size, contentType, versionId. |
| object.deleted | event | An object was deleted through the console, the REST API or S3. Deleting a folder sends one for every object under it. data: bucketId, key. |
| import.finished | event | An import completed. data: bucketId, importId, source, copied, skipped, failed, bytes. |
| bucket.created / bucket.deleted | event | data: bucketId, name. |
POST https://example.com/hooks/dbrij
Content-Type: application/json
X-Dbrij-Event: object.created
X-Dbrij-Signature: sha256=<hmac(secret, body)>
X-Dbrij-Signature-V2: t=1760000000,sha256=<hmac(secret, "1760000000." + body)>
{ "id": "evt_…", "type": "object.created", "createdAt": "…", "data": { "bucketId": "…", "key": "images/logo.png", "size": 18233, "contentType": "image/png" } }import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(secret, body, header) {
const [t, sig] = header.split(',').map((p) => p.split('=')[1]);
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = createHmac('sha256', secret).update(t + '.' + body).digest('hex');
return timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}Reply with any 2xx within ten seconds. Anything else is retried four more times over a few hours; twenty consecutive failures switch the endpoint off until you turn it back on. Deliveries are kept for thirty days.
Files is the drive: what a person or a company keeps without writing code. Folders, sharing with members or by email, links anyone can open, trash, versions of edited files, and editors for documents, spreadsheets and presentations. It sits on the same allowance as the buckets, so a company buys storage once.
WHAT LIVES WHERE
| Files | people | Drive and Documents in the app and on mobile. Sharing, links, editors, trash. Counted as driveBytes. |
| Buckets | code | Objects reached by key or by S3. Public links, presigned uploads, versioning, lifecycle. Counted as bucketBytes. |
| Repositories | code | Source git repositories, counted against the same allowance. |
One click import from more cloud drives, and mounting Files as a disk over WebDAV, are on the roadmap and land with the switching tools below.
Because the endpoint speaks S3, most moves are a change of endpoint and key. This is what changes in the common stacks, and what does not.
FROM ANOTHER S3 COMPATIBLE STORE
| Endpoint | change | https://s3.dbrij.com, path style on. Region can stay whatever it was. |
| Credentials | change | A Dbrij access key id and secret in place of the old pair. |
| Bucket names | keep | Create buckets with the same names and your keys and paths stay identical. |
| Your code | keep | SDK calls, presigned URLs and multipart uploads are unchanged. |
| Copying the data | built in | Open the bucket, Delivery → Import, paste the source endpoint, bucket and keys, and Dbrij copies everything in the background (dry run first if you like). Sync mode skips what is already here, so run it again right before cutover. rclone works too. |
| Moving the hostname first | lazy fetch | Set the old bucket URL as the origin on the bucket page and point your delivery domain here now: any key not yet copied is pulled across on first request and kept. |
rclone config create old s3 provider=AWS access_key_id=… secret_access_key=… region=eu-west-1
rclone config create dbrij s3 provider=Other access_key_id=dbsk_… secret_access_key=dbss_… endpoint=https://s3.dbrij.com force_path_style=true
rclone sync old:my-assets dbrij:my-assets --transfers 16 --checksum
# run it again just before cutover: only what changed is copiedFROM A MEDIA CDN WITH URL TRANSFORMATIONS
| Uploads | change | An unsigned upload preset replaces the upload preset; the widget replaces the upload widget; signed uploads become a presigned PUT or an S3 PutObject. |
| Delivery URLs | mostly keep | The delivery host reads the common media CDN path syntax (/image/upload/w_300,c_fill/v123/folder/file.jpg) on a custom domain, so a CNAME from your media hostname keeps existing URLs serving. The common transformation tokens are the same. |
| Copying the data | built in | Open the bucket, Delivery → Import, choose Cloudinary and paste the cloud name, API key and secret. Every image, video and raw file is copied with its public id as the key, so /image/upload paths keep resolving. |
| Lazy fetch | zero downtime | Set https://res.cloudinary.com/<cloud>/image/upload as the origin and CNAME your media hostname here: anything not yet imported is pulled on first request. |
FROM A MOBILE BACKEND'S FILE STORAGE
| Uploads | change | Uploads through the backend's SDK become presigned PUTs or S3 PutObject calls. |
| Copying the data | built in | Most cloud object stores behind these backends offer S3 interoperability keys: make a pair in the provider console, then run an S3 import with its S3 endpoint (for example https://storage.googleapis.com) and your bucket name. |
FROM ANOTHER CLOUD DRIVE (FOR PEOPLE)
| One click import | built in | In Files, Create → Import from Google Drive. Sign in once and the whole drive is copied in the background with its folders intact; documents, spreadsheets and slides arrive as .docx, .xlsx and .pptx files you can keep editing. |
| Any other drive | today | Download the files and drop them onto Files. Folders are not uploaded whole: make each one with New folder and drop its files in. One click import for more drives is next. |
The allowance is one number for bucket objects and their kept versions, Drive files and their versions, and code repositories together. Storage is its own product: database backups belong to Hosting and do not count here, and a Hosting plan's storage is for those backups. Free stops uploads at the allowance. Paid plans keep accepting uploads and bill the gigabytes held above it by the day, at renewal, from the closed period, never the live month. Transfer works the same way each month: Free stops serving at its allowance and is never billed, paid plans bill ₦15 a GB past theirs.
Nothing is deleted when a plan ends. A lapsed plan drops to Free: files stay readable, and uploads stop while you hold more than Free includes.
Files nobody pays for. The one exception is long neglect, always with warnings first. If an account holds more than its plan includes with no paid plan for 12 months, or nobody signs in, uses its keys, uploads or downloads for 12 months, the files in its buckets are removed: above the free amount, newest first, or all of them when unused. The owner is emailed 90, 30 and 7 days before, never with less than 30 days to act, and a plan, a payment or a single sign in stops it. Files in Drive and code repositories are never removed this way.
| Plan | Storage | Transfer a month | Buckets | Keys | Over the allowance | Versioning |
|---|---|---|---|---|---|---|
| Free | 5 GB | 10 GB | 2 | 2 | Uploads stop | No |
| Starter | 100 GB | 500 GB | 25 | 10 | ₦25 per GB month | Yes |
| Pro | 500 GB | 2 TB | 1,000 | 50 | ₦25 per GB month | Yes |
| Business | 2.5 TB | 10 TB | 1,000 | 200 | ₦25 per GB month | Yes |
PLATFORM LIMITS
| Object size | 5 GB | Per object, on every plan. Use multipart above a hundred megabytes. |
| Key length | 1024 characters | UTF-8, no NUL. Slashes make folders. |
| Presigned upload | 15 minutes | Then mint another. An unconfirmed upload is settled after a day: counted if its bytes landed, dropped if not. |
| Signed download | 60 minutes | From the REST download endpoint. Sign your own lifetime over S3. |
ERRORS WORTH HANDLING
| 400 | bad request | A bad bucket name, a key past the limit, a plan cap reached (buckets, keys, or storage on Free). The message says which, and where to change it. |
| 401 | unauthorized | Missing, revoked or wrong key. Over S3: InvalidAccessKeyId or SignatureDoesNotMatch. |
| 403 | forbidden | The key is real but lacks the scope, or reaches for a bucket outside the space it was made in. |
| 404 | not found | No bucket by that id, or no object by that key. |
| 501 | not implemented | Over S3, a bucket policy or lifecycle rule finer than the enforced subset (the reason names the clause), or a feature not built, such as tagging, object lock or replication. |
| 503 | unavailable | The object store is not reachable. Existing links keep working through the signed path; retry shortly. |
Every SDK and package for Storage, with what it does, how to install it and the smallest working use. The whole catalogue, for every product, is on the apps page.
Buckets, uploads, delivery links and image transformations from Node or the browser.
SDK · TypeScript · npm · Runs on Node 18+, Browsers
npm install @dbrij/storageimport { DbrijStorage, transform } from '@dbrij/storage';const storage = new DbrijStorage({ keyId: process.env.DBRIJ_KEY_ID!, secret: process.env.DBRIJ_KEY_SECRET! });const bucket = await storage.buckets.create('my-assets');await storage.objects.upload(bucket.id, 'images/logo.png', bytes, { contentType: 'image/png' });const link = await storage.delivery.url(bucket.id, 'images/logo.png', { transform: transform({ width: 800, format: 'auto' }) });An upload window for any web page: people pick files and they land in your bucket through an upload preset, with no key in the page.
Script for your site · JavaScript · Served by Dbrij · Runs on Browsers
<script src="https://storage.dbrij.com/storage-widget.js"></script><script src="https://storage.dbrij.com/storage-widget.js"></script><script> DbrijStorage.attach(document.querySelector('#upload'), { preset: 'dbup_…', onUploaded: (file) => console.log(file.deliveryUrl), });</script>Dbrij is Nigeria’s company operating system: the first Nigerian built platform to put team chat, video meetings, email, HR, tax compliant payroll, customer support, marketing, analytics and cloud hosting in one product on one subscription.