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. 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, and guest are three 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.
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=/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 and live updates actually happen.
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 clear # back to alice, no membershipThis 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.
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.
#Workspace membership locally
A local persona carries the same workspaceIds a deployed assertion does, so workspaceTable()
policies and memberOf() guards decide locally exactly as they will in production. 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).
One rule worth internalising: a persona holding membership is asserted as kind: 'user'; one holding
none is a guest. A deployed app only ever gives membership to a provisioned identity, so this keeps
both authenticated() and memberOf() honest locally. Membership never changes appUserId, so joining
a workspace does not orphan rows a persona already owns.
guest is a third identity the sign-in route deliberately will not mint, and it can never be given
membership. It exists so a test can prove an unclaimed visitor sees nothing Alice or Bob 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.
#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.