# Data

Databases, collections and records with schemas and rules, over one REST/JSON door.

Base URL: `https://data.inovacc.dev`

## Overview

Data is your application's database, served over one REST/JSON door at `https://data.inovacc.dev`. You create **databases**, give them **collections**, and read and write **records** with plain HTTP calls. A collection is either typed, with fields declared by a schema, or free, holding schemaless JSON documents. Who may do what is decided on every request, first by the credential's permissions and then, record by record, by the **rules** the collection declares.

The problem it solves is running persistent data without running a database: no server, no connection pool, no migrations tool, no backups to schedule. Your application keeps its own code and stores its data here; where the data lives is decided by Inovacc and never shown. The same API serves your servers, your web pages (with a browser key) and the people signed in to your app, and a realtime listener pushes changes as they happen.

Use Data for application state: users' notes, orders, settings, catalogues, anything shaped as records. Store large binary content with [Files](/en/products/files/), read what happened with the [Activity log](/en/products/activity/), and notify other systems with [Events](/en/products/events/).

## Concepts

**Databases.** A database is a key your application chooses, such as `shop` or `crm/v2` (a `/` is written `%2F` in a path). A database belongs to the application whose credential created it; another application's database answers `404`, as if it did not exist. An organization-level credential sees every database of the organization.

**Collections: schema or free.** A **schema** collection declares up to 64 fields, each with a type: `text`, `integer`, `number`, `bool`, `datetime`, `json`, `relation` (a record id in a `target` collection) or `blob` (the SHA-256 of a file in the same database). A field can be `required`, `unique`, `indexed`, or `fulltext` (on `text`). A **free** collection declares nothing: each record is a JSON document, queried by dotted path (`profile.city`), and it can be given a schema later once every document fits. Writing to a collection that does not exist creates a free one, when your credential may change schemas.

**Records and versions.** A record is `{id, version, created_at, updated_at, ...fields}`. Its `id` is made by the service (`rec_` plus 26 sortable characters) or chosen by you. Every change raises `version` by one; updates and deletes must send `If-Match: <version>`, so two writers never overwrite each other silently.

**Rules.** A collection declares five rules, `list`, `view`, `create`, `update` and `delete`. Each is `null` (nobody), `""` (any server credential of your organization), `public` (anyone, browser keys included), `users` (the signed-in people of the owning application, and servers), or an expression such as `owner = @auth.id`. Rules apply to every credential, an administrator's included.

**Credentials.** A **secret key** is for servers. A **publishable key** (`apb_...`) is for web pages: it works only from the origins its application lists and only where a rule admits it. A signed-in person's **access token** carries only record and file permissions.

## How it works

Every call carries `Authorization: Bearer <credential>`. There is no organization header: your organization, and the application, come from the credential, and nothing in a path, query or header can name another. A request without a valid credential, or to a path that is not a route, gets one identical `401`.

For each call the service first checks the credential's permission for the action (read, write, create, update, delete, or schema changes) on the database. Anything but "allow" is `403 forbidden` with no detail. Then the collection's rule decides per record: a list returns only the records the `list` rule admits; reading a record the `view` rule hides is `404`, the same as a missing record; a create checks the new data; an update checks both the stored record and the new data.

Writes are versioned. `POST /records` creates and answers `201` with `ETag: "1"`. `PATCH` changes the named fields (on a free collection it is a JSON merge patch), `PUT` replaces the record, and `PUT` with `If-None-Match: *` creates it at your chosen id. A wrong `If-Match` is `409 version_conflict`; a missing one is `428`.

Lists take `filter` (`field op value` joined by `&&` and `||`), `sort`, `limit` (up to 200) and an opaque `cursor`, and return `next_cursor` until the last page; `q` searches the `fulltext` fields. A **batch** applies up to 100 creates, updates and deletes in one transaction: all succeed, or none does.

A **realtime listener** is a WebSocket on `GET .../collections/{c}/listen`: it sends `ready`, then `added`, `modified` and `removed` events for the records your rule lets you list. Every call is recorded in the [Activity log](/en/products/activity/).

## Get started

You need a secret key with permission to create databases and collections ([Authentication](/en/guides/authentication/)).

1. **Create a database.** `PUT /v1/databases/notes` with no body. The answer is `201` with `{key, created_at}`; repeating it answers `200`.
2. **Declare a collection.** `PUT /v1/databases/notes/collections/items` with fields `owner` and `body` and rules (see [the samples](#example)). The answer is the collection with `schema_version` 1.
3. **Write a record.** `POST .../collections/items/records` with the fields. The answer is `201` with the record, its `id` and `version` 1.
4. **Update it.** `PATCH .../records/{id}` with `If-Match: 1`. The answer has `version` 2; sending `If-Match: 1` again is `409 version_conflict`.
5. **List.** `GET .../records?filter=owner%20%3D%20%22me%22&limit=10` returns your record and `next_cursor: null`.

## Use cases

**Per-user data in a web app.** A notes app declares a collection whose five rules say `owner = @auth.id`. The page calls Data directly with the signed-in person's token; each person lists and edits only their own notes, and another person's note is a `404`. No server code enforces it: the rules do.

**A public catalogue.** An online shop keeps `products` as a free collection with `list` and `view` set to `public` and the writes closed. The storefront reads it from the browser with a publishable key; the back office writes it from a server with a secret key.

**Live dashboards.** An operations screen opens a listener on `orders` filtered to today's open orders. It runs the query after `ready`, then applies `added`, `modified` and `removed` events as they arrive, without polling.

## Limits and pricing

| Limit | Value |
|---|---|
| JSON request body | 1 MiB |
| One record | 900 KiB |
| A `json` field | 64 KiB |
| Fields per collection; collections per database | 64; 64 |
| Records per page | 200 (default 50) |
| Operations per batch | 100, in one transaction |
| Filter | 2,048 characters, 40 values, nesting depth 8 |
| Query string | 16 KiB |
| Full-text search `q` | 16 terms, 256 characters |
| Listeners per collection; one listener event | 1,000; 512 KiB |
| Rate | 600 requests per minute per credential holder, per location (`429 rate_limited`) |

**Pricing:** on request.

## Errors

| Status | Code | What it means and what to do |
|---|---|---|
| 400 | `invalid_body`, `invalid_name`, `unknown_field`, `missing_field`, `invalid_value` | The body or a name breaks the rules; fix it. |
| 400 | `record_too_large`, `field_too_large`, `reserved_field` | Shrink the record; do not send `id`, `version`, `created_at`, `updated_at` in a body. |
| 400 | `invalid_schema`, `invalid_rule`, `schema_change_unsupported` | The collection definition is not valid, or a field was changed in place. |
| 400 | `invalid_filter`, `invalid_sort`, `invalid_cursor`, `invalid_limit`, `invalid_search` | Fix the list query; start again without the cursor. |
| 400 | `invalid_if_match`, `batch_too_large`, `cross_shard_batch` | Check the header or split the batch. |
| 401 | `invalid_credentials` | Send a valid credential. |
| 403 | `forbidden` | The credential's permissions or the collection's rule refuse it. |
| 403 | `origin_not_allowed`, `secret_key_in_browser` | A browser key from an unlisted origin, or a secret key in a browser. |
| 404 | `database_not_found`, `collection_not_found`, `record_not_found` | It does not exist, or you may not see it. |
| 409 | `version_conflict` | Someone changed the record; read it again and retry. |
| 409 | `already_exists`, `unique_violation`, `not_empty`, `schema_violation` | The id or value is taken; empty it before deleting; documents do not fit the schema. |
| 412, 428 | `precondition_failed`, `precondition_required` | The record already exists; send `If-Match`. |
| 413 | `payload_too_large` | The body is over 1 MiB. |
| 429 | `rate_limited`, `too_many_listeners` | Slow down; close listeners you do not need. |
| 503 | `service_unavailable` | Retry later; nothing was written. |

## Best practices

- **Always send `If-Match`** with the version you read, and on `409 version_conflict` re-read before retrying.
- **Close every collection by default** and open it rule by rule; a collection born from a write admits any server credential until you tighten it.
- **Never put a secret key in a web page**: use a publishable key and `public` or `@auth` rules.
- **Choose your own record ids** for imports, so a retried import creates nothing twice (`409 already_exists`).
- **Use a batch** when several writes must succeed together.
- **Index the fields you filter and sort on**, and follow `next_cursor` instead of large pages.
- **With a listener, wait for `ready` before querying**, then drop events not newer than what the query returned.

## Related

- [Files](/en/products/files/): binary content beside your records
- [Activity log](/en/products/activity/): what was written, by whom
- [Events](/en/products/events/): tell other systems about a change
- [Authentication](/en/guides/authentication/), [Errors](/en/guides/errors/), [Limits](/en/guides/limits/)

## Endpoints

- GET /v1/databases
- PUT /v1/databases/{database}
- PUT /v1/databases/{database}/collections/{collection}
- POST /v1/databases/{database}/collections/{collection}/records
- POST /v1/databases/{database}/batch

## Example

```sh
curl -X GET "https://data.inovacc.dev/v1/databases" -H "Authorization: Bearer $INOVACC_API_KEY"
```
