Project structure
xeer new my-app writes eleven files. There is no hidden configuration and no generated code you are
not allowed to read.
This is the default notes template; --template todo, blog, and personal-site differ in their
manifest and source but have exactly this shape. personal-site declares no database, so it has no
capabilities and no database block — everything else on this page still applies.
my-app/
├── xeer.app.json the application manifest — the shape of your app
├── xeer.project.json the app's identity. commit this.
├── package.json
├── tsconfig.json
├── .gitignore
├── public/
│ └── favicon.svg anything here is served as a static asset
├── src/
│ ├── server.ts queries, mutations, endpoints
│ ├── client.tsx your UI
│ └── styles.css
└── tests/
└── notes.test.ts runs under `xeer test`#xeer.app.json — the manifest
The manifest declares everything about the app that is not code: its name, its entrypoints, its database tables and indexes, the capabilities it is allowed to use, and its resource budgets.
{
"$schema": "https://spec.xeer.dev/application-v0.schema.json",
"format": "xeer.application-source.v0",
"name": "my-app",
"entrypoints": { "client": "src/client.tsx", "server": "src/server.ts" },
"app": { "spa": true, "title": "My App", "language": "en", "favicon": "/favicon.svg" },
"database": {
"tables": {
"notes": {
"fields": {
"text": { "type": "string", "maxLength": 500 },
"ownerId": { "type": "string", "maxLength": 128 }
},
"indexes": { "by_owner": ["ownerId"] }
}
}
},
"capabilities": ["database"]
}This is not a config file you tweak occasionally — it is the contract the rest of the project is
generated from. Declare a table here and ctx.db.table('notes') becomes typed. Add a field and your
editor knows about it on the next save. Get it wrong and xeer check tells you exactly where.
Every field and every rule is in the manifest reference. Two things are worth knowing up front:
- Unknown keys are errors, everywhere. A typo in a field name is a failed build, not a silently ignored setting.
- The manifest is hashed into your build artifact's identity. Two identical manifests plus identical source produce the identical artifact id, which is what makes promote and rollback exact rather than approximate.
#xeer.project.json — the app's identity
{
"format": "xeer.project.v0",
"appId": "app_…"
}appId is the permanent identity of the deployed app: its server, its stored data, and its per-app
user ids all hang off it. It is not derived from your project name, so renaming the project or the
directory changes nothing.
Commit this file. It is what makes git clone followed by xeer deploy update the app you already
deployed instead of creating a new, empty one. The id is a random identifier rather than a secret and
grants nothing on its own — the platform binds each appId to the account that first deployed it and
refuses deployments from anyone else.
Use xeer link to change what a checkout points at:
xeer link # list the apps you own
xeer link --app app_… # point this directory at one of them
xeer link --new # deliberately fork: mint a new identityIt lives beside the manifest rather than inside it precisely because the manifest is hashed into artifact ids — pointing a checkout at a different app must not change your build hashes.
#src/server.ts — the server
A single default-exported defineServer({ … }) call. The compiler reads it statically, which is
why the operations you declare can be typed all the way through to the client.
import { defineServer, endpoint, mutation, query } from '@impetik/xeer/server';
export default defineServer({
queries: { /* read */ },
mutations: { /* write */ },
endpoints: { /* raw HTTP under /api */ },
authPolicies: { /* data-layer authorization */ },
});Full details in Server functions and the database.
#src/client.tsx — the client
A default-exported component. Xeer mounts it for you, wrapped in the provider that supplies auth and
the live-update stream, so there is no main.tsx and no createRoot call to write.
import { useMutation, useQuery } from '@impetik/xeer/client';
export default function App() {
const notes = useQuery('notes.list', {});
// …
}JSX is Preact, configured for you in tsconfig.json via "jsxImportSource": "@impetik/xeer". Full
details in The client.
#The two zones, and the wall between them
Your server code and your client code are compiled as separate graphs, and the compiler enforces what each may import.
| Zone | May import |
|---|---|
Server (src/server.ts and what it imports) | @impetik/xeer/server, @impetik/xeer/shared |
Client (src/client.tsx and what it imports) | @impetik/xeer/client, @impetik/xeer/shared, preact, preact/hooks |
A client module that imports @impetik/xeer/server fails the build with XE1202; one that imports the
server entrypoint fails with XE1201. This is not a lint rule you can turn off — it is the reason a
secret read from ctx.env cannot end up in a browser bundle by accident.
Code both zones need goes in a shared module, as examples/expenses and examples/team-board do with
src/shared/. Plain TypeScript with no platform imports is importable from either side.
#tests/*.test.ts
Anything matching tests/**/*.test.ts runs under xeer test, type-checked against
the same generated contract your server and client see.
#public/
Static files, served from the root of your app. public/favicon.svg is referenced as /favicon.svg in
the manifest's app.favicon.
#.xeer/ — disposable local state
Generated build output, the generated type declarations, cached local database state, and the plaintext
values xeer env pull writes. The scaffolded .gitignore excludes it, and it must stay excluded.
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.
xeer state reset clears the local database; xeer doctor reports on what is there.
#Bring your own tooling
package.json is an ordinary package.json. Add dependencies, add scripts, add a formatter, add a
linter. Xeer owns the compile-and-deploy path and the platform imports; it does not own your project.
{
"scripts": {
"dev": "xeer dev .",
"check": "xeer check .",
"build": "xeer build .",
"test": "xeer test ."
}
}