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