Skip to content

Storage API

The Storage API manages a flat namespace of atoms (objects and folders). Keys are arbitrary path-like strings (e.g. posts/hello-world). The API serves the root endpoint for meta-information and then routes on the first path segment.

Key Encoding

Object and folder keys are arbitrary path-like strings (e.g. posts/hello-world). When a key contains slashes, those slashes must be percent-encoded as %2F in the URL path. The router takes only the first path segment after the resource prefix as the key, so a raw slash is interpreted as a new path segment rather than part of the key.

KeyCorrect URL pathWrong URL path
posts/objects/posts
posts/hello-world/objects/posts%2Fhello-world/objects/posts/hello-world
a/b/c/objects/a%2Fb%2Fc/objects/a/b/c

The same rule applies to /folders/{key}.

Endpoints


GET /

Returns meta-information about the Storage API and its available endpoints.

Response

json
{
  "data": {
    "type": "api-info",
    "id": "storage",
    "attributes": {
      "name": "Storage API",
      "version": "1.0.0",
      "endpoints": [
        {
          "path": "/openapi.json",
          "methods": ["GET"],
          "description": "OpenAPI 3.1 specification for this API"
        },
        {
          "path": "/openapi.yaml",
          "methods": ["GET"],
          "description": "OpenAPI 3.1 specification for this API, as YAML"
        },
        {
          "path": "/capabilities",
          "methods": ["GET"],
          "description": "Underlying storage repository capabilities"
        },
        { "path": "/atoms", "methods": ["POST"], "description": "Create a folder" },
        { "path": "/atoms/{key}", "methods": ["GET"], "description": "List atoms in a folder" },
        {
          "path": "/atom-summaries/{key}",
          "methods": ["GET"],
          "description": "List atom summaries (lightweight listing) in a folder"
        },
        {
          "path": "/objects",
          "methods": ["POST"],
          "description": "Create a storage object"
        },
        {
          "path": "/objects/{key}",
          "methods": ["GET", "PATCH", "DELETE"],
          "description": "Read, update, or delete a storage object"
        },
        { "path": "/folders/{key}", "methods": ["GET"], "description": "Read a folder" },
        {
          "path": "/operations",
          "methods": ["POST"],
          "description": "Atomic operations (add, update, remove)"
        }
      ]
    }
  }
}

GET /openapi.json

Returns the OpenAPI 3.1 specification for the Storage API as a JSON document.

Response200 OK, Content-Type: application/json

The response body is an OpenAPI 3.1.0 document. The servers array is rewritten to the absolute mount point so the document is usable as-is by code generators and API clients.

json
{
  "openapi": "3.1.0",
  "info": { "title": "Storage API", "version": "1.0.0" },
  "servers": [{ "url": "https://example.com/api/storage" }],
  "paths": { ... }
}

GET /openapi.yaml

Returns the same OpenAPI 3.1 specification as GET /openapi.json, serialized as YAML.

Response200 OK, Content-Type: application/yaml


POST /atoms

Create a new folder.

Request Headers

Content-Type: application/vnd.api+json

Request Body

json
{
  "data": {
    "type": "folder",
    "id": "posts/drafts",
    "attributes": {}
  }
}
FieldTypeRequiredDescription
data.type"folder"yesResource type — must be folder
data.idstringyesKey for the new folder
data.attributesobjectyesAttributes object (may be empty)

Response201 Created with the created folder resource

json
{
  "data": {
    "type": "folder",
    "id": "posts/drafts",
    "attributes": {
      "type": "folder",
      "createdAt": "2024-01-15T10:30:00Z",
      "updatedAt": "2024-01-15T10:30:00Z"
    }
  }
}

Error Responses

StatusCondition
400Request body is missing, malformed JSON, or fails validation

GET /atoms/:key

List all atoms (objects and folders) under the given key prefix. Returns full content for each atom.

Omitting :key (i.e. GET /atoms) lists root-level atoms — equivalent to GET /atoms/ with an empty key.

Path Parameters

ParameterTypeRequiredDescription
keystringNoFolder key prefix to list atoms under (e.g. posts). Defaults to '' (root).

Query Parameters

ParameterTypeDefaultDescription
page[number]numberPage number (1-based) for page-based pagination
page[size]number100Items per page (page- and cursor-based). When no page[*] param is present, defaults to 100
page[offset]numberZero-based offset for offset-based pagination
page[limit]numberMaximum items for offset-based pagination
page[after]stringForward cursor. Rejected with 400 when backend capabilities report pagination.styles.cursor: false
page[before]stringBackward cursor. Rejected with 400 when backend capabilities report pagination.styles.cursor: false
filter[depth]number1Traversal depth (minimum 1)

Note: Cursor params (page[after] / page[before]) are rejected with HTTP 400 on all built-in backends (FS, R2, WebDAV, Drizzle, libSQL, …) because they report pagination.styles.cursor: false in GET /capabilities. Use page[number], page[offset], or page[limit] for those backends.

Response — collection of object and/or folder resources

json
{
  "data": [
    {
      "type": "object",
      "id": "posts/hello-world",
      "attributes": {
        "type": "object",
        "content": {
          "body": "This is my first post."
        },
        "createdAt": "2024-01-15T10:30:00Z",
        "updatedAt": "2024-01-16T08:00:00Z"
      }
    },
    {
      "type": "folder",
      "id": "posts/drafts",
      "attributes": {
        "type": "folder",
        "createdAt": "2024-01-10T09:00:00Z",
        "updatedAt": "2024-01-10T09:00:00Z"
      }
    }
  ],
  "links": {
    "self": "http://localhost:3000/atoms/posts",
    "first": "http://localhost:3000/atoms/posts",
    "next": "http://localhost:3000/atoms/posts?page[after]=posts%2Fdrafts",
    "prev": null,
    "last": null
  },
  "meta": {
    "page": {
      "total": 2
    }
  }
}

GET /atom-summaries/:key

List atom summaries (without full content) under the given key prefix. Useful for listing large collections efficiently.

Omitting :key (i.e. GET /atom-summaries) lists root-level atom summaries — equivalent to GET /atom-summaries/ with an empty key.

Path Parameters

ParameterTypeRequiredDescription
keystringNoFolder key prefix (e.g. posts). Defaults to '' (root).

Query Parameters

Same as GET /atoms/:key.

Response — collection of object-summary and/or folder-summary resources

createdAt and updatedAt are optional — all built-in backends populate them, but a custom backend may omit them.

json
{
  "data": [
    {
      "type": "object-summary",
      "id": "posts/hello-world",
      "attributes": {
        "type": "object-summary",
        "createdAt": "2024-01-15T10:30:00Z",
        "updatedAt": "2024-01-16T08:00:00Z"
      }
    },
    {
      "type": "folder-summary",
      "id": "posts/drafts",
      "attributes": {
        "type": "folder-summary",
        "createdAt": "2024-01-10T09:00:00Z",
        "updatedAt": "2024-01-10T09:00:00Z"
      }
    }
  ],
  "links": {
    "self": "http://localhost:3000/atom-summaries/posts",
    "first": "http://localhost:3000/atom-summaries/posts",
    "next": null,
    "prev": null,
    "last": null
  },
  "meta": {
    "page": {
      "total": 2
    }
  }
}

POST /objects

Create a new storage object.

Request Headers

Content-Type: application/vnd.api+json

Request Body

json
{
  "data": {
    "type": "object",
    "id": "posts/hello-world",
    "attributes": {
      "content": {
        "body": "This is my first post."
      }
    }
  }
}

Note: When using rawSerializer (the default in the Getting Started guide), only the body field is persisted. Passing any other fields (e.g. title) will throw an error at write time. Use jsonSerializer if you need to store multi-field content.

content must always be an object — raw strings cannot be stored directly. To transport a raw string, the convention is to wrap it as { "body": "<content>" }; markdown with frontmatter becomes { ...frontmatter, "body": "<markdown>" }. See the body convention.

FieldTypeRequiredDescription
data.type"object"yesResource type
data.idstringyesThe key for the new object
data.attributes.contentobjectnoArbitrary JSON content (defaults to {})

Response201 Created with the created object

json
{
  "data": {
    "type": "object",
    "id": "posts/hello-world",
    "attributes": {
      "type": "object",
      "content": {
        "body": "This is my first post."
      },
      "createdAt": "2024-01-15T10:30:00Z",
      "updatedAt": "2024-01-15T10:30:00Z"
    }
  }
}

GET /objects/:key

Get a single storage object by key.

Path Parameters

ParameterTypeDescription
keystringKey of the object (slashes must be encoded as %2F, e.g. posts%2Fhello-world)

Example

GET /objects/posts%2Fhello-world

Response — the requested object

json
{
  "data": {
    "type": "object",
    "id": "posts/hello-world",
    "attributes": {
      "type": "object",
      "content": {
        "title": "Hello World",
        "body": "This is my first post."
      },
      "createdAt": "2024-01-15T10:30:00Z",
      "updatedAt": "2024-01-16T08:00:00Z"
    }
  }
}

Error Response404 Not Found (also returned if the key is not encoded and the router interprets it as a different path)

json
{
  "errors": [
    {
      "status": "404",
      "code": "not_found",
      "detail": "The file at posts does not exist"
    }
  ]
}

PATCH /objects/:key

Update an existing storage object. The id in the request body must match the :key path parameter.

Path Parameters

ParameterTypeDescription
keystringKey of the object (slashes must be encoded as %2F, e.g. posts%2Fhello-world)

Request Headers

Content-Type: application/vnd.api+json

Request Body

json
{
  "data": {
    "type": "object",
    "id": "posts/hello-world",
    "attributes": {
      "content": {
        "body": "Updated content."
      }
    }
  }
}

Response — updated object

json
{
  "data": {
    "type": "object",
    "id": "posts/hello-world",
    "attributes": {
      "type": "object",
      "content": {
        "body": "Updated content."
      },
      "createdAt": "2024-01-15T10:30:00Z",
      "updatedAt": "2024-01-16T08:00:00Z"
    }
  }
}

DELETE /objects/:key

Delete a storage object by key. Equivalent to using the remove operation in POST /operations.

Path Parameters

ParameterTypeDescription
keystringKey of the object (slashes must be encoded as %2F, e.g. posts%2Fhello-world)

Example

DELETE /objects/posts%2Fhello-world

Response — on success

json
{
  "meta": {
    "deleted": true
  }
}

Returns 404 if the object does not exist.

Note: Unsupported methods (e.g. PUT) on /objects/{key} return 405 Method Not Allowed with an Allow: GET, PATCH, DELETE header.


GET /folders/:key

Get a single folder by key.

Path Parameters

ParameterTypeDescription
keystringKey of the folder (slashes must be encoded as %2F, e.g. posts%2Fdrafts)

Example

GET /folders/posts%2Fdrafts

Response

json
{
  "data": {
    "type": "folder",
    "id": "posts/drafts",
    "attributes": {
      "type": "folder",
      "createdAt": "2024-01-10T09:00:00Z",
      "updatedAt": "2024-01-10T09:00:00Z"
    }
  }
}

POST /operations

Execute a batch of atomic operations. Supports adding objects, adding folders, updating objects, and removing atoms. All operations are processed in order; failed operations are omitted from atomic:results — the response is HTTP 200 with a shorter result array (matching the OpenAPI spec).

Request Headers

Content-Type: application/vnd.api+json

Request Body

json
{
  "atomic:operations": [
    {
      "op": "add",
      "data": {
        "type": "object",
        "id": "posts/new-post",
        "attributes": {
          "content": { "title": "New Post" }
        }
      }
    },
    {
      "op": "add",
      "data": {
        "type": "folder",
        "id": "posts/archive"
      }
    },
    {
      "op": "update",
      "data": {
        "type": "object",
        "id": "posts/hello-world",
        "attributes": {
          "content": { "title": "Updated Title" }
        }
      }
    },
    {
      "op": "remove",
      "ref": {
        "type": "atom",
        "id": "posts/old-post"
      }
    }
  ]
}

Supported operation types

opSupported data.type / ref.typeDescription
add"object", "folder"Create a new object or folder
update"object"Update an existing object
remove"object", "folder", "atom"Remove an existing atom

Response

Results are returned in the same order as the input operations. Remove operations return a meta entry (same as the Documents API).

json
{
  "atomic:results": [
    {
      "data": {
        "type": "object",
        "id": "posts/new-post",
        "attributes": {
          "type": "object",
          "content": { "title": "New Post" },
          "createdAt": "2024-01-15T10:30:00Z",
          "updatedAt": "2024-01-15T10:30:00Z"
        }
      }
    },
    {
      "data": {
        "type": "folder",
        "id": "posts/archive",
        "attributes": {
          "type": "folder",
          "createdAt": "2024-01-15T10:30:00Z",
          "updatedAt": "2024-01-15T10:30:00Z"
        }
      }
    },
    {
      "data": {
        "type": "object",
        "id": "posts/hello-world",
        "attributes": {
          "type": "object",
          "content": { "title": "Updated Title" },
          "createdAt": "2024-01-15T10:30:00Z",
          "updatedAt": "2024-01-16T08:00:00Z"
        }
      }
    },
    {
      "meta": {
        "deleted": true,
        "ref": {
          "type": "atom",
          "id": "posts/old-post"
        }
      }
    }
  ]
}

Released under the MIT License.