Custom Object Metadata
Attach arbitrary key-value pairs to any storage object. Set them on upload, edit them independently of the file bytes, and query them later — without re-uploading or relying on path conventions.
What it's for
File paths and content types only carry so much. Metadata gives you a structured place to record application-level facts about each object — the user who owns it, the content version it belongs to, an alt-text caption, a language code, the originating device, an editorial status. Everything that doesn't belong inside the file but is useful to know about the file.
user_idusr_a4f9b21cWho uploaded thisalt_textSunset over the AtlanticAccessibility captioncontent_version3Editorial revision countersource_deviceios_pro_maxWhere the upload came fromlanguageen_USLocale of the contentSetting metadata on upload
Pass a metadata map alongside the rest of the upload arguments. The SDK forwards it in the confirm step, so the metadata lands at the same moment the object is recorded.
final object = await koolbase.storage.upload(
file: file,
bucket: 'avatars',
path: 'user_abc.jpg',
metadata: {
'user_id': 'usr_a4f9b21c',
'orig_filename': 'IMG_0942.HEIC',
'content_version': '1',
},
);
print(object.metadata['user_id']); // 'usr_a4f9b21c'const object = await koolbase.storage.upload({
file,
bucket: 'avatars',
path: 'user_abc.jpg',
metadata: {
user_id: 'usr_a4f9b21c',
orig_filename: 'IMG_0942.HEIC',
content_version: '1',
},
});
console.log(object.metadata.user_id); // 'usr_a4f9b21c'POST /v1/sdk/storage/confirm
x-api-key: pk_live_...
{
"bucket": "avatars",
"path": "user_abc.jpg",
"content_type": "image/jpeg",
"size": 204800,
"etag": "9e107d9d372bb6826bd81d3542a419d6",
"metadata": {
"user_id": "usr_a4f9b21c",
"orig_filename": "IMG_0942.HEIC",
"content_version": "1"
}
}Upload metadata replaces, doesn't merge
overwrite: true creates a new object at an existing path, the new upload's metadata fully replaces the old one. This mirrors how object stores treat overwrites — a new upload is a new object, not a patch of the old.Updating metadata
Use updateMetadata to add, change, or remove keys without re-uploading the file. The call applies merge semantics: keys you send with a string value are set or updated, keys you send with null are deleted, and keys you don't mention are left untouched.
"alt_text": "New caption""alt_text": null(leave it out)final updated = await koolbase.storage.updateMetadata(
bucket: 'avatars',
path: 'user_abc.jpg',
metadata: {
'alt_text': 'New caption', // sets or updates
'orig_filename': null, // deletes
// 'user_id' is omitted — left as-is on the server
},
);const updated = await koolbase.storage.updateMetadata(
'avatars',
'user_abc.jpg',
{
alt_text: 'New caption', // sets or updates
orig_filename: null, // deletes
// user_id is omitted — left as-is on the server
},
);PATCH /v1/sdk/storage/objects/metadata
x-api-key: pk_live_...
{
"bucket": "avatars",
"path": "user_abc.jpg",
"metadata": {
"alt_text": "New caption",
"orig_filename": null
}
}The merge runs atomically server-side
Reading metadata
Every storage object response carries the current metadata as a metadata field. It's always present — an empty map ({}) when no keys have been set — so you never have to null-check before reading.
final object = await koolbase.storage.getObject(
bucket: 'avatars',
path: 'user_abc.jpg',
);
final version = object.metadata['content_version'] ?? '0';
final alt = object.metadata['alt_text'];const object = await koolbase.storage.getObject('avatars', 'user_abc.jpg');
const version = object.metadata.content_version ?? '0';
const alt = object.metadata.alt_text;Editing in the dashboard
In the dashboard bucket browser, every file row has a tag icon next to the download and delete buttons. Clicking it opens a metadata editor showing every current key/value pair, with controls to add, edit, or remove entries. Saving applies the same merge semantics as the SDK — changes you make are diffed against the original snapshot and only the differences are sent.
The validation budget is shown alongside the form
Validation rules
Metadata is meant for short, structured tags — not for stashing arbitrary blobs of data. Server-side limits enforce this:
≤ 50 keys per object
Counted after the merge is applied — adding a 51st key fails even if you also delete one in the same request.
≤ 8 KB total size
Sum of all key lengths + all value lengths (UTF-8 bytes).
Keys: 1–64 characters
Must match [a-z0-9_]+. Lowercase letters, digits, and underscores only.
Values: ≤ 1024 characters
Any printable string — UTF-8 is allowed.
Reserved prefix
Keys starting with _ are reserved for system use and rejected.
Validation runs against the merged state
Errors
Validation failures return HTTP 400 with wire code metadata_invalid and a detail field naming the failing key and rule:
// HTTP 400
{
"error": "metadata is invalid",
"code": "metadata_invalid",
"detail": "key \"bad key\": must match [a-z0-9_]+"
}The SDKs surface this as a typed exception so you can branch on it without parsing strings:
try {
await koolbase.storage.updateMetadata(
bucket: 'avatars',
path: 'user_abc.jpg',
metadata: {'bad key': 'value'},
);
} on KoolbaseStorageMetadataInvalidException catch (e) {
// e.detail names the offending key and rule
print(e.detail);
}import { KoolbaseStorageMetadataInvalidError } from '@koolbase/react-native';
try {
await koolbase.storage.updateMetadata('avatars', 'user_abc.jpg', {
'bad key': 'value',
});
} catch (err) {
if (err instanceof KoolbaseStorageMetadataInvalidError) {
// err.detail names the offending key and rule
console.error(err.detail);
}
}Reacting to metadata changes
Every metadata edit fires a storage.object.updated event. Bind a function to that event to run side effects when tags change — re-indexing, cache invalidation, downstream notifications. See Triggers for the binding details and payload shape.
API reference
Full request/response schemas for the metadata endpoints live in the Storage API reference: PATCH /v1/sdk/storage/objects/metadata for SDK callers, and PATCH /v1/projects/{id}/buckets/{name}/objects/metadata for dashboard / admin callers.