Tools
The ulabase command line
Configure a service from a file in git: collections, indexes, permissions, features and settings, applied the same way from a terminal or a CI pipeline.
ulabase configures a service from a file committed to git: collections, indexes, permissions, features and their settings. It applies the file the same way from a terminal or a CI pipeline.
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 personal access token. Issue one at https://ulabase.com/me/tokens.
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 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
| Command | What it does |
|---|---|
|
Store a personal access token for later commands. |
|
Forget the stored token. |
|
Bring a service to the state the setup file describes. |
| Option | Meaning |
|---|---|
|
The service to set up. Required. |
|
A module that exports a setup, as the default export or as |
|
Run every check, apply nothing, write nothing. |
|
Apply the steps whose name contains |
|
Print the report as JSON, for a pipeline that reads it. |
|
The Ulabase API address. You will not need it. |
|
Print the version and exit. |
|
Print this summary. |
The setup file
ulabase setup looks for ./ulabase.setup.ts in the working directory, or the path given by --file.
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:
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
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:
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
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.
# .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
| Code | Meaning |
|---|---|
|
Every step satisfied or applied |
|
A step failed |
|
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 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
-
The Ulabase Kit: the client code that talks to the service
ulabaseconfigures -
Tokens: the credential
ulabaseauthenticates with -
the starters: each has a
ulabase.setup.tsyou can read