Palbase
Sign inGet started

CLI

Test Users

palbase test-user mints, lists, copies and purges disposable is_test accounts on the environment this checkout resolves to — accounts you can sign into your app as, with real credentials and, when you clone or seed from a template, a real data tree behind them. Five verbs cover the whole surface: create, list, templates, clone and delete. Every one of them is a thin client over the project's own Management API at https://<ref>.palbase.studio/v1/management/test-users*, so a script holding the service-role key sees exactly the same accounts these commands do. Nothing secret is generated on your machine: the project mints the passwords and the access tokens, the CLI prints them once, and there is no way to read a password back afterwards.

Quick example

palbase test-user create
# ▸ todoapp/main
# ✓ created 1 test user(s)
#   id:       usr_123
#   email:    test-9f3c1a2b@test.invalid
#   password: a7f2c1d80b4e
#   token:    eyJhbGciOi…
#   (creds shown once — store them now)
palbase test-user list       # this project's test users
palbase test-user delete usr_123

Where it acts

The directory decides, exactly the way it does for push, plan and status: palbase test-user acts on the environment this checkout resolves to — one environment of the project palbase/project.json names — or on the local stack while palbase start is running and the machine-local record it writes exists. Every verb prints that destination to stderr first, so a scripted run still pipes cleanly.

There is no second implementation for a stack on your machine. A local stack and a cloud project answer the same five routes, so an account minted here and an account minted there are the same kind of thing.

Note: Test users belong to one environment. --env <name> acts on another environment of the same project for one command — palbase test-user delete usr_x --env staging deletes on staging, and the banner says ▸ todoapp/staging before it does — and palbase env use <name> remembers one on this machine. See Which environment a command acts on.

To act on a different project, link the directory to it — see Linking a Checkout.

Note: Each Environment verifies tokens against its own auth, so a token minted here is only valid on the Environment that minted it. Copying one into a request against another Environment gets a 401, not a wrong-account bug.

create

Mints test users and prints their credentials once.

FlagDefaultMeaning
--count N1How many users to mint. Below 1 the CLI refuses locally: --count must be at least 1.
--template <name>Seed each minted user's data tree from a template stored on this Environment. Combines with --count; every instance gets its own copy of the rows.
--jsonfalseEmit the identities as JSON, in the shape the test harness accepts (see below).

create takes no positional argument. It mints; it does not name.

With --template, the per-table row counts come back beside the login, because what you want to know is whether the user you just minted can sign in to a working app:

palbase test-user create --template banking
# ✓ created 1 test user(s) from template "banking"
#   id:       usr_789
#   email:    test-4c8e11d9@test.invalid
#   password: 3b91d0aa7c65
#   token:    eyJhbGciOi…
#   rows:     2 accounts, 1 profiles, 2 transactions
#   (creds shown once — store them now)

The counts include nested rows: a template that puts two transactions inside an account reports the account and the transactions.

--json, and what it is shaped for

The payload is what createTestApi({ identities }) takes, so it goes straight into a test run with nothing in between:

{
  "identities": {
    "user1": { "id": "usr_…", "email": "…", "password": "…", "accessToken": "eyJ…" },
    "user2": { "id": "usr_…", "email": "…", "password": "…", "accessToken": "eyJ…" }
  }
}

Keys are the template's names when the mint came from a template, and positional (user1, user2, …) when it did not. The destination line goes to stderr, so palbase test-user create --count 2 --json > identities.json writes only the payload.

Warning: accessToken is a session, and a session expires — roughly half an hour. A file of identities is good for the run that made it, not for tomorrow: the harness answers a stale one with access_token_expired and tells you to re-mint. palbase test does that for you.

templates

Lists the fixture-account templates this Environment holds.

palbase test-user templates
# NAME        FIXTURE EMAIL     SEEDS
# demo        demo@test.local   lists, todos
# heavy_user  -                 lists

A FIXTURE EMAIL means the stack already holds an account with those credentials — sign into your app with it directly. A - means the row is a template you stamp out on demand with create --template.

Templates are stack state, and nothing in your repository ships them. Nothing reads a config/test-users.ts file: no build evaluates it, no artifact carries it, and a leftover copy on disk is ignored; palbase push carries code and schema and nothing else. The only door that writes templates is PUT /v1/management/test-users/templates on the project's own Management API, which replaces the whole set — and no CLI verb calls it. So a project created on this platform holds no templates until something writes them through that route, and palbase test-user templates prints:

No fixture accounts on this stack. Add one: palbase test-user create <name>

Warning: That hint is a stale string in the binary, and following it does not work: palbase test-user create takes no argument and mints a user rather than defining a template. The group's own --help is stale in the same way — it says templates "take effect on deploy" and that "git is the source of truth for them", which stopped being true when config/ stopped being evaluated. Treat templates as something you write through the management API, or do without them: clone gives you a seeded account from an account you already have.

list

The Environment's current test users.

palbase test-user list
# ID       EMAIL                       VERIFIED
# usr_123  test-9f3c1a2b@test.invalid  no
# usr_456  demo@test.local             yes

With none, it says so rather than printing an empty table: No test users in this project. --json emits the same rows for scripting.

clone

Copies a test user's whole data tree onto a newly minted account.

palbase test-user clone usr_123 \
  --email second@test.local --password second-password-1 \
  --set profiles.display_name="Second User"
# ✓ cloned usr_123
#   id:       usr_901
#   email:    second@test.local
#   password: second-password-1
#   token:    eyJhbGciOi…
#   rows:     1 profiles, 4 todos
#   (creds shown once — store them now)
FlagMeaning
--email / --passwordFixed credentials for the copy. Give both or neither — with neither, the project generates a pair.
--set table.column=valueChange a column on every copied row of a table. Repeatable.
--jsonEmit the credentials and token as JSON.

Half a pair is refused before any request leaves: --email and --password must be given together, or neither. The named pair itself reaches the project — it used to be refused against a stack, and no longer is, so a fixture whose login is written into the test that uses it is minted the same way everywhere.

Rows are re-keyed, not duplicated verbatim: the copy gets its own primary keys, the new user owns them, and foreign keys within the copied tree are remapped to the copies. A reference pointing outside the tree is carried over untouched. The source must itself be a test user — cloning a real account would copy real personal data into a throwaway one.

--set values are parsed the way the panel's row editor parses them: null becomes JSON null, anything that is valid JSON is decoded so numbers and booleans arrive typed, and everything else stays a literal string.

--set profiles.age=41            # the number 41
--set profiles.active=true       # the boolean true
--set lists.archived_at=null     # JSON null
--set profiles.bio="hello there" # the string

Column names are validated server-side against the live schema, never here — a typo comes back from the project, not from the CLI. Two malformed-input shapes are caught locally:

--set "profiles" must be table.column=value
--set "profiles=x" must name a table and a column: table.column=value

delete

Purges a test user and everything that belongs to them.

palbase test-user delete usr_123
# ✓ deleted usr_123

This cascades through the account's rows, and it is not reversible.

When the project refuses

Anything that is not a success is reported with the project's own words rather than a bare status, in the shape <target> answered <status>: <body>. Three refusals are worth knowing by name:

StatusWhat it means
409The same fixture, applied twice — that account already exists.
429This project's test-user cap is reached. A refusal, not a failure: the answer names the cap, so purge some and retry.
503The stack refused the work on budget grounds.

About billing

The minted users are marked is_test on the project, and both the @palbase/backend type declarations you install and the auth module's own admin OpenAPI say an is_test user is excluded from MAU/billing.

The measurement does not implement that. The MAU figure the platform actually collects from a project is select count(*) from auth.users where last_login_at > …, with no is_test filter — so a test user that has signed in inside the window counts like any other account. Until the two agree, treat a fleet of test users as something that counts, and purge the ones you are done with.

  • Test Users — what a test user is on the backend, and the seed data behind a template
  • Linking a Checkout — the file that decides which Environment these commands act on
  • Running It Locally — the stack palbase start brings up, and the same five verbs against it
  • Stack Settings — the other settings that live on the stack rather than in your repository
  • Authentication — how a minted token is verified on a request
  • CLI: Overview — the whole command surface these verbs belong to