Baselyra documentation
A self-hosted backend-as-a-service: Postgres with row level security, an auto-generated REST API over your tables, authentication, file storage, realtime and an admin Studio — running as one Node process next to one Postgres container, in a few hundred megabytes of RAM.
The path
Six pages, in this order. It takes about an hour end to end, and step 2 is the one that stops a public table being world-writable.
-
Install and make the first query
Clone, run
./scripts/setup.sh,docker compose up -d. Two containers start, migrations apply, and the Studio is onhttp://127.0.0.1:3130. Then create a table, mint the keys and read a row. -
Authorisation is Postgres', not JavaScript's. Every request runs as
anon,authenticatedorservice_rolewith the verified JWT claims published to the session, and policies decide the rest. A table inpublicwith RLS off is readable and writable by anyone holding the anon key — which is a public key that ships in your frontend. -
Filters, ordering, pagination, one level of embedded resources, upserts and RPC over every table, view and function in
public. A PostgREST-compatible subset, so existing client patterns transfer. -
Email and password, magic links, one-time codes, recovery and email change, plus OAuth sign-in and phone codes over SMS. Refresh tokens rotate and reuse is detected. Read the part about Studio accounts and application users being two unrelated populations.
-
Buckets on local disk with MIME allowlists, per-bucket size limits, signed URLs and Range requests. Policies on
storage.objectsguard files exactly as they guard a table. -
Put it behind a proxy and back it up
nginx or Apache, TLS, SMTP,
pg_dumpplus the storage volume, and the security checklist to run before you point a domain at it.
How the pieces fit
One process serves every prefix. It holds two Postgres connection pools: one to the project database your application lives in, and one to the control database that holds Studio accounts and the audit log. Postgres has no cross-database queries without FDW, so a project connection — even one holding the service key, which bypasses RLS entirely — cannot read a Studio password hash.
request.jwt.claims, so the same policies apply through REST, storage and
realtime alike.The Studio
An admin console ships inside the server and is served at /: a data grid with
inline editing, a SQL editor with schema-aware autocompletion, user management, a file
browser, a live realtime inspector, a per-table API reference, an email template editor
and the audit log. It signs in against control.platform_users, which lives in
a different database from your data.
select * from control.platform_users fails with
relation does not exist — which is the correct answer.Read by topic
Core
Clients and operations
In one request
The anon key is public and meant to ship in your frontend. It says only which Postgres role the request runs as; the policies decide what that role may read and write.
import { createClient } from '@baselyra/client';
const bl = createClient('https://api.example.com', ANON_KEY);
const { data, error } = await bl
.from('posts')
.select('id, title, author:users(name)')
.eq('published', true)
.order('created_at', { ascending: false })
.limit(20);
curl 'https://api.example.com/rest/v1/posts?select=id,title&published=eq.true' \
-H "apikey: $ANON_KEY"
alter table public.posts enable row level security;
create policy posts_select_published on public.posts
for select to anon, authenticated
using (published);
What Baselyra does not do
Stated here rather than discovered on day two. In progress means work has started and nothing is shippable yet — plan as though it does not exist. The full list is on What is not built.
| Not available | What you have instead | Status |
|---|---|---|
| Edge functions | Postgres functions over /rest/v1/rpc/:fn | In progress |
| S3 or object storage | Files on the local disk of the host running the app | In progress |
| Image transformation | Files come back exactly as uploaded | In progress |
| A project switcher | control.projects holds one row and every lookup resolves through it, but nothing creates a second | In progress |
| A Studio page for webhooks and jobs | Both are configured over /admin/v1/hooks; the console has no screen for them yet | In progress |
| Apple sign-in | Seven other OAuth providers, or email and phone | Not built |
| Replication and failover | One Postgres; pg_dump plus the storage volume | Not built |
Questions
Is Baselyra a Supabase fork?
No. It is a separate implementation that reuses two of Supabase's public interfaces so
client code transfers: a PostgREST-compatible subset for the REST API, and row level
security as the authorisation model. The server is one Fastify process with eight runtime
dependencies — fastify, five Fastify plugins, pg and
nodemailer. No ORM, no query builder, no Redis, no message broker, no sidecar.
Does Baselyra support Google or GitHub sign-in?
Yes. OAuth ships for Google, GitHub, LinkedIn, Facebook,
Instagram, TikTok and Envato: you add a client id and secret, and the provider writes a
row in auth.identities against the same auth.users record an
email sign-up would have created. Phone one-time codes go
out over Twilio, Vonage, MessageBird, Amazon SNS, Plivo or a webhook you host. There is
no Apple sign-in.
How much memory does Baselyra need?
Roughly 400–600 MB idle across both containers. 1 GB of RAM is enough to start; 2 GB is comfortable. Sizing is on Self-hosting.
Can I move an existing project onto Baselyra?
Tables, rows and users import from Supabase, plain Postgres, Appwrite, Firebase or a
pg_dump file. Password hashes survive from Supabase, Postgres and bcrypt SQL
dumps, so those users never have to reset. Appwrite and Firebase use hash schemes Baselyra
cannot verify, so those users need a password reset —
Importing says exactly which, and what to send them.
For language models
The whole documentation is published as plain text so a model can ingest it in one fetch, following the llms.txt convention.
/llms.txt— every page with a one-line summary./llms-full.txt— the entire documentation as one plain-text file.
Every page is a real file with no client-side routing, so any crawler reading HTML gets the same content a browser does.