Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

Data

Generally Available

Data gives you a PostgREST-shaped HTTP API over PostgreSQL. If you have used Supabase Data, the query syntax, filters, embedded resources, and RPC calls will feel familiar.

What makes Reactor Data different is where policies live. Row-level access is enforced by a single Rust policy engine—not native Postgres RLS—so the same policy DSL works across backends and composes directly with Identity permissions (auth.has_permission(...), auth.org_id(), and friends).

Every request requires a JWT from Identity. There is no anonymous anon key; public read access is modeled through permissions on a role, not a separate key type.

  • RESTful CRUDGET, POST, PATCH, DELETE on any table in your user schema.
  • Rich filtering — PostgREST operators: eq, gt, in, like, is.null, nested and/or groups.
  • Embedded resources — Fetch related rows in one round trip: ?select=id,title,author(name).
  • SQL-defined RPC — Call database functions via POST /data/v1/rpc/{name}.
  • Portable migrations — Write SQL once in the Reactor dialect; lint rejects non-portable constructs.
  • Policy DSL in migrations — Declare policy ... on ... using (...) inline with your schema.
  • Audit on mutations — Inserts, updates, deletes, and RPC calls write audit rows in the same transaction.

Define a table with a tenant policy, apply migrations, then query as an authenticated user.

Migration (migrations/001_todos.sql):

create table todos (
id reactor_id primary key,
org_id reactor_id not null,
title text not null,
done bool not null default false,
created_at timestamptz not null default now()
);
policy todos_tenant on todos
for select, update, delete
using (org_id = auth.org_id());
policy todos_insert on todos
for insert
check (org_id = auth.org_id());
Terminal window
reactor db migrate
reactor auth login user@example.com --password '...'
reactor data insert todos --json '{"title":"Ship v1","org_id":"<your-org-id>"}'
reactor data query todos --select 'id,title,done'
Terminal window
reactor data query todos \
--filter 'done=eq.false' \
--order 'created_at.desc' \
--limit 20

Common filter operators:

OperatorExampleMeaning
eq?status=eq.activeEquals
gt, gte, lt, lte?views=gt.100Comparison
in?id=in.(a,b,c)In list
like?title=like.api*Pattern match (*%)
is?deleted_at=is.nullIS NULL / TRUE / FALSE
and?and=(a.eq.1,b.eq.2)Conjunction
or?or=(a.eq.1,a.eq.2)Disjunction

Traverse foreign keys in a single query. Each joined table’s policies apply independently.

const posts = await reactor.data
.from('posts')
.select('id, title, author(id, name), comments(id, body)');

Register SQL functions in migrations, then invoke them by name with JSON arguments.

Terminal window
reactor data rpc search_posts --json '{"query":"reactor","limit":10}'
[data]
migrations_dir = "./migrations"
user_schema = "public"
max_embed_depth = 5
max_limit = 1000
default_limit = 100
# Monolith mode uses in-process auth; microservices need:
# auth_url = "http://reactor-auth:8001"
# internal_secret = "shared-secret"

Environment variables (override reactor.toml):

VariableDefaultDescription
REACTOR_DATA_DATABASE_URLPostgres connection string
REACTOR_DATA_BIND0.0.0.0:8002HTTP bind address
REACTOR_DATA_MIGRATIONS_DIR./migrationsUser migration source
REACTOR_DATA_MAX_LIMIT1000Maximum rows per read
REACTOR_DATA_DEFAULT_LIMIT100Default when no limit specified
LimitDefaultNotes
Max rows per request1,000max_limit / REACTOR_DATA_MAX_LIMIT
Default page size100When no limit or Range header
Embed depth5Nested FK traversal in ?select=
Bulk insert policy denial207 Multi-StatusPermitted rows commit; denied rows reported per-row
Realtime subscriptionsv0.2?subscribe=1 returns 426 Upgrade Required in v0
SQLite backendv0.2Migrations are portable from day one; Postgres ships first

Required permissions:

OperationPermission
Read (GET, stable RPC)data:{table}:read or wildcard
Write (POST, PATCH, DELETE)data:{table}:write
RPC invokedata:rpc:{name}:invoke
MethodPathDescription
GET/data/v1/{table}Select with filters
POST/data/v1/{table}Insert row(s)
PATCH/data/v1/{table}?filter=...Update matching rows
DELETE/data/v1/{table}?filter=...Delete matching rows
POST/data/v1/rpc/{function}Call SQL function
ValueEffect
return=representationReturn affected rows on mutation
return=minimalEmpty body (default)
count=exactInclude total in Content-Range
resolution=merge-duplicatesUpsert on primary key conflict

A row-level policy blocked the operation. Check which policy fired in the error details field. Common fixes:

  • Ensure org_id on inserted rows matches auth.org_id()
  • Grant the role data:{table}:write or a wildcard
  • Add a using policy for the operation’s scope (select, insert, update, delete)

Multiple foreign keys match the embed name. Disambiguate with the constraint name:

?select=id,author:users!posts_author_id_fkey(name)

If a migration file’s content changed after it was applied, Reactor rejects re-application. Never edit applied migrations—create a new migration file instead.

Policies may be too restrictive, or X-Reactor-Org may not match the rows’ org_id. Verify the active org and policy expressions.