The Studio
The Studio is compiled into the server and served at /. It talks to /admin/v1 and nothing else — every screen here is a view over an API you can call yourself. Its accounts live in the control database and have nothing to do with your application's users.
Signing in
Open the instance root and sign in with the credentials scripts/setup.sh
printed on the first boot. That account lives in control.platform_users, in the
control database, and is created by scripts/migrate.js from
BASELYRA_ADMIN_EMAIL and BASELYRA_ADMIN_PASSWORD.
POST /admin/v1/login is capped at five attempts per minute per IP: it is the
one route on the instance that turns a password into a token holding
service_role, and it is reachable without any credential.
The database page
Schemas are grouped the way the server groups them: your schemas first and
expanded, auth and storage collapsed under System, and baselyra,
pg_catalog, information_schema and pg_toast not returned at all. Every
table carries a planner row estimate and its RLS state.
The pages
| Page | What it does | API behind it |
|---|---|---|
| Overview | Instance health, counts, largest tables, recent audit entries, and the Connect panel with your URL and anon key | /admin/v1/overview, /usage, /keys |
| Database | Table list with RLS state, data grid, row editor, column and policy editing | /admin/v1/schema, /tables/…/rows, /tables, /policies |
| SQL | Editor with schema-aware autocompletion, a results grid, saved snippets, and a read-only/write toggle | /admin/v1/sql |
| Auth | Application users: list, search, create, edit, ban, delete | /auth/v1/admin/users |
| Storage | Buckets, a file browser, upload, preview and signed URLs | /storage/v1/* |
| Realtime | Which tables are enabled, the live socket count, and an event inspector over a WebSocket | /admin/v1/realtime |
| API | A generated reference per table, the project keys, and copyable snippets | /admin/v1/schema, /keys |
| AI | Ask-your-database and SQL generation. Hidden entirely when no key is configured. | /ai/v1/* |
| The six templates, with a live preview and a test send | /admin/v1/email-templates/* | |
| Settings | Instance settings, keys, the team, and the audit log | /admin/v1/settings, /team, /roles, /logs |
Cmd + K opens a command palette that jumps between pages,
tables and actions. Below the md breakpoint the sidebar becomes a drawer and
every table scrolls inside its own container.
The SQL editor
The editor defaults to read-only. Flipping the segmented control beside Run to Write tints it with the warning colour, because that is the mode that can drop things. Cmd + Enter runs the selection, or the whole statement when nothing is selected.
{"columns":[{"name":"count","type":"int8"}],
"rows":[{"count":"3"}],
"rowCount":1,
"durationMs":4,
"command":"SELECT"}
Three properties are worth knowing before you use it:
- It runs as
baselyra_sql, not as the server's own login role. That role keeps every object right the owner has — including reading every password hash inauth.users— and loses access to the host. See The SQL editor. - The audit row is written before the statement runs, on a separate connection, so a statement that fails or rolls back is still recorded. It contains the statement text: do not type a password into the editor and expect it to be forgotten.
- A
viewermay run read-only statements and nothing else. The write half of that decision is made from the request body, at the one point that can see it.
Studio accounts
| Route | Capability |
|---|---|
POST /admin/v1/login | — (rate limited to 5/min per IP) |
GET /admin/v1/me · PUT /admin/v1/me | any signed-in account |
GET /admin/v1/team | team.read |
POST PUT DELETE /admin/v1/team | team.manage |
GET /admin/v1/roles | team.read |
POST PUT DELETE /admin/v1/roles | team.manage |
Changing your own email or password requires current_password. You cannot
delete your own account, and you cannot delete or demote the last owner — an
instance with no owner cannot be recovered from the Studio at all, only by an
INSERT into control.platform_users from psql against the control
database.
Roles and capabilities
Three roles ship, and they are rows rather than constants — an owner can add more with their own capability set.
| Role | Holds |
|---|---|
owner | Every capability, including team.manage |
admin | Everything except managing accounts and roles |
viewer | Every read capability, including keys.read. Every mutating route answers 403. |
A capability is checked per route, against the pattern the route was registered with, using the list inside the token's signature:
database.read database.write database.ddl
sql.read sql.write
auth.read auth.write
storage.read storage.write
realtime.read realtime.write
import.run
settings.read settings.write keys.read
email.read email.write
logs.read
team.read team.manage
Tokens are stateless
A Studio token lasts one hour and nothing is stored server-side. No route re-checks the account row, so:
- Signing out records an audit entry and nothing more.
- Deleting an operator's row stops them signing in again; a token already issued keeps working until it expires, at most an hour later.
- To cut every Studio token off at once, rotate
JWT_SECRET— which also invalidates both project keys and every application session.
The service key is a second way in and always has been: it is the instance's own
credential, it predates any Studio account, and /admin/v1 accepts it so scripts
and first-boot recovery keep working. It has no /me, because it is not a person.
What the Studio does not have
| Missing | Do it here instead |
|---|---|
| A page for database webhooks | /admin/v1/hooks/webhooks — the routes exist, the UI does not yet In progress |
| A page for scheduled jobs | /admin/v1/hooks/jobs, same In progress |
ALTER POLICY | /admin/v1 creates and drops policies; editing one is a drop and a create, or an alter policy in the SQL editor |
| A project switcher | control.projects holds one row that every lookup resolves through, but nothing creates a second In progress |
| Two-factor authentication | Not built. Rate limiting and the audit log are what there is. |
The audit log
Every Studio sign-in, SQL execution, DDL statement, policy change, settings write
and import is written to control.audit_log — in the control database, so
the SQL editor cannot read it and an operator cannot quietly edit their own trail.
curl -s "$URL/admin/v1/logs?limit=100" -H "authorization: Bearer $ADMIN_TOKEN"
Read it under Studio → Settings. Import runs redact as they go: connection
strings, passwords and API keys are replaced in the audit row, in the
control.import_runs summary, in the progress stream and in error messages.
Failure modes
| What you see | Why | Fix |
|---|---|---|
| The Studio loads as a blank page | The server is pointed at the Vite source rather than a built Studio | Put studio/dist next to dist/ — Installation |
| Sign-in fails with an application user's credentials | Studio accounts are in another database entirely | Use the credentials setup.sh printed |
Every request is 401 a moment after signing in | JWT_SECRET changed since the token was issued | Sign in again |
403 on a page that used to work | The account is a viewer, or a custom role is missing a capability | GET /admin/v1/roles shows the matrix |
select * from control.platform_users fails | Correct and deliberate — it is in another database | Nothing. That is the boundary. |
| The AI pages are missing | /ai/v1/status reports enabled: false | Set DEEPSEEK_API_KEY and restart |
| A new table does not appear | The schema response is cached briefly; DDL through the Studio invalidates it | Refresh |