Local development
xeer devOne command, and you have your client, your server, your database, and your object store running together — with a real verified identity for the caller. No account, no network, no containers, no credentials for anything the framework provides.
#What xeer dev gives you
- Your client and server, together. Client edits hot-reload. Server edits recompile and restart behind a health check, with an automatic rollback if the new version does not come up — so a typo in a handler does not leave you with a dead dev server.
- A real database. Not a mock, not an in-memory shim: the same transactional engine your deployed app uses, with the schema from your manifest. Change a table and the local schema follows.
- A real object store, if you declared the
storagecapability. It exists on the firstput, andxeer dev,xeer preview, andxeer testeach get their own. - Live updates, once your manifest declares
budgets.liveConnections. Open two browser windows and watch a write in one appear in the other. - An identity for every request, without sign-in. See local personas below.
The URL is printed on startup. Xeer picks a free port unless you pass --port.
xeer dev --port 5173 # pin it
xeer dev --json # newline-delimited events, for an agent#xeer check — the fast loop
xeer checkValidates the manifest and analyses both source graphs without running anything: entrypoints resolve, the server export is statically readable, no client module imports server code, routes do not collide. Every failure carries a file, a line, and a suggested repair. This is the command to run on every edit; see the diagnostics reference.
xeer doctor is its counterpart for the machine rather than the code: Node version, platform
support, whether the build dependencies resolve, local state. Run it when something works for a colleague
and not for you.
#Local personas
A deployed Xeer app gives every visitor a verified identity of its own — see
Auth. Locally there is no identity service to talk to, so
xeer dev, xeer preview, and xeer test mint deterministic local personas instead.
alice, bob, carol, and guest are four distinct application users with stable ids. That is what
makes ownership and authorization something you can exercise on your laptop instead of hoping about in
production. Three real users, so a suite can express author, co-member, and outsider as three
identities rather than making one of them wear two hats.
appUserId under `xeer dev`: local:alice:1f0qk3
appUserId once deployed: xu_… (opaque, and different in every app)The shapes differ visibly on purpose: a local id is never mistakable for a deployed one.
#Switching persona: two controls
Mistaking the first for the second is the usual reason a persona switch appears to do nothing.
1. The live switch — the local sign-in route. Takes effect immediately, and survives reloads:
http://127.0.0.1:5173/_xeer/auth/sign-in?persona=alice&returnTo=/
http://127.0.0.1:5173/_xeer/auth/sign-in?persona=bob&workspace=design&returnTo=/
http://127.0.0.1:5173/_xeer/auth/sign-in?persona=carol&role=admin&permission=notes:write&returnTo=/Paste it into the browser, or call it from your own UI:
import { signIn, signOut, useAuth } from '@impetik/xeer/client';
const { refresh } = useAuth();
signIn({ persona: 'bob' });
signIn({ persona: 'bob', workspaceIds: ['design'] });
void signOut().then(() => refresh());It sets an httpOnly cookie and redirects back. Because the selection is a cookie, two browser
profiles — or one normal and one private window — hold two personas against the same dev server at
once. That is the quickest way to watch per-user isolation happen — and live
updates too, once your manifest declares budgets.liveConnections.
2. The startup default — xeer auth as.
xeer auth as bob # writes .xeer/auth.local.json
xeer auth as alice --workspace design # with workspace membership
xeer auth as alice --workspace design,ops
xeer auth as carol --role admin --permission notes:write
xeer auth clear # back to alice, holding nothingThis value is read when the dev server starts, and again when a server-code change restarts it. It is a
default only: it never reaches a tab that has already chosen a persona, the cookie always wins, and
xeer test ignores the file entirely because tests name their persona per call. Signing out clears the
cookie, so the session falls back to this default. A plain signIn() or SignInButton deliberately
restores this complete default into the tab; an explicit persona clears membership unless workspace ids
are passed with it.
xeer auth as has nothing to do with xeer auth login.
login signs you in so you can deploy; as picks which pretend
app user your local browser is. Two different doors —
see Auth.
#Membership, roles, and permissions locally
A local persona carries the same workspaceIds, roles, and permissions a deployed assertion does,
so workspaceTable() policies and memberOf(), hasRole(), and hasPermission() guards decide
locally exactly as they will in production. Workspace ids are the labels you declare — no derivation,
so the same label means the same workspace on every machine — validated against the production charset
(A-Za-z0-9:_-, 1–96 characters, at most 16 per persona). Roles and permissions use the wider
assertion charset (A-Za-z0-9._:@/-, 1–128 characters, at most 16 each), so notes:write and
org/admin are spelled as you would spell them in production.
One rule worth internalising: a persona holding a workspace, a role, or a permission is asserted as
kind: 'user'; one holding none is a guest. A deployed app only ever grants any of the three to a
provisioned identity, so this keeps authenticated(), memberOf(), hasRole(), and hasPermission()
all honest locally. None of them changes appUserId, so joining a workspace or gaining a role does not
orphan rows a persona already owns.
Each of the three travels in its own httpOnly cookie — xeer_local_persona,
xeer_local_workspaces, xeer_local_roles, xeer_local_permissions — and each fails closed on its
own: a cookie that is absent, empty, or malformed grants nothing, so hand-editing one can only ever
narrow what a guard lets through.
guest is a fourth identity the sign-in route deliberately will not mint, and it can never be given
membership, a role, or a permission. It exists so a test can prove an unclaimed visitor sees nothing
alice, bob, or carol owns — a fail-closed control that cannot be accidentally upgraded.
#Personas are development-only, by construction
The local identity adapter refuses any non-loopback origin, the local sign-in and sign-out routes exist
only in the development identity mode, and a deployed app requires a cryptographically verified identity
assertion for every single request. There is no flag that turns personas on in production, and
signIn({ persona }) is ignored there.
#Your local data
Everything lives under a project-local .xeer/, which the scaffolded .gitignore excludes and which
must stay excluded — it holds the plaintext values xeer env pull writes.
Each mode has its own state, and they never mix:
| Mode | Persists across restarts? |
|---|---|
xeer dev | yes |
xeer preview | yes, separately from dev |
xeer test | no — fresh and discarded every run |
xeer export --state dev . --out dev-backup.json # database records, as readable JSON
xeer state reset . --state dev --confirm my-app # start clean (prints a recovery path)
xeer import --state dev . dev-backup.json # put it backThat export-reset-import loop is what makes an incompatible schema change survivable: take the
export first, then change the manifest. import validates the document against the schema currently in
force and accepts it only when that schema is the export's own or a compatible successor, so it can
never write rows your queries are not allowed to assume.
Two things to know: xeer state reset clears stored objects along with the database for that mode, and
xeer export carries database records only — stored objects are not in the document.
Nothing in .xeer/ is required to redeploy. Your app's identity is in the checked-in
xeer.project.json, so deleting the
directory, or cloning a repository without it, is safe.
#Browsing and editing rows
xeer dev and xeer preview serve a data editor at /__admin/ on the same port:
http://127.0.0.1:5173/__admin/Browse tables, page through rows, and edit values. It reads the declared schema, so a column's editor matches its field type and the table's constraints apply to what you save.
For the same data from a terminal, use xeer db.
The editor authenticates with a secret minted per run. The dev server injects it into the page and
records it in the local state lease so sibling xeer db commands can use the same protected routes.
The lease lives under .xeer/ and the server removes it during a clean shutdown. Therefore:
- Restarting the server invalidates the old secret.
- Keep the server on loopback while using the editor. A visitor who can reach a server exposed with
--hostcan load/__admin/and receive the secret from the page. The secret blocks blind requests, but it does not distinguish the builder from another network visitor. - There is no deployed equivalent. A deployed application returns 404 for the admin routes and never admits the surface exists.
#Running the real artifact
xeer build # produce a content-addressed artifact
xeer preview # run that exact artifactxeer preview verifies and runs the built artifact, reading only the artifact's own contents and never
your source. It is the closest local approximation of production, and distinct from xeer dev, which
recompiles from source on every change. Use it when you want to be sure what you are about to deploy
actually works.