VS Code extension
The extension talks to a live instance over /admin/v1, /auth/v1 and /storage/v1 — the same API the Studio uses. There is no local index, no cached copy of your database and no service in between. It holds a service key, so read the first section before you connect.
Read this before you connect
What the extension does with it:
- Stores it in VS Code SecretStorage — the macOS Keychain, libsecret on
Linux, the Windows Credential Manager. Never in
settings.json, never in a workspace file, never inglobalState, and never in a file it writes. - Sends it only as an
apikey/Authorizationheader to the instance URL you entered. - Never logs it. Every string on its way to the output channel, a notification or
an error message passes through a redaction step first — at every log level,
including
debug. - Never puts it in a webview. The SQL results panel receives its data over
postMessageafter loading, so no server value and no credential is ever part of that page's HTML. - Keeps it out of the realtime URL. The WebSocket handshake is made by hand so
the credential travels in an
Authorizationheader instead of the?apikey=query string a browser would be forced to use — a query string ends up in every reverse proxy access log on the way.
There is deliberately no command that reveals the service key. Baselyra: Copy Anon Key copies the anon key, which is public and meant to ship in your frontend.
Install and build
cd vscode
npm install
npm run compile # or npm run watch
npm test # the wire-codec and type-mapping checks
Then press F5 to launch an Extension Development Host, or package it
with npx @vscode/vsce package. No runtime dependencies; two dev dependencies,
@types/vscode and typescript.
Connecting
Run Baselyra: Connect to Instance. You are asked for the origin, shown the warning above, and asked for the service key. Before anything is stored, two checks run:
| Check | What it proves |
|---|---|
GET /health | The URL is a Baselyra, and its database is up |
GET /admin/v1/keys | The key opens the admin API |
The anon key passes the first and fails the second with a 401, so pasting the
wrong one tells you immediately instead of half-working.
Several named profiles are supported. The status bar shows the active one; click it to switch, add or verify. Baselyra: Disconnect forgets the key but keeps the profile, so reconnecting is one command and one paste.
Database explorer
Schemas grouped the way the server groups them — yours first and expanded,
auth and storage collapsed under a lock, baselyra and the catalogs not
returned at all. Tables carry a planner row estimate and an RLS indicator: a
table with row level security off is flagged in the description and drawn in the
warning colour, because that is the one mistake that ships a world-writable API.
Expand a table for its columns with types, primary keys, foreign keys and
defaults, and a Policies node listing every policy with its USING and
WITH CHECK expressions in the tooltip. Right-click for:
| Command | Does |
|---|---|
| View Rows | Opens the rows as a read-only JSON document |
| Copy REST URL | The /rest/v1 URL with every column selected. Only public is exposed through REST, so it says so rather than handing you a URL that 404s. |
| Generate Select Statement | A select … from … limit 100 at the cursor of the SQL file you are in, or a new one |
| Edit Policies | Lists, creates through a guided flow, drops, or opens a definition as an alter policy statement |
| Enable or Disable Realtime on Table | Toggles the NOTIFY trigger |
| Drop Table | A modal naming the row count and size, then asks you to type the table's name. CASCADE is a separate button. |
SQL
Cmd + Enter in a .sql file runs the selection, or the
whole file when nothing is selected. Results open beside the editor in a sortable
grid with CSV and JSON export.
A failure becomes a diagnostic on the offending token. Baselyra returns the
Postgres position with its errors, and the extension maps that offset back
through your selection into a range in the document, so the squiggle lands on the
word Postgres actually objected to. The SQLSTATE is the diagnostic's code and the
hint is in the message.
Type generation
Baselyra: Generate TypeScript Types reads the live schema and writes a
Database type into the workspace, in the shape @baselyra/client expects.
import { createClient } from '@baselyra/client';
import type { Database } from './database.types';
const bl = createClient<Database>(url, ANON_KEY);
const { data } = await bl.from('posts').select('*'); // data: Post[] | null
Row, Insert and Update are emitted per table, with Insert marking
a column optional when it has a default, is an identity column, or is nullable.
Views get a Row. Only public is emitted, because only public is
reachable through /rest/v1.
Set baselyra.generateTypesOnSave to regenerate after saving a .sql file.
When the live schema drifts from what was generated, the extension offers to
regenerate — once per distinct schema, so declining is not asked again for the same
change.
Storage and users
Storage shows buckets with their public/private flag, size limit and MIME allowlist, browsed one prefix at a time the way the server lists them. Upload files from the workspace — the content type is derived from the extension, which is what the bucket's MIME allowlist is checked against — download, delete with a confirmation, and copy a public or signed URL. Asking for a public URL on a private bucket offers a signed one instead of handing you a link that 404s.
Users lists rows of auth.users — the application users of the app you
are building — with confirmation state, ban state and last sign-in, searchable by
email substring or exact id, paged with an explicit "load more". Copy an id, open
the full JSON, or delete with a confirmation.
Realtime inspector
Baselyra: Subscribe to Table picks a table — realtime-enabled ones first —
and streams postgres_changes, broadcast and presence frames into the
Baselyra Realtime output channel, with a status bar indicator and a live event
count. If the table has no NOTIFY trigger it says so and offers to add one,
because a subscription to a table without one silently emits nothing.
An optional filter (room_id=eq.42) is offered — a filter is a convenience, not
a security boundary. DELETE events are annotated in the log: a deleted row cannot
be re-read, so deletes are the one event delivered without an RLS re-check.
Settings
| Setting | Default | Effect |
|---|---|---|
baselyra.url | "" | Default URL offered when creating a profile |
baselyra.generateTypesOnSave | false | Regenerate types after saving a .sql file |
baselyra.typesPath | src/database.types.ts | Where the Database type is written |
baselyra.requestTimeout | 30000 | Milliseconds before a request is aborted |
baselyra.logLevel | info | off, error, info, debug |
baselyra.rowPageSize | 100 | Rows fetched when opening a table |
baselyra.signedUrlExpiry | 3600 | Seconds a signed storage URL stays valid |
There is no key setting, on purpose.
Snippets
One snippet file, contributed to TypeScript, JavaScript, Dart and PHP, with prefixes namespaced by dialect so the wrong language never wins a completion.
| Prefix | For |
|---|---|
bl-client bl-select bl-embed bl-single bl-insert bl-update bl-upsert bl-delete bl-rpc | @baselyra/client queries |
bl-signin bl-signup bl-magiclink bl-authstate bl-signout | Auth |
bl-channel bl-presence | Realtime |
bl-upload bl-signedurl bl-publicurl bl-list | Storage |
bl-ai | The DeepSeek relay |
bl-policy | The four policies that make a table per-user private |
bl-dart-* · bl-php-* | The hand-rolled Dart and PHP clients |
What it does not do
- No
ALTER POLICY./admin/v1creates and drops policies; it has no update. - No table or column editing. Creating a table, adding a column and altering a type are SQL, in the SQL editor. The extension will not grow a schema designer that generates DDL you cannot see.
- No row editing. Rows open as a read-only JSON document. Writing a row
is an
updateyou can read before you run it. - No user creation or editing. The Users view lists, searches and deletes.
- One project per profile, because Baselyra has one project per instance In progress.
- Realtime broadcast and presence are read-only here. The inspector watches; it does not send.
Failure modes
| What you see | Why | Fix |
|---|---|---|
401 when verifying a new profile | You pasted the anon key, not the service key | Studio → Settings, or GET /admin/v1/keys |
/health passes and the second check fails | The URL is right and the credential is not | Same as above |
| A table is missing from the tree | It is in a hidden schema, or the schema response is stale | Baselyra: Refresh |
| Generated types do not match the API | The schema changed since generation | Regenerate; the extension offers to when it notices |
| The realtime inspector shows nothing | The table has no trigger, or a proxy ate the Upgrade header | Accept the offer to add the trigger; then the 101 test |
| A public URL command offers a signed one instead | That bucket is private | Expected — a public URL would 404 |