AkurAI Build
Menu

Public repository / a9fc8f30566513d0a91a01baccbbd1b15bd8a72b

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

Latest change caebd6669c276c9c4dd88ff423984ed8df65d490 - feat(cli): auto-generated REST API from backend/collections.toml 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` (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.

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 |
| `POST` | `/api/collections/posts/records` | Create from a JSON body |
| `GET` | `/api/collections/posts/records/<id>` | Fetch one record |
| `PATCH` | `/api/collections/posts/records/<id>` | Partial update |
| `DELETE` | `/api/collections/posts/records/<id>` | Delete |

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

## 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, `204` on delete.
- `400` for a bad JSON body, a malformed id, or a validation failure
  (the body is `{ "error": "..." }`).
- `404` for an unknown collection name or a missing record id.

## 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

Two environment variables, both optional:

| Variable | Meaning | Default |
| -------- | ------- | ------- |
| `AKURAI_EMBED_URL` | OpenAI-compatible `/v1/embeddings` endpoint | _unset_ → substring search |
| `AKURAI_EMBED_MODEL` | Embedding model name | `embeddinggemma` |

```
AKURAI_EMBED_URL=http://192.168.8.14:8081/v1/embeddings \
AKURAI_EMBED_MODEL=embeddinggemma \
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).