$ supablock
Read-only FUSE filesystem

Your Supabase account,
mounted at /mnt/supabase

supablock mirrors the Supabase Management API as a browsable directory tree. Log in once, mount, and inspect organizations, projects, config, API keys, edge functions, storage buckets, auth providers and realtime — plus table rows as CSV or JSON — with the tools you already trust: ls, cat, grep, diff.

supablock — zsh

Why a filesystem

Everything is a file. Now your infrastructure is too.

No SDK, no dashboard tab farm, no one-off scripts. Your whole account becomes greppable, diffable and scriptable, and the guarantees are built in:

mount -o ro

Read-only by construction. The Management API sees only GET; the database/ tree reads through the Data API with GET only; the kernel rejects every write with EROFS.

ttl + single-flight

Cached & rate-limit friendly. ls -R costs one request per endpoint, not hundreds. A 429 degrades to EAGAIN — never a hang.

sbp_…f23a

Token-safe. Stored at mode 0600, never logged, never rendered into the tree. Secret API keys stay redacted unless you opt in.

What you can browse

One tree, your whole account

Every read-only surface of the Supabase Management API is a path. Organizations nest projects; each project unfolds into its config, keys, edge functions, storage and auth — and, if you opt in, its table rows. Directories are lazy and cached, so the tree only talks to the API for the parts you actually open.

/mnt/supabase
└── organizations/<org>/
    ├── info.json  members.json  regions.json     # org metadata, members, regions
    └── projects/<ref>/
        ├── info.json                            # project metadata
        ├── health                               # auth · db · realtime · rest · storage
        ├── config/
        │   ├── auth.json   database.json        # core project settings
        │   ├── realtime.json   storage.json     # realtime & storage settings
        │   └── auth/
        │       ├── sso/<id>/info.json        # SAML SSO providers
        │       └── third-party/<id>/info.json # Firebase · Auth0 · Cognito · …
        ├── api-keys/
        │   ├── publishable                      # always shown
        │   └── secret                           # redacted until you opt in
        ├── functions/<slug>/
        │   ├── info.json                        # metadata, verify_jwt, version…
        │   └── body                             # the deployed eszip bundle
        ├── storage/buckets/<name>/info.json  # visibility, size limit, MIME types
        ├── branches/<branch>/info.json        # preview branches
        └── database/<schema>/<table>/rows-*.csv # rows via the Data API, GET-only

JSON files are pretty-printed with sorted keys, so diff between projects is clean; edge-function body files are the deployed eszip bundles as raw bytes.

Install

Install in one line

The installer downloads the single-file binary for your platform from the GitHub releases — no Erlang/Elixir needed. FUSE is built in on Linux; on macOS install macFUSE or FUSE-T first. (Prefer building from source? The README has the full recipe.)

$ curl -fsSL https://filipecabaco.github.io/supablock/install.sh | sh

Then let the tool check its own environment — every failed check comes with a fix hint:

$ supablock doctor
 /dev/fuse present
 unmount tool on PATH (fusermount3)
 config dir mode 0700
 credentials mode 0600
 efuse port binary compiled

Docker

One command — docker run

The filipecabaco/supablock image needs nothing on your machine but Docker: it logs you in, mounts your account inside the container, and drops you into a shell at /supabase — with ls, cat, grep, find and diff ready to go. Exiting the shell unmounts and removes the container.

$ docker run -it --rm \
    --device /dev/fuse --cap-add SYS_ADMIN \
    --security-opt apparmor=unconfined \
    -v supablock-config:/root/.config/supablock \
    filipecabaco/supablock
login once, in the volume

Credentials live on your machine. An empty supablock-config volume triggers the login flow — open the printed URL on your host, type the verification code — and every later run goes straight to the mounted shell. docker volume rm supablock-config logs you out.

--device /dev/fuse

The flags are FUSE plumbing. Mounting inside a container needs the host's FUSE device, CAP_SYS_ADMIN, and — on Debian/Ubuntu hosts — an AppArmor exemption for mount. The tree stays inside the container's namespace, read-only as always.

scriptable

One-off commands and CI. Anything after the image name runs at the mountpoint — … filipecabaco/supablock grep -r '"site_url"' organizations — and -e SUPABLOCK_TOKEN=sbp_… skips the login flow entirely for headless use.

No binary yet? That single command expands to a plain docker run you can copy anywhere — nothing but Docker required. The image is Alpine-based, multi-arch (amd64/arm64), and CI keeps :latest on main:

$ docker run -it --rm \
    --device /dev/fuse --cap-add SYS_ADMIN \
    --security-opt apparmor=unconfined \
    -v supablock-config:/root/.config/supablock \
    filipecabaco/supablock

AI agents

Built for agents: nothing to break, nothing to learn

A read-only tree with deterministic output is the safest thing you can hand an AI agent — exploring it cannot damage anything, and the filesystem shape means an agent's existing file tools are the integration. Verifying Supabase state ("are signups off in prod?", "any public buckets?", "did the config drift?") becomes an ls/cat/diff exercise.

# no mount, no FUSE, no privileges — works in any sandbox
$ export SUPABLOCK_TOKEN=sbp_...
$ supablock serve &   # optional: one warm cache for every read below
$ supablock grep -l '"disable_signup": false' organizations/my-org
$ supablock find organizations/my-org -name 'auth.json' | supablock cat - | jq .site_url

# or with zero installation, via the Docker image
$ docker run --rm -e SUPABLOCK_TOKEN filipecabaco/supablock \
    cat organizations/my-org/projects/<ref>/config/auth.json
ls · cat · find · grep, no mount

Reads without FUSE. ls, cat, head, tail, find and grep resolve tree paths straight off the API — same guarantees as the mount, zero privileges, coreutils semantics and exit codes. They connect with ordinary pipes (find … | cat -), and supablock serve gives them one shared warm cache for batch reads — still no FUSE.

npx skills add

An installable skill. npx skills add filipecabaco/supablock teaches a skills-aware agent (Claude Code and friends) how to authenticate and verify account state, with recipes for health, signup policy, bucket visibility and config-drift checks.

llms.txt

A machine-readable summary. llms.txt gives an agent the tree shape, auth story and ground rules in one fetch. Hand agents a scoped, revocable access token and leave expose_secrets off.

Quick start

Two commands to a mounted account

1

Authenticate

login just works: an OAuth2 + PKCE browser flow storing short-lived, scoped tokens that refresh automatically. No browser or headless environment? --token sbp_… always works.

$ supablock login
Open this link in your browser to authorize supablock:

  https://api.supabase.com/v1/oauth/authorize?client_id=…&code_challenge=…

Waiting for the callback on http://localhost:53682/callback …
 Logged in via OAuth — authenticated, 2 organizations found.
  Stored in ~/.config/supablock/credentials (mode 0600).
  Tokens are short-lived and refresh automatically.
2

Mount and browse

mount returns immediately — the filesystem runs in the background and you browse from the same shell. The mountpoint defaults to ~/Supabase; override it with --path or config set mountpoint. --foreground blocks instead.

$ supablock mount
Mounting at ~/Supabase (background).
Log: ~/.local/state/supablock/supablock.log
Check status: supablock status
$ ls ~/Supabase/

Onboarding a whole team?

One command applies a shared profile — a flat JSON of config keys (OAuth app, mountpoint convention, TTLs) — logs in, and offers the auto-start service. Secrets and tokens never belong in a profile.

$ supablock setup https://team.example.com/supablock.json

Auto-start

Keep it mounted — as a background service

One command turns the mount into a per-user service that comes up at login and restarts on failure — a systemd user unit on Linux, a launchd agent on macOS. No root, same config and credentials as the CLI, and stopping the service unmounts cleanly. Set a mountpoint first (supablock config set mountpoint /mnt/supabase).

$ supablock service install     # register + start at login
$ supablock service status      # is it installed and running?
$ supablock service uninstall   # remove it

One-liners

Your account, greppable

Output is deterministic — JSON is pretty-printed with sorted keys — so byte sizes are exact and diff between projects is clean.

Which projects disable signups?

$ grep -l '"disable_signup": true' /mnt/supabase/organizations/*/projects/*/config/auth.json

Compare two projects' auth config.

$ diff /mnt/supabase/organizations/my-org/projects/{refA,refB}/config/auth.json

Health of every service, everywhere.

$ cat /mnt/supabase/organizations/*/projects/*/health

Which storage buckets are public?

$ grep -l '"public": true' /mnt/supabase/organizations/*/projects/*/storage/buckets/*/info.json

Unpack a deployed edge function's source (the body is an eszip bundle).

$ npx eszip extract .../projects/<ref>/functions/hello/body ./hello-src

Table data

Rows as files, too

The Management API has no row endpoint, so table browsing reads through each project's Data API (PostgREST) with GET requests only. There's nothing to set up and no database password: every project already has a database/ folder — one directory per exposed schema, one per table, rows paged into .csv (default) or .json files — populated on first read with a key supablock fetches for you.

Browse schemas → tables → row pages. No setup.

$ ls  /mnt/supabase/organizations/*/projects/<ref>/database/public/users
rows-000000.csv  rows-000500.csv  rows-001000.csv
$ cat .../database/public/users/rows-000000.csv

Format, page size, and which key to read with.

$ supablock config set db_format json     # csv (default) | json
$ supablock config set db_page_size 1000  # rows per file (default 500)
$ supablock config set db_key publishable # secret (default) | publishable

By default rows are read with the project's service_role key, which bypasses RLS so you see every row; db_key publishable switches to the anon key and shows only what RLS allows. Pages are ordered by primary key so paging is stable, and every request is a GET — no direct database connection, no extra credential.

Commands

The whole CLI

CommandWhat it does
supablock setup [profile]One-command onboarding: apply a shared team profile (URL or file), log in, offer the auto-start service.
supablock loginBrowser login: OAuth2 + PKCE with scoped, auto-refreshing tokens (app identity baked into released binaries), dashboard session flow otherwise. --token sbp_… pastes a token; --no-browser just prints the URL.
supablock logoutDelete the credential and revoke the OAuth grant.
supablock statusAuth state (masked token), org count, mount state, last-seen rate limits.
supablock doctorEnvironment checks with a fix hint per failure.
supablock config set|get|listMountpoint, cache TTLs, http_timeout_ms, expose_secrets, db_format, db_page_size, db_key.
supablock mount [--path dir] [dir]Mount in the background and return immediately (default ~/Supabase); recovers stale mounts automatically. --foreground blocks instead.
supablock unmount [dir]Unmount from another shell.
supablock ls [path]List a tree directory straight off the API — no mount, no FUSE, no privileges.
supablock cat <path|-> [path…]Print tree file(s) straight off the API. - reads paths from stdin as they arrive (find … | cat -); -0 takes NUL-delimited stdin.
supablock head|tail [-n N] <path…>First/last lines of tree file(s) — sample a big rows file before fetching every page.
supablock find [path] [filters]Walk the tree and print paths, find(1)-style: -type f|d, -name <glob>, -maxdepth N, -print0.
supablock grep [-iln] <pat> [path…]Search file contents; directories recurse, binary bodies are flagged, exit 1 means no match — grep(1) semantics.
supablock serve [stop]Mountless cache daemon: while it runs, every ls/cat/find/grep on the machine shares its warm cache. No FUSE needed.
supablock refreshFlush the cache of a live mount or serve daemon; next reads re-fetch.
supablock refresh --checkReport cache staleness (total / past-TTL entries) without flushing.
supablock service install|uninstall|statusAuto-start the mount at login — systemd user unit (Linux) or launchd agent (macOS).

Exit codes: 0 ok · 1 usage (grep: no matches) · 2 not authenticated · 3 API/network · 4 environment · 141 downstream pipe closed. Output is pipe-safe: supablock cat … | head ends quietly, and bodies pass through byte-exact.

Security

Read-only, by construction

The write path does not exist

Every Management API request is a GET — there is no code path for anything else. The database/ tree reads through the project's Data API (PostgREST) with GET requests only, all generated by supablock. The FUSE layer exposes no mutating callbacks, and the mount is created with -o ro, so touch, rm, mkdir and every other write fails in the kernel with EROFS before it reaches userspace.

  • Row browsing needs no database password or extra credential: the database/ tree reuses a key supablock fetches from the Management API — service_role by default (bypasses RLS), or anon with db_key publishable. The key is used only for GETs to the Data API and is never written into the tree.
  • The token lives in ~/.config/supablock/credentials (mode 0600, dir 0700), is never logged, and shows up masked (sbp_…f23a) in status.
  • api-keys/secret renders as REDACTED — run: supablock config set expose_secrets true until you explicitly opt in. The publishable key is always visible.
  • SUPABLOCK_TOKEN overrides the stored credential — the only environment variable in play, intended for CI.
  • Request deadlines (http_timeout_ms, default 8000) turn slow calls into EIO; a rate-limit turns into EAGAIN. The filesystem never hangs.

Troubleshooting

When something's off

"Transport endpoint is not connected"
A stale mount from a crashed session. Run supablock mount again — it recovers stale mounts automatically — or fusermount3 -u <mountpoint>. If the VM is killed outright, the port process notices and unmounts by itself.
Reads fail with "Resource temporarily unavailable"
That's EAGAIN: the Management API rate-limited you. Wait a moment and retry; supablock status shows the last-seen remaining budget per scope.
Mount fails outright
Run supablock doctor. It checks /dev/fuse, unmount tools, file permissions and the compiled FUSE port, and prints a fix per failed check (exit code 4 if anything fails).
Where are the logs?
The log path is printed every time you mount: ~/.local/state/supablock/supablock.log. Pass --verbose to mount for debug level. Tokens are scrubbed from all log output.