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 stagingdeletes on staging, and the banner says▸ todoapp/stagingbefore it does — andpalbase 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.
| Flag | Default | Meaning |
|---|---|---|
--count N | 1 | How 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. |
--json | false | Emit 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:
accessTokenis 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 withaccess_token_expiredand tells you to re-mint.palbase testdoes 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 createtakes no argument and mints a user rather than defining a template. The group's own--helpis 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 whenconfig/stopped being evaluated. Treat templates as something you write through the management API, or do without them:clonegives 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)
| Flag | Meaning |
|---|---|
--email / --password | Fixed credentials for the copy. Give both or neither — with neither, the project generates a pair. |
--set table.column=value | Change a column on every copied row of a table. Repeatable. |
--json | Emit 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:
| Status | What it means |
|---|---|
409 | The same fixture, applied twice — that account already exists. |
429 | This project's test-user cap is reached. A refusal, not a failure: the answer names the cap, so purge some and retry. |
503 | The 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.
Related
- 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 startbrings 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