Getting started¶
This walks through running rel against a real schema, then making a read and a write request,
with plain curl — no client library needed to try any of this out. The schema is a small
hotel-booking database — chains, properties, rooms, guests, bookings — close enough to a real
app to be worth keeping around as you read the rest of the docs.
Run rel against a database¶
Point rel at Postgres with a single connection string:
That's the entire minimum configuration. On startup, rel connects, introspects every table,
view, function, and constraint reachable on the connecting role's search path, and starts
serving on port 8080. See Run it manually if you don't have a rel
binary yet — a prebuilt release, Docker, or building from source.
Every setting has an environment-variable form (REL_<SECTION>__<KEY>, __ separating
nesting), a config-file form (TOML, YAML, or HUML), and a --flag form — flags win over
environment variables, which win over the config file. pg.uri above is REL_PG__URI,
--pg.uri, or pg.uri under [pg] in a config file, interchangeably.
Before the first request: anonymous access¶
None of the curl examples below send a session — they're all unauthenticated requests. rel
only serves those if pg.query.anonymous_role (default ~anonymous) is a real role in your
database; otherwise anonymous access is disabled outright and every request without a session
gets a 401:
create role "~anonymous";
grant usage on schema hotel to "~anonymous";
grant select, insert, update, delete on all tables in schema hotel to "~anonymous";
grant execute on all functions in schema hotel to "~anonymous";
Skip this if you're following Launch it yourself — the dev fixture's schema already grants this to itself. See Authentication for running real, per-user sessions instead of anonymous access once you're past this page.
A query, end to end¶
Every query goes to POST /rel, as JSON, whether it reads or writes. This one reads
four-and-up-star properties, with each property's room types and the chain it belongs to:
curl http://localhost:8080/rel \
-H 'Content-Type: application/json' \
-d '{
"relation": "properties",
"schema": "hotel",
"where": [">=", "star_rating", 4],
"join": {
"room_types": {
"relation": "room_types",
"schema": "hotel",
"on": { "property_id": "id" }
},
"chain": {
"relation": "chains",
"schema": "hotel",
"on": { "id": "chain_id" }
}
},
"select": {
"id": "id",
"name": "name",
"star_rating": "star_rating",
"chain": "chain",
"room_types": "room_types"
}
}'
room_types is joined the way a child row usually is — its own property_id points back at
the parent's id — and comes back as an array, one entry per room type. chain is the
opposite direction: properties.chain_id points at chains.id, so it comes back as a
single object, not an array. rel tells the two apart from the join's on shape; you never
declare "to-one" or "to-many" yourself.
[
{
"id": 1,
"name": "Marina Bay Hotel",
"star_rating": 4,
"chain": { "id": 3, "name": "Blue Horizon" },
"room_types": [
{ "id": 1, "property_id": 1, "name": "Deluxe", "base_price": "189.00", "capacity": 2 },
{ "id": 2, "property_id": 1, "name": "Suite", "base_price": "349.00", "capacity": 4 }
]
}
]
See The query language for where, select, and join in full.
The same query, as a plain GET¶
A read-only query like the one above is also reachable as a plain GET, the whole tree encoded
into the URL instead of a JSON body — handy for anything you want cacheable, bookmarkable, or
usable from a plain link (or, here, a curl one-liner with no -d at all):
curl 'http://localhost:8080/rel?relation=properties&schema=hotel&where=gte(star_rating,4)&select=id,name,star_rating,chain,room_types&join.chain.relation=chains&join.chain.schema=hotel&join.chain.on.id=chain_id&join.room_types.relation=room_types&join.room_types.schema=hotel&join.room_types.on.property_id=id'
This decodes to exactly the same query tree as the POST body above, and returns the same rows.
GET /rel only ever reads — no data, no batching, no write-only fields — see Querying with
GET for the full grammar (where='s operator-as-call
syntax, select='s own/full shorthands, and more).
Writing, nested, before the parent even has an id¶
Send {"query": <Relation>, "data": <payload>} instead of a bare relation to write. This
creates a brand-new property and two new room types in one request — data is an array (a
write's payload always mirrors what reading this same query would produce, and a relation reads
back as an array of rows, root included) and neither the property nor its room types carry an
id, because none of them exist yet:
curl http://localhost:8080/rel \
-H 'Content-Type: application/json' \
-d '{
"query": {
"relation": "properties",
"schema": "hotel",
"join": {
"room_types": {
"relation": "room_types",
"schema": "hotel",
"on": { "property_id": "id" }
}
},
"select": { "id": "id", "name": "name", "room_types": "room_types" }
},
"data": [{
"name": "Marina Bay Grand Hotel",
"room_types": [
{ "name": "Deluxe", "base_price": "199.00", "capacity": 2 },
{ "name": "Penthouse", "base_price": "899.00", "capacity": 2 }
]
}]
}'
[
{
"id": 14,
"name": "Marina Bay Grand Hotel",
"room_types": [
{ "id": 43, "property_id": 14, "name": "Deluxe", "base_price": "199.00", "capacity": 2 },
{ "id": 44, "property_id": 14, "name": "Penthouse", "base_price": "899.00", "capacity": 2 }
]
}
]
rel resolves the write order itself: properties is inserted first, and its freshly generated
id is threaded into each room_types row as it's inserted right after, all inside one
transaction — no client-side orchestration, no separate request to get an id back before you
can insert what depends on it.
Send that same shape again, this time with ids already filled in from a prior read, and rel
updates matching rows instead of inserting duplicates — an incoming relation like room_types
here defaults to merge: insert what's new, update what matches, delete what the payload leaves
out entirely. The root relation defaults to insert instead (as above); set write_mode on it
explicitly ("write_mode": "upsert") to update an existing row rather than insert a new one. See
The query language ## Writing data back for every write mode and
how deletes are ordered against foreign keys.
Where to go next¶
- Launch it yourself — bring up this exact schema, seeded with fake data, and point rel at it locally.
- Run it manually — a prebuilt release binary, the published Docker image, or building from source.
- Docker deployment — a realistic deployment: rel and Postgres in Docker, fronted by a reverse proxy, with automatic TLS.
- Configuration — how settings are supplied and layered; the
Configuration reference lists every setting beyond
pg.uri: secrets, logging, pool sizing, well-known-query directories. - The query language — the full shape of
where/select/join, operators, aggregates, and every write mode. - HTTP layer — arbitrary server-side logic as a Postgres function: login flows, server-rendered HTML via Jet templates, file uploads, static files.
- Authentication — username/password, OpenID Connect, and SAML, and how a request ends up running as a particular Postgres role.