Anatomy of an app
The files of an app repo, what each one owns, and the small contract between an app and the host that runs it.
An app is one git repo. It stays small: the schema, the pages, the hooks, its own spec. Everything generic comes from locosdk.
The repo#
<app>/
CLAUDE.md two lines: the kit, then the app's own spec
agent/ the app's spec, read by every agent, wins over the kit
schema.ts collections, fields, rules: the single source of truth
server.ts createApp({ schema, app, pages }), hooks, routes, crons, then serve()
loco.config.ts the identity (name, color, logo, lang), review topics, deploy: false
CHANGELOG.md Unreleased, then v<n> sections: what the app's users read
src/ main.tsx, data.ts, collections/<name>/, pages
tests/
bunfig.toml the test preload: fast password hashing in tests only
data/ gitignored, your data only: app.db, uploaded files, backups
.loco/ ignores itself: the builders' workspace, review cards, shots
| file | owns | rule |
|---|---|---|
schema.ts | every collection, field, index, view, rule and auth option | a change here is applied at the next boot, see Schema as code |
server.ts | hooks, custom routes, crons, mail, MCP | plain TypeScript in the app's process |
loco.config.ts | the identity, review topics, deploy: false | plain data only: no import, no expression. It is read as text and rewritten in place by the portal |
CHANGELOG.md | what the users read in the what's new modal | one line per visible change, see Changelog and releases |
agent/ | the app's own spec: what it is for, its screens, its decisions | updated before the code it describes |
CLAUDE.md and the agent kit#
CLAUDE.md holds two lines:
@node_modules/locosdk/agent/index.md
@agent/index.md
The first line loads the agent kit that ships inside locosdk: conduct, code rules, and one page per sdk surface. The kit is versioned with the sdk, so the docs an agent reads always match the API it calls. The second line loads the app's own spec, which wins on conflict. More: The agent kit.
The data folder#
data/ is gitignored and holds your data, nothing else. The agents that change the app cannot read or write it: their sandbox closes the folder.
| path | what |
|---|---|
data/app.db | the SQLite database, WAL mode, PocketBase's layout |
data/storage/ | uploaded files, <collectionId>/<recordId>/<file>, unless the app stores files on S3 |
data/backups/ | backups on disk, unless the app keeps them on S3 |
.loco/ is the workspace of whoever builds the app, kept out of git by its own .gitignore:
| path | what |
|---|---|
.loco/review.db, .loco/review/<id>/ | review cards, their threads, their images and clips |
.loco/shots/ | shots of the app taken by golo shot |
The contract with the host#
The host that runs an app asks two things of it:
- Listen on the port in
PORT.app.serve()does it, and uses 8090 whenPORTis not set. - Answer
GET /api/healthwith 200.createAppmounts it.
Nothing else. In production an app binds the loopback interface only, and the server's front door passes it its own visitors. See Security.
One process#
- One Bun process serves the API, the pages (Bun's HTML imports, hot reload in dev) and the database.
- Hooks and crons run in that process as plain TypeScript. No VM, no sandbox, no sleep between requests.
- A restore of a backup ends the process on purpose. The host starts it again on the restored data, in about one second.
- A second
createAppon the same data folder (a hot reload) retires the first one: its crons stop and its database closes.
How an app gets locosdk#
The dependency is a snapshot of committed sdk code, installed as a normal package. Every agent run and every start of the app brings it up to the newest snapshot first, so a dev app always runs the current sdk.