Baselyra Docs

Getting started

Baselyra is a self-hosted backend that runs as two containers: one Node process and one Postgres. This page is the orientation — what the pieces are, which one decides who may read a row, and where to go next. If you would rather type than read, go straight to the quickstart.

What you get

One Node 22 process serves every API prefix and the admin Studio. It talks to one Postgres 17 server, which holds two databases: the project database with your tables, and the control database with Baselyra's own operating data.

Your app browser, mobile, server HTTPS / WSS Baselyra one Node 22 process, Fastify /auth/v1 /rest/v1 /storage/v1 /realtime/v1 /admin/v1 /ai/v1 / serves the Studio files on disk at STORAGE_ROOT baselyra the project database public, auth, storage baselyra_control Studio accounts, audit import history, metering One Postgres 17 server, two databases. No cross-database query joins them.
The whole deployment. Two containers. The split between the two databases is a security boundary, not a convention — Postgres has no cross-database queries without FDW.
PrefixWhat it is
/rest/v1An auto-generated REST API over every table, view and function in public. Read from the catalog on demand, so a table created a second ago is queryable now.
/auth/v1Sign-up, sign-in, refresh-token rotation, magic links, one-time codes, phone codes over SMS and third-party sign-in.
/storage/v1Buckets on local disk, with metadata in storage.objects so the same policies guard files as guard tables.
/realtime/v1One WebSocket carrying database change feeds, broadcast and presence.
/admin/v1What the Studio talks to: schema, SQL, users, settings, keys, imports, webhooks and scheduled jobs.
/ai/v1Optional DeepSeek routes. Every one answers 503 until DEEPSEEK_API_KEY is set.
/healthLiveness plus whether both databases answer SELECT 1.
/The compiled Studio, served last so it never shadows an API prefix.

Postgres decides, not JavaScript

This is the single idea the rest of the product is built on. Baselyra performs no authorisation in JavaScript. No route handler checks ownership and no query is narrowed for security reasons in the server process. Every request that touches your data runs inside a transaction that has switched to the caller's Postgres role and published their verified JWT claims to the session; row level security policies do the rest.

1. Request apikey: authorization: 2. verifyJwt HS256 pinned, exp checked 401 if it fails 3. asRole() — one transaction BEGIN; SET LOCAL role = 'authenticated'; SET LOCAL request.jwt.claims = '{...}'; 4. Postgres evaluates the policy using (user_id = auth.uid()) rows it rejects are simply absent — never an error 5. COMMIT both settings were LOCAL, so the pooled connection is clean no identity survives into the next request
The request path. The same five steps run for REST, storage and realtime, which is why one policy governs all three.
What the caller sentPostgres roleRLS
Nothing, or the anon keyanonEnforced
Authorization: Bearer <user access token>authenticatedEnforced, and auth.uid() is that user
The service keyservice_roleBypassed — the role has BYPASSRLS
A Studio session on /admin/v1service_roleBypassed

The anon key is public and is meant to be. It is not a password; it only names the role a request runs as. The service key is the opposite: it reads and writes every row in the project database, so it belongs on a server and nowhere else.

Two identities that are not the same

Baselyra keeps operators and application users in two unrelated tables in two unrelated databases. Conflating them is the mistake this project most wants you not to make.

Studio accountApplication user
WhoYou, and whoever else operates the instanceThe end users of the app you build
Tablecontrol.platform_usersauth.users
Databasebaselyra_controlbaselyra (the project database)
Signs in atPOST /admin/v1/loginPOST /auth/v1/token
Token carriesrole: service_role, typ: "platform"role: authenticated, sub, app_metadata
Can open the StudioYesNever
Visible to /rest/v1No — it is in another databaseOnly through your own policies

Consequences worth knowing on day one:

  • No row in auth.users can ever open the Studio, however it is configured. Studio tokens carry a typ: "platform" claim inside the signature, and exactly one endpoint mints it.
  • select * from control.platform_users in the SQL editor fails with relation does not exist. That is the correct outcome: the hashes are in a database that connection cannot address.
  • There is no is_admin column and no auth.is_admin() function. Both were removed. An application's own admin role lives in app_metadata and is read in a policy as (auth.jwt() -> 'app_metadata' ->> 'role') = 'admin'.

The shortest real example

A table, a policy, a user, a query. Every line of this works against a fresh install.

create table public.posts (
  id         bigint generated always as identity primary key,
  author     uuid not null default auth.uid() references auth.users(id) on delete cascade,
  title      text not null,
  published  boolean not null default false,
  created_at timestamptz not null default now()
);

alter table public.posts enable row level security;

create policy posts_read on public.posts
  for select to anon, authenticated
  using (published or author = (select auth.uid()));

create policy posts_write_own on public.posts
  for insert to authenticated
  with check (author = (select auth.uid()));
curl -s "$URL/rest/v1/posts?select=id,title&published=eq.true" -H "apikey: $ANON_KEY"
[{"id":1,"title":"Hello"}]

Nobody sent an author. The column defaults to auth.uid(), and the policy's with check would refuse anything else — ownership is established by the database, not by trusting the client.

What Baselyra does not do

Stated plainly, because finding out on day two is worse.

Not builtWhat you have instead
Edge functions In progressPostgres functions called over /rest/v1/rpc/:fn
Replication, read replicas, failover In progressOne Postgres. Backups are pg_dump plus the storage volume.
An S3 or object-storage backend In progressFiles on the local disk of the host running the app
Image transformation In progressFiles come back exactly as they were uploaded
A project switcher In progresscontrol.projects holds one row that every lookup resolves through, but nothing creates a second
Apple sign-inGoogle, GitHub, LinkedIn, Facebook, Instagram, TikTok and Envato — see Third-party sign-in
Two-factor authentication on Studio accountsA password, rate limiting, and an audit log

Database webhooks and cron-scheduled jobs are built — they run in-process with their queue in the project database, under /admin/v1/hooks/* — but the Studio has no page for either yet.

Read in this order

Edit this page Report a problem

Esc
navigate open Esc close