Data
Databases, collections and records with schemas and rules, over one REST/JSON door.
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, read what happened with the Activity log, and notify other systems with 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.
Get started
You need a secret key with permission to create databases and collections (Authentication).
- Create a database.
PUT /v1/databases/noteswith no body. The answer is 201 with{key, created_at}; repeating it answers 200. - Declare a collection.
PUT /v1/databases/notes/collections/itemswith fieldsownerandbodyand rules (see the samples). The answer is the collection withschema_version1. - Write a record.
POST .../collections/items/recordswith the fields. The answer is 201 with the record, itsidandversion1. - Update it.
PATCH .../records/{id}withIf-Match: 1. The answer hasversion2; sendingIf-Match: 1again is 409version_conflict. - List.
GET .../records?filter=owner%20%3D%20%22me%22&limit=10returns your record andnext_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-Matchwith the version you read, and on 409version_conflictre-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
publicor@authrules. - 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_cursorinstead of large pages. - With a listener, wait for
readybefore querying, then drop events not newer than what the query returned.
Related
- Files: binary content beside your records
- Activity log: what was written, by whom
- Events: tell other systems about a change
- Authentication, Errors, Limits
Authentication
Every call carries your key; your organization comes from it. See the authentication guide.
AuthorizationEndpoints
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
Language
⋮
cURL
curl "https://data.inovacc.dev/v1/databases" \
-H "Authorization: Bearer $INOVACC_API_KEY"curl "https://data.inovacc.dev/v1/databases" \
-H "Authorization: Bearer $INOVACC_API_KEY"
TypeScript
const url = "https://data.inovacc.dev/v1/databases";
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.INOVACC_API_KEY}`,
},
});
console.log(response.status, await response.text());const url = "https://data.inovacc.dev/v1/databases";
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.INOVACC_API_KEY}`,
},
});
console.log(response.status, await response.text());
Python
import os
import urllib.request
request = urllib.request.Request(
"https://data.inovacc.dev/v1/databases",
method="GET",
headers={
"User-Agent": "inovacc-python-sample",
"Authorization": "Bearer " + os.environ["INOVACC_API_KEY"],
},
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())import os
import urllib.request
request = urllib.request.Request(
"https://data.inovacc.dev/v1/databases",
method="GET",
headers={
"User-Agent": "inovacc-python-sample",
"Authorization": "Bearer " + os.environ["INOVACC_API_KEY"],
},
)
with urllib.request.urlopen(request) as response:
print(response.status, response.read().decode())
Go
package main
import (
"fmt"
"io"
"net/http"
"os"
)
const url = "https://data.inovacc.dev/v1/databases"
func main() {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("INOVACC_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(out))
}package main
import (
"fmt"
"io"
"net/http"
"os"
)
const url = "https://data.inovacc.dev/v1/databases"
func main() {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("INOVACC_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(res.Status, string(out))
}
Rust
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
fn main() -> Result<(), Box<dyn std::error::Error>> {
let api_key = std::env::var("INOVACC_API_KEY")?;
let response = reqwest::blocking::Client::new()
.get("https://data.inovacc.dev/v1/databases")
.bearer_auth(api_key)
.send()?;
println!("{} {}", response.status(), response.text()?);
Ok(())
}// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
fn main() -> Result<(), Box<dyn std::error::Error>> {
let api_key = std::env::var("INOVACC_API_KEY")?;
let response = reqwest::blocking::Client::new()
.get("https://data.inovacc.dev/v1/databases")
.bearer_auth(api_key)
.send()?;
println!("{} {}", response.status(), response.text()?);
Ok(())
}
JavaScript
const url = "https://data.inovacc.dev/v1/databases";
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.INOVACC_API_KEY}`,
},
});
console.log(response.status, await response.text());const url = "https://data.inovacc.dev/v1/databases";
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.INOVACC_API_KEY}`,
},
});
console.log(response.status, await response.text());
PHP
<?php
$curl = curl_init('https://data.inovacc.dev/v1/databases');
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INOVACC_API_KEY'),
],
]);
$response = curl_exec($curl);
echo curl_getinfo($curl, CURLINFO_HTTP_CODE), ' ', $response, "\n";
curl_close($curl);<?php
$curl = curl_init('https://data.inovacc.dev/v1/databases');
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INOVACC_API_KEY'),
],
]);
$response = curl_exec($curl);
echo curl_getinfo($curl, CURLINFO_HTTP_CODE), ' ', $response, "\n";
curl_close($curl);
Ruby
require "net/http"
require "uri"
uri = URI('https://data.inovacc.dev/v1/databases')
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('INOVACC_API_KEY')}"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
http.request(request)
end
puts "#{response.code} #{response.body}"require "net/http"
require "uri"
uri = URI('https://data.inovacc.dev/v1/databases')
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch('INOVACC_API_KEY')}"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
http.request(request)
end
puts "#{response.code} #{response.body}"
Java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Main {
public static void main(String[] args) throws Exception {
String apiKey = System.getenv("INOVACC_API_KEY");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://data.inovacc.dev/v1/databases"))
.header("Authorization", "Bearer " + apiKey)
.GET()
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Main {
public static void main(String[] args) throws Exception {
String apiKey = System.getenv("INOVACC_API_KEY");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://data.inovacc.dev/v1/databases"))
.header("Authorization", "Bearer " + apiKey)
.GET()
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}
C#
var url = "https://data.inovacc.dev/v1/databases";
var apiKey = Environment.GetEnvironmentVariable("INOVACC_API_KEY");
using var client = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Add("Authorization", $"Bearer {apiKey}");
using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");var url = "https://data.inovacc.dev/v1/databases";
var apiKey = Environment.GetEnvironmentVariable("INOVACC_API_KEY");
using var client = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Add("Authorization", $"Bearer {apiKey}");
using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");
Kotlin
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
fun main() {
val apiKey = System.getenv("INOVACC_API_KEY")
val request = HttpRequest.newBuilder()
.uri(URI.create("https://data.inovacc.dev/v1/databases"))
.header("Authorization", "Bearer " + apiKey)
.GET()
.build()
val response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString())
println("${response.statusCode()} ${response.body()}")
}import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
fun main() {
val apiKey = System.getenv("INOVACC_API_KEY")
val request = HttpRequest.newBuilder()
.uri(URI.create("https://data.inovacc.dev/v1/databases"))
.header("Authorization", "Bearer " + apiKey)
.GET()
.build()
val response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString())
println("${response.statusCode()} ${response.body()}")
}
Swift
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
let apiKey = ProcessInfo.processInfo.environment["INOVACC_API_KEY"] ?? ""
let url = URL(string: "https://data.inovacc.dev/v1/databases")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
let (data, response) = try await URLSession.shared.data(for: request)
let status = (response as? HTTPURLResponse)?.statusCode ?? 0
print(status, String(decoding: data, as: UTF8.self))import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
let apiKey = ProcessInfo.processInfo.environment["INOVACC_API_KEY"] ?? ""
let url = URL(string: "https://data.inovacc.dev/v1/databases")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
let (data, response) = try await URLSession.shared.data(for: request)
let status = (response as? HTTPURLResponse)?.statusCode ?? 0
print(status, String(decoding: data, as: UTF8.self))
Source details
- Id
- identity/component-taxonomy/capabilities#data
- Repository
- identity
- Path
- contracts/component-taxonomy/catalog/capabilities.json
- Commit
- 08ef3cd75d82
- Id
- identity/component-taxonomy/quickstart#data
- Repository
- identity
- Path
- contracts/component-taxonomy/catalog/capabilities.json
- Commit
- 08ef3cd75d82