AkurAI Build
Menu

Public repository / master

AkurAI-Framework / site/content/docs/auto-api.md

Latest change 40e7ccf643510ab1faf1693f1491727fbfac8767 - File copies for any stored file (AKURAI_FILE_COPY_DIR/COLLECTIONS) replace the image API's save dir by Ólafur Búi Ólafsson

# Auto-generated REST API

Declare your data model once in `backend/collections.toml` and the framework
mounts a full REST API for it at startup — list, create, read, update, delete,
and `?search` — with no handwritten endpoint code. The schema is the single
source of truth; validation, storage, and search all derive from it.

If `backend/collections.toml` is absent, nothing changes: the server serves your
site exactly as before, with zero collections.

## Declaring collections

```toml
# backend/collections.toml

[[collection]]
name = "posts"

  [[collection.field]]
  name = "title"
  type = "text"
  required = true

  [[collection.field]]
  name = "body"
  type = "text"
  embed = true          # auto-embedded for semantic search (?search=...)
```

Field types: `text`, `int`, `float`, `bool`, `relation`, `file`
(case-insensitive).

- `required = true` — the field must be present (and non-null) on create.
- `embed = true` — (text only) the field is indexed for semantic `?search` when
  an embedding endpoint is configured. Otherwise it is ignored.
- `collection = "<target>"` — **required for a `relation` field**; names the
  collection it points at (see [Relations](#relations) below).

### Relation fields

A `relation` field stores the integer `id` of a record in another collection.
It needs a sibling `collection` key naming the target; omitting it fails
`akurai serve` at startup.

```toml
[[collection]]
name = "posts"

  [[collection.field]]
  name = "author"
  type = "relation"
  collection = "users"   # the target collection (required)
```

On create/update the referenced id is **existence-checked**: if the target
collection is known and the id does not exist, the write is rejected with `400`.
On read, pass [`?expand=`](#expanding-relations) to inline the referenced
record.

### File fields

A `file` field holds an uploaded file. Files arrive over
`multipart/form-data`, not in a JSON body — see
[File uploads](#file-uploads-multipart) below and the [Uploads](/docs/uploads)
page. The stored value is a small descriptor object:

```json
{ "blob": "<32-char-hex-id>", "filename": "data.bin",
  "content_type": "application/octet-stream", "size": 1234 }
```

In a JSON create/update a file field may be omitted or `null` (a non-object,
non-null value is a `400` validation error). The bytes themselves are fetched
from the [download endpoint](#downloading-a-file).

The engine owns two reserved keys on every record: `id` (auto-increment) and
`created` (unix seconds). Your input may not set them.

A malformed `collections.toml` (TOML syntax error, or an unknown field type)
fails `akurai serve` loudly at startup with the file and line — it never
silently drops your routes.

## Routes

For a collection named `posts`:

| Method | Path | Purpose |
| ------ | ---- | ------- |
| `GET` | `/api/collections` | Manifest of every collection and its endpoints |
| `GET` | `/api/collections/posts/records` | List records, newest first |
| `GET` | `/api/collections/posts/records?search=<q>&limit=<n>` | Search |
| `GET` | `/api/collections/posts/records?expand=<field>[,<field>...]` | List with relations inlined |
| `POST` | `/api/collections/posts/records` | Create (JSON or multipart) |
| `GET` | `/api/collections/posts/records/<id>` | Fetch one record |
| `GET` | `/api/collections/posts/records/<id>?expand=<field>` | Fetch with relations inlined |
| `PATCH` | `/api/collections/posts/records/<id>` | Partial update (JSON or multipart) |
| `DELETE` | `/api/collections/posts/records/<id>` | Delete |
| `GET` | `/api/collections/posts/records/<id>/<field>` | Download a `file` field's bytes |

The collection manifest is also merged into `/api/_meta` under `collections`.

## Expanding relations

Add `?expand=<field>` (comma-separate several) to a `GET` list or single-record
request. For each named **relation** field, the referenced record is inlined
under a sibling key `<field>_expanded` (the original id key is left untouched).
A reference that is absent, dangling, or points at an unknown collection expands
to `null`; names that are not relation fields are ignored. Without `?expand`,
behavior is unchanged — you get the raw relation id.

```
curl 'http://localhost:8090/api/collections/posts/records/1?expand=author'
```

```json
{ "id": 1, "created": 1750000000, "title": "Hello", "author": 7,
  "author_expanded": { "id": 7, "created": 1749990000, "name": "Ada" } }
```

## File uploads (multipart)

To set a `file` field, `POST` (or `PATCH`) a `multipart/form-data` request
instead of JSON. Each part whose `name` matches a `file` field has its bytes
stored in the content-addressed blob store and the field is set to the
`{blob,filename,content_type,size}` descriptor. Parts matching non-file fields
are taken as text and coerced to the field's declared type. A request with a
JSON `Content-Type` is handled exactly as before — multipart is opt-in via the
header.

```
curl -X POST http://localhost:8090/api/collections/documents/records \
  -F 'title=My Document' \
  -F 'attachment=@./data.bin;type=application/octet-stream'
```

```json
{ "id": 1, "created": 1750000000, "title": "My Document",
  "attachment": { "blob": "9f8e...c0", "filename": "data.bin",
                  "content_type": "application/octet-stream", "size": 1234 } }
```

### Downloading a file

`GET /api/collections/<name>/records/<id>/<field>` streams the stored bytes back
with the descriptor's `content_type` (default `application/octet-stream`) and a
`Content-Disposition` naming the stored filename. A missing record, field, or
blob is a `404`.

```
curl -OJ http://localhost:8090/api/collections/documents/records/1/attachment
```

## File copies

Set `AKURAI_FILE_COPY_DIR` (and list collections in `AKURAI_FILE_COPY_COLLECTIONS`,
comma-separated) to also write every stored `file` field of those collections to
that directory as `<collection>-<id>-<slug>.<ext>`. The slug comes from the
record's `prompt`, `name` or `title`; `~/` expands to `$HOME`. It applies to
multipart uploads and to the [image API](/docs/images) alike. Copies are best
effort: the record is stored first and a failed copy only logs a warning.

```
AKURAI_FILE_COPY_DIR=~/Pictures AKURAI_FILE_COPY_COLLECTIONS=images akurai serve
```

## Examples

Create a record:

```
curl -X POST http://localhost:8090/api/collections/posts/records \
  -H 'Content-Type: application/json' \
  -d '{"title":"Hello","body":"a wonderful world"}'
```

```json
{ "id": 1, "created": 1750000000, "title": "Hello", "body": "a wonderful world" }
```

List records (newest first):

```
curl http://localhost:8090/api/collections/posts/records
```

```json
[ { "id": 2, "created": 1750000100, "title": "Other", "body": "..." },
  { "id": 1, "created": 1750000000, "title": "Hello", "body": "..." } ]
```

Fetch, update, delete:

```
curl http://localhost:8090/api/collections/posts/records/1
curl -X PATCH http://localhost:8090/api/collections/posts/records/1 \
  -H 'Content-Type: application/json' -d '{"title":"Hello again"}'
curl -X DELETE http://localhost:8090/api/collections/posts/records/1
```

### Status codes

- `201` on create, `200` on list/get/update/search/download, `204` on delete.
- `400` for a bad JSON/multipart body, a malformed id, a validation failure, or
  a relation id that references a non-existent record (the body is
  `{ "error": "..." }`).
- `404` for an unknown collection name, a missing record id, or a download of a
  field/record/blob that is not present.

## Search: semantic vs substring

`?search=<query>` works in two modes, transparently:

- **Substring (default).** With no embedding endpoint configured, `?search`
  matches the query as a case-sensitive substring over the collection's `text`
  fields, newest first. No setup required.
- **Semantic.** When `AKURAI_EMBED_URL` is set, the query is embedded and ranked
  by cosine similarity against the stored vectors of each record's `embed` text
  fields. Results come back in ranked (most-similar-first) order.

If the embedding endpoint is unreachable or returns an error for a given query,
search falls back to substring matching for that request — it never errors out.
The response is always a plain JSON array of records in either mode.

## Embedding configuration

Three environment variables, all optional:

| Variable | Meaning | Default |
| -------- | ------- | ------- |
| `AKURAI_EMBED_URL` | OpenAI-compatible `/v1/embeddings` endpoint | _unset_ → substring search |
| `AKURAI_EMBED_MODEL` | Embedding model name | `embeddinggemma` |
| `AKURAI_EMBED_API_KEY` | Bearer token for protected embedding routers | _unset_ |

```
AKURAI_EMBED_URL=http://127.0.0.1:4219/v1/embeddings \
AKURAI_EMBED_MODEL=intfloat/multilingual-e5-small \
AKURAI_EMBED_API_KEY=akr_... \
akurai serve
```

Embeddings live in `data/embeddings.db` (separate from the record store at
`data/collections.db`). They are written best-effort after each successful
create/update and removed on delete — **a failed embedding never fails the
write**; the record is always saved, and a warning is printed to stderr.

### Plain HTTP only (no TLS)

The built-in embedding client speaks **plain HTTP** — it has no TLS stack, to
keep the framework dependency-free. An `https://` endpoint is rejected. Point
`AKURAI_EMBED_URL` at a plain-HTTP embeddings server (e.g. one running on your
LAN or behind a local reverse proxy that terminates TLS).