Ulabase

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

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.

Option Meaning

--srv <id>

The service to set up. Required.

--file <path>

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 <name>

Apply the steps whose name contains <name> without running their check first. Repeatable. See below.

--json

Print the report as JSON, for a pipeline that reads it.

--api <url>

The Ulabase API address. You will not need it.

--version, -v

Print the version and exit.

--help, -h

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

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 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.

  • The Ulabase Kit: the client code that talks to the service ulabase configures

  • Tokens: the credential ulabase authenticates with

  • the starters: each has a ulabase.setup.ts you can read