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 this
alt_textSunset over the AtlanticAccessibility caption
content_version3Editorial revision counter
source_deviceios_pro_maxWhere the upload came from
languageen_USLocale of the content

Setting 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.

Flutter
Dart
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'
React Native
TypeScript
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'
REST
HTTP
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

When 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.

Set / updatePass the key with a string value"alt_text": "New caption"
DeletePass the key with null"alt_text": null
PreserveOmit the key from the payload entirely(leave it out)
Flutter
Dart
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
  },
);
React Native
TypeScript
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
  },
);
REST
HTTP
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

The update is applied with a single JSONB merge on the database row — partial failures aren't possible. Either every change in the payload is committed or none of them are. The same merge runs whether the request comes from the SDK, the REST endpoint, or the dashboard editor.

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.

Flutter
Dart
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'];
React Native
TypeScript
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

The editor surfaces the validation rules in the same modal, so you see the limits without having to flip back to the docs. A failed save shows the server's structured detail message naming the offending key and the rule it broke.

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

The server validates the result of your merge, not just your payload. If a bucket has 49 keys and you try to add 2, the request fails on the second key — even though your payload only contained 2 keys, the merged state would have been 51.

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:

Flutter
Dart
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);
}
React Native
TypeScript
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.