The command line for SelfHost. One binary to manage databases, projects, and deployments.
This project is young. The whole command tree is registered and every command has help, but the ones marked ⏳ in the command list answer not implemented yet until that part is built. Sign-in, profiles, organizations, raw API calls, help, completions, the tree printer, the welcome screen and selfhost update work today.
curl -fsSL https://cli.selfhost.dev/install.sh | shThe installer works out your platform and downloads the matching build from cli.selfhost.dev. It checks the SHA-256 against the release manifest before it writes the binary to ~/.local/bin. If that directory is not on your PATH, the installer prints what to add. You need curl, awk, and one of sha256sum, shasum or openssl.
In PowerShell:
irm https://cli.selfhost.dev/install.ps1 | iexIt unpacks selfhost.exe into %LOCALAPPDATA%\Programs\selfhost and adds that directory to your user PATH. In Command Prompt, run install.cmd, which starts the same script through PowerShell.
selfhost update # install the newest release over the one you have
selfhost update --check # just say whether there is oneupdate downloads the build for your platform, checks it against the SHA-256 the release list publishes, and replaces the running binary. It never asks for administrator rights. If it cannot write where the binary sits, it prints the installer command that can and stops. Set SELFHOST_MANIFEST_URL to read a different release list. The installer honours the same name.
The installer stays the way to install into a directory the CLI cannot write, and to move the binary somewhere else in the first place. Releases follow semantic versioning, and every release is tagged vX.Y.Z. The installer has no version pin, so it never fetches a pre-release, and update follows the same rule and leaves a pre-release build alone. Each pre-release tag publishes its own binaries on the releases page. Download the file for your platform and put it on your PATH.
selfhost --version
shasum -a 256 "$(command -v selfhost)" # must match the digest in https://cli.selfhost.dev/latest.jsonDelete the binary and, on Windows, drop the install directory from your user PATH.
You need Rust 1.99.1 or newer.
cargo build
./target/debug/selfhost --helpselfhost --help # everything it can do
selfhost postgres --help # one database engine
selfhost tree # every command, one per line
selfhost completion bash # also zsh and fish
selfhost tui # welcome screen, needs a terminalRunning selfhost with no subcommand on a terminal opens the welcome screen too. Press q to quit. Set SELFHOSTDEV_NO_TUI=1 to make that bare call print help instead. Nothing else is affected: a command that names a subcommand runs that command and never opens the screen.
The built-in profiles are prod, qa, and local. Pick one, or pass a URL per command.
selfhost --profile qa postgres list
selfhost --base-url http://localhost:3000 postgres listFlags have env var equivalents: SELFHOSTDEV_PROFILE, SELFHOSTDEV_BASE_URL, SELFHOSTDEV_ORG.
Output is a table on a terminal and JSON when piped. Force it with -o table|json|yaml or --json. Commands that need a sign-in stop with exit 3 until you run selfhost auth login.
selfhost auth login # opens your browser to finish sign-in
selfhost auth login --no-browser # print the URL instead, for a remote machine
selfhost auth status # what the current profile can do
selfhost auth logout # drop the saved credentials
selfhost auth token # print the access token for scriptsauth login hands off to the console and comes back to the CLI when you are done. On a remote machine, pass --no-browser, open the printed URL yourself, and paste the redirect URL from the browser's address bar back into the terminal. auth status is a checklist and exits 3 when something is missing. auth token prints the short-lived access token on stdout only, so keep it out of logs.
Profiles decide where a command points and which credentials it uses. Select one with --profile or SELFHOSTDEV_PROFILE, add one with selfhost profile add staging --base-url https://api.staging.example.com, and make it the default with selfhost profile use qa.
In CI or any headless environment, set FIREBASE_API_KEY and FIREBASE_REFRESH_TOKEN and no login step is needed. They are the same pair the MCP server uses.
selfhost org list # every organization you belong to
selfhost org show # details for the one you have selected
selfhost org use acme # choose the one other commands use
selfhost org create "Acme Labs" --description "Staging and demos"
selfhost org update --name "Acme Labs EU"
selfhost org delete # asks for the name first
selfhost org members list # the people in it
selfhost org members add dana@example.com --role manager
selfhost org members update-role dana@example.com --role admin
selfhost org members remove dana@example.com
selfhost org roles list # what each role can do
selfhost org invites list # invitations and where they stand
selfhost org invites create dana@example.com
selfhost org invites revoke dana@example.com
selfhost org activity list --page 2 # recent activity, newest firstorg list marks the organization other commands use by default and also shows invitations that are still waiting. org show, org members list, org invites list and org activity list work on that same organization, or on the one you name. org create takes the organization only from the command line, so the selected organization means nothing to it. The caller owns whatever they create.
org create names the new organization and the platform derives the slug from the name. The slug is never something you pass, and org show is where you read it. org update changes the name or the description, and needs at least one of them.
Invitations arrive by email. There is no separate add-member call. org members add and org invites create do the same thing, and the person becomes a member when they accept the invitation. org invites list shows every invitation and its status, not only the ones still waiting. Managing members and invitations needs owner or admin rights on the organization, and the API refuses anything a role cannot do with a message the CLI prints as-is.
org roles list is printed by the CLI itself. The platform ships five system roles, owner, admin, billing, manager and member, and has no endpoint that returns them. A role added on the server needs a newer CLI to appear here.
Deleting an organization, removing a member and cancelling an invitation are destructive, so they ask first. org delete wants the organization's name typed back, and the other two want a yes. --yes answers the prompt for scripts. Without it, a run with no terminal stops and tells you to pass --yes, rather than waiting on a prompt that can never arrive.
Every row of org list and the details org show prints also say which profile they came from, so a machine with several saved profiles is easy to tell apart.
Name an organization by slug (acme) or by pid (org_…). A pid is used as it is. Anything else is looked up among the organizations you belong to, so a slug that does not resolve is a usage error. The organization you name on the command line wins over --org and SELFHOSTDEV_ORG, which in turn win over the organization saved in the profile. An empty value counts as unset. org list and org use ignore --org, because the listing has to work even when the saved organization is gone, and org use is how you replace it.
⏳ means the command is registered but answers not implemented yet. Everything else in the list works today.
| Command | What it is for |
|---|---|
| auth | Sign in, sign out, check the session |
| profile | Saved profiles: endpoints, default org |
| org | Organizations: create, list, show, update, delete, members, roles, invitations, activity |
| api | Call any platform endpoint directly |
| ⏳ config | Your saved default settings |
| ⏳ project | Projects with databases, services, backups |
| ⏳ deploy | Deploy a repo, watch runs, set env vars and domains |
| ⏳ github | Connected repos, branches, build settings |
| ⏳ domain | Custom domains and their DNS status |
| ⏳ postgres, mysql, redis, clickhouse, opensearch | Managed databases, one group per engine |
| ⏳ catalog | Regions, instance types, cost estimates |
| ⏳ billing | Wallet, top-ups, transactions |
| ⏳ cloud | Cloud provider credentials |
| ⏳ network | VPCs, subnets, security groups |
| ⏳ ssh-key | SSH keys for the org and projects |
| ⏳ alert | Alert rules and fired alerts |
| ⏳ scaling | Scaling policies and capacity plans |
| ⏳ webhook | Webhook endpoints for the org |
| tui | The interactive terminal UI |
| tree | Every command, one per line |
| help | Help for any command |
| completion | Shell completions for bash, zsh, fish |
| update | Move this CLI to the newest published build |
Every command takes the same global flags: --profile, --base-url, --org, -o/--format, --json, --no-color, --timeout, --poll-interval, -y/--yes, --dry-run, -q/--quiet, -v/--verbose, --debug.
No command previews a request yet. A command run with --dry-run stops before it does anything and says nothing was sent, so the flag can never be mistaken for a safety net.
selfhost api /organizations/{org}/members --org acme
selfhost api -X GET /organizations -f per_page=50
selfhost api -X PATCH /organizations/{org} -F description="Staging and demos"
echo '{"description":"x"}' | selfhost api -X PATCH /organizations/{org} --input -
selfhost api /organizations/{org} -i --format jsonapi calls any platform endpoint directly, with the profile's sign-in and organization handling, so it works under any profile. It defaults to GET and switches to POST when parameters or a body are present, unless a method is passed with -X. -f adds plain text parameters and -F adds typed ones, where a value starting with @ reads a file or stdin. On a GET they become query parameters, on a write they become the JSON body, and when --input sends a raw body from a file or stdin they move to the query instead. {org} in the path is filled in with the resolved organization. -i includes the status line and headers in the output. --silent prints nothing and leaves the exit code to say how it went. -H adds extra headers, except the ones the CLI manages itself, which are authorization, accept, content type and content length. Those fail the command instead of being overridden or silently dropped. Output and exit codes behave like every other command.
If an agent drives the CLI, give it the skill first. It tells the agent how to sign in and pick a profile and organization. It also says which commands work today.
npx skills add selfhost-dev/selfhost-cli --skill selfhostdevRun that in the repo where the agent works. The CLI still needs to be on PATH and signed in, so install the binary and run selfhost auth login first.
Apache 2.0. See LICENSE for the full text.