= The ulabase command line :nav-title: ulabase CLI :description: ulabase configures a service from a file in git, the same way from a terminal or a CI pipeline, so every service and environment gets the same setup. :keywords: CLI, infrastructure as code, configure backend from git, CI pipeline, ulabase setup, deploy backend configuration :group: Tools :order: 10 `ulabase` is a command line tool. It configures a service from a setup file in your repository: collections, indexes, permissions, features and their settings. Use it when you need the same configuration more than once: a second service, a production environment, a colleague's copy. It applies the file the same way from a terminal or a CI pipeline. What you get: * **The configuration in git.** It sits in the same commit as the code it configures, and you see its diff when it changes. * **Safe to run again.** A second run changes only what is not already in place, and `--dry-run` shows what is missing before anything changes. [source,bash] ---- npm install -g ulabase ulabase login ulabase setup --srv c0ffee --dry-run ulabase setup --srv c0ffee ---- == When to use a setup file The console is the tool for looking around and for one-off changes. A setup file is for configuration you need again: a second service, a new environment, a colleague's copy. A service's configuration is a handful of settings that must line up: an ACL that lets guests read a catalog, a feature's success URL that matches a route in the app, a collection name the client also knows. In a setup file you can run it, run it again, see its diff when it changes, and keep it in the same commit as the code it configures. == Logging in The credential is a xref:tokens.adoc[personal access token]. Issue one at https://ulabase.com/me/tokens. [source,bash] ---- ulabase login # prompts for the token; stored 0600 under ~/.config/ulabase ulabase logout # forgets it here; revoke it in the console to disable it everywhere ---- `ulabase` accepts no password, by design: an OAuth user has none, and an account password reaches far more than a pipeline needs. See xref:tokens.adoc#personal-access-tokens[personal access tokens]. `ulabase login` checks the token with Ulabase before storing it, so a wrong token fails at login, before any other command. == Commands and options [cols="1,3"] |=== | Command | What it does | `ulabase login` | Store a personal access token for later commands. | `ulabase logout` | Forget the stored token. | `ulabase setup` | Bring a service to the state the setup file describes. |=== [cols="1,3"] |=== | Option | Meaning | `--srv ` | The service to set up. Required. | `--file ` | A module that exports a setup, as the default export or as `setup`. A function export is called with no arguments. Defaults to `./ulabase.setup.ts`. | `--dry-run` | Run every check, apply nothing, write nothing. | `--force ` | Apply the steps whose name contains `` without running their check first. Repeatable. See <>. | `--json` | Print the report as JSON, for a pipeline that reads it. | `--api ` | The Ulabase API address. You will not need it. | `--version`, `-v` | Print the version and exit. | `--help`, `-h` | Print this summary. |=== == The setup file [[the-setup-file]] `ulabase setup` looks for `./ulabase.setup.ts` in the working directory, or the path given by `--file`. [source,typescript] ---- import { defineSetup, step } from '@ulabase/cli'; export default defineSetup('Notes app', [ step('notes collection', { check: ({ service }) => service.collectionExists('notes'), apply: ({ service }) => service.createCollection('notes'), }), step('authors may read their own notes', { check: ({ service }) => service.permissionExists('authorReadsOwnNotes'), apply: ({ service }) => service.putPermission('authorReadsOwnNotes', { predicate: "path-prefix('/notes') and method(GET)", roles: ['author'], priority: 10, }), }), ]); ---- The file imports the package, so the project needs its own copy alongside the global command: [source,bash] ---- npm i -D @ulabase/cli ---- The global install gives you the `ulabase` command for every project. The local one is where the setup file's imports resolve, since Node does not look in the global install. === Every step is a check and an apply `check` answers "is this already so?" without changing anything. `apply` runs only when the answer is no, and then the check runs again to confirm the apply worked. So a run is safe to repeat. On a service already configured, `ulabase setup` writes nothing and reports every step satisfied. You can use it as a deploy gate as well as a deploy step. `--dry-run` runs the checks and stops. It changes nothing and tells you what the service is missing. === Forcing a step [[forcing-a-step]] A check usually answers "does this exist?". If you edit a permission's predicate under the same id, or rewrite a schema under the same name, the check still says yes: the run reports the step satisfied and your change never reaches the service. `--force` names the steps to apply without running their check first: [source,bash] ---- ulabase setup --srv c0ffee --force 'consents gate' ulabase setup --srv c0ffee --force catalog --force orders ---- The match is case-insensitive and on a substring, so part of the step's name is enough. The check after the apply still runs, and a forced step that did not work is reported failed like any other. CAUTION: `--force` with no name forces every step. Avoid it: an apply written to run once may fail the second time, and a step that seeds sample data seeds it again over what is there now. When a check keeps missing a change, make the check compare the stored content, such as the predicate as well as the id. Until then, use `--force` on that step. === Secrets come from the environment [source,typescript] ---- import { fromEnv } from '@ulabase/cli'; 'secret-key': fromEnv('STRIPE_SECRET_KEY'), ---- `fromEnv` puts the *name* of a variable in the setup file and reads its value at apply time. The server returns a stored secret masked, so a step that keeps what is already configured needs no secret in the environment. A pipeline can re-run a setup without every key it once needed. When variables are missing, the run fails and names all of them at once. === What a step can reach `check` and `apply` receive `service` and `admin`. `service` calls your service: collections, indexes, permissions, users, schemas. It authenticates with a token `ulabase` issues for itself, so you never handle one. `admin` calls Ulabase: it installs features, reads and writes their configuration, and runs their initialisers. == From a pipeline Set `ULABASE_TOKEN` from your platform's secret store, with no `ulabase login` step. The variable **always takes precedence over a stored session**, so a CI run never falls back to a session left on a shared runner. [source,yaml] ---- # .github/workflows/deploy.yml - run: npx ulabase setup --srv c0ffee env: ULABASE_TOKEN: ${{ secrets.ULABASE_TOKEN }} STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }} ---- There is no flag for the token: a credential in a flag ends up in the shell history and in the process list of every user on the machine. === Exit codes [cols="1,4"] |=== | Code | Meaning | `0` | Every step satisfied or applied | `1` | A step failed | `2` | A dry run found steps to apply: the service differs from the file |=== With exit code `2`, a pipeline can fail on drift with `--dry-run`, without applying anything. == Creating services `ulabase setup` cannot create a service, and no setup file can. A setup runs again on every merge: a step that created a service would create one per merge, and a paid one would start a purchase per merge. The server enforces this: a token carries the `cli` role, which cannot start a Stripe Checkout session. See xref:tokens.adoc#it-carries-the-cli-role-not-yours[the `cli` role]. == Troubleshooting **`Cannot find package '@ulabase/cli' imported from …`.** The setup file resolves its imports from its own directory, where the global install is not visible. Run `npm i -D @ulabase/cli` in the project of the setup file. **A relative import in the setup file does not resolve.** Node loads it as an ES module and does not add extensions: write `./config.ts` in full. **`Unknown file extension ".ts"`.** Node 22.18 and later run TypeScript files directly. With an earlier Node, use `npx tsx`. == Related pages * xref:kit.adoc[The Ulabase Kit]: the client code that talks to the service `ulabase` configures * xref:tokens.adoc[Tokens]: the credential `ulabase` authenticates with * https://github.com/ulabase/starter-react[the starters]: each has a `ulabase.setup.ts` you can read