BlobStore
Store and retrieve arbitrary binary files — images, PDFs, archives, model weights — alongside your agent data. Files are content-addressed by their BLAKE3 hash, auto-chunked for large uploads, and linked to the strand via a BLOB_REF nucleotide so they participate in the append-only audit chain.
Enable blob storage
Set the SAPIX_BLOB_DIR environment variable to a directory where SapixDB can write blob chunks. All blob endpoints return 503 Service Unavailable when this variable is unset.
SAPIX_BLOB_DIR=/var/sapixdb/blobs
Uploading a blob
Send raw bytes to PUT /v1/blobs. The body is the file content — do not wrap it in JSON or multipart. The server sniffs the MIME type from the first 16 bytes (magic bytes), stores the blob content-addressed, writes a BlobMeta sidecar, and appends a BLOB_REF record to the strand.
PUT /v1/blobs Authorization: Bearer spx_root_YOUR_ROOT_KEY Content-Type: image/png <raw PNG bytes>
PUT /v1/blobs?filename=profile_photo.png Authorization: Bearer spx_root_YOUR_ROOT_KEY Content-Type: image/png <raw PNG bytes>
{
"content_hash": "3a7bd3f1c2e9a4b5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8",
"size_bytes": 204800,
"chunked": false,
"encrypted": false,
"strand_record_id": "018f3c2a-4b1d-7e8f-a3c2-1d4e5f6a7b8c"
}The content_hash is the stable identifier for this blob. Store it in your agent records to link them to the file. Re-uploading identical bytes is a no-op — the same hash is returned and no new strand record is written.
Downloading a blob
GET /v1/blobs/3a7bd3f1c2e9a4b5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8 Authorization: Bearer spx_root_YOUR_ROOT_KEY
The response body is the raw file bytes. The Content-Type header is set from the stored BlobMeta sidecar (which contains the MIME type detected at upload time). Use HEAD /v1/blobs/:hash to check existence and retrieve headers without downloading the body.
For range requests, pass a standard Range header:
GET /v1/blobs/3a7bd3... Authorization: Bearer spx_root_YOUR_ROOT_KEY Range: bytes=0-1048575
Blob metadata
Every uploaded blob gets a BlobMeta sidecar stored in the instance-level metadata store. Retrieve it at any time without downloading the blob body.
GET /v1/blobs/3a7bd3f1.../meta Authorization: Bearer spx_root_YOUR_ROOT_KEY
{
"content_type": "image/png",
"original_filename": "profile_photo.png",
"size_bytes": 204800,
"uploaded_at_ms": 1756224000000
}| Field | Description |
|---|---|
| content_type | MIME type sniffed from magic bytes at upload time. |
| original_filename | The ?filename= query param value if provided, otherwise null. |
| size_bytes | Total blob size in bytes. |
| uploaded_at_ms | Unix millisecond timestamp of when the blob was first stored. |
MIME type detection
SapixDB detects the MIME type from magic bytes (the first 16 bytes of the body) — not from the Content-Type request header. The header is accepted but ignored for storage purposes. This prevents clients from mislabeling uploads.
| Magic bytes | Detected MIME type |
|---|---|
| \xFF\xD8\xFF | image/jpeg |
| \x89PNG\r\n\x1A\n | image/png |
| GIF87a or GIF89a | image/gif |
| RIFF....WEBP | image/webp |
| application/pdf | |
| PK\x03\x04 | application/zip |
| (anything else) | application/octet-stream |
Upload policy
A global BlobPolicy lets you restrict what can be uploaded. The policy is stored at the instance level — it applies to all blob uploads.
Set policy
PUT /v1/blobs/policy
Authorization: Bearer spx_root_YOUR_ROOT_KEY
Content-Type: application/json
{
"allowed_mime_types": ["image/jpeg", "image/png", "image/webp"],
"max_size_bytes": 5242880
}Read current policy
GET /v1/blobs/policy Authorization: Bearer spx_root_YOUR_ROOT_KEY
{
"allowed_mime_types": ["image/jpeg", "image/png", "image/webp"],
"max_size_bytes": 5242880
}| Field | Type | Description |
|---|---|---|
| allowed_mime_types | string[] | MIME types allowed for upload. Empty array = all types allowed (default). |
| max_size_bytes | number | null | Maximum upload size in bytes. null = no per-policy cap (1 GiB hard cap still applies). |
max_size_bytes to a larger value has no effect — the hard cap still applies.Deleting a blob
DELETE /v1/blobs/3a7bd3f1... Authorization: Bearer spx_root_YOUR_ROOT_KEY
Returns 204 No Content. The blob chunks and the strand BLOB_REF record are removed. The BlobMeta sidecar is also deleted. Deleting a blob that does not exist returns 404.
HTTP API Reference
| Method | Path | Description |
|---|---|---|
| PUT | /v1/blobs | Upload a blob (raw body). Returns 201 with content_hash. |
| GET | /v1/blobs/:hash | Download blob bytes. Supports Range header for partial reads. |
| HEAD | /v1/blobs/:hash | Check existence and read headers without downloading. |
| DELETE | /v1/blobs/:hash | Delete a blob. Returns 204. |
| GET | /v1/blobs/:hash/meta | Read BlobMeta sidecar. Returns 404 if not found. |
| GET | /v1/blobs/policy | Read current upload policy. |
| PUT | /v1/blobs/policy | Replace upload policy. Returns updated policy. |
Upload query params
| Param | Description |
|---|---|
| filename | Optional original filename stored in BlobMeta.original_filename. |
| phi | If true, the blob is PHI-encrypted (requires HIPAA add-on). Default false. |
Authentication and Scopes
Uploading a blob requires write:blobs. Downloading (or checking existence via HEAD) requires read:blobs. Replacing the upload policy (PUT /v1/blobs/policy) and deleting a blob (DELETE /v1/blobs/:hash) require the root key or a scoped key holding admin:blobs— the upload policy is instance-wide, and the blob store is content-addressed and shared across every tenant, so an unauthorized delete could remove a blob another tenant's record still references.
Downloading a ?phi=true blob additionally requires admin:hipaa (or the root key) — a plain read:blobs key can download an ordinary blob but not one holding PHI. See the HIPAA add-on page.