Model an application by decomposing user requirements into stories, entities, documents, methods, and change targets using the Easy CLI. Use when the user wants to design or model a new app, add features to a model, or work with the model database.
Decompose application requirements into Simple concepts using the bun model CLI. The modeler stores the design in SQLite and generates diagrams and specs.
bun modelresolves todocker compose exec easy bun modelvia themodelscript in package.json — it runs inside the Easy container.
Important:
bun model save <schema> '<json>'. Use the exact syntax from reference.md.bun model call so you can see errors immediately and fix them before proceeding. Only use bun model batch for large bulk imports where you've already verified the syntax.Check that the Easy container is running:
docker compose ps --format '{{.Service}} {{.State}}' | grep -E '^(easy|postgres) '
If not running, tell the user:
The Easy container is not running. Start it with:
bun run up
Then try again.
Do not proceed with modeling until Easy is running.
Simple is a minimal full-stack pattern: postgres owns everything, the server relays, the client merges. Auth, business logic, and permissions all live in SQL. The server is a thin WebSocket relay that prepends user_id to every call. The client opens documents, receives live updates via pg_notify, and merges deltas into reactive signals.
Created with bun create blueshed/simple <app-name>.
| Concept | What it is | How it works in Simple |
|---|---|---|
| Entity | A postgres table with typed fields | CREATE TABLE room (id SERIAL PRIMARY KEY, name TEXT, ...) |
| Document | A composed JSON shape the client subscribes to — one per screen | Client calls openDoc("room_doc", roomId), server calls room_doc(user_id, room_id) |
| Expansion | Related entity loaded within a document (has-many, belongs-to, nestable) | SQL subqueries/joins in the doc function: jsonb_agg(...) for has-many, jsonb_build_object(...) for belongs-to |
| Method | A mutation the user can perform on an entity | Postgres function: save_room(p_user_id, ...) or remove_room(p_user_id, ...) |
| Publish | Fields changed by a method — tells /implement what the mutation's pg_notify payload should carry | Method publishes name → save function notifies with the updated name field |
| Auth | Token-based WebSocket auth, public vs protected documents | Pre-auth via POST /auth (login/register), then WS /ws?token=... for everything else |
| Story | User requirement that decomposes into the above | "As a member, I can send a message" |
NOT modeled: client-side components, CSS, signals code, SQL queries. Those are generated by the /implement skill from the exported spec.
bun model ... → bun model export > spec.md → /implement
spec.md — a framework-neutral description of entities, documents, methods, and change targets/implement skill reads spec.md and generates: SQL schema, doc functions, mutation functions with pg_notify, web components with openDoc/closeDoc, routingAlways work top-down in this order:
pg_notify payloadbun model export to generate the specThe model database has a key-value metadata store for application-level information. Use it to capture anything that describes the app being built — theme, project name, target audience, etc.
bun model save metadata '{"key":"theme","value":"60s flower power — warm oranges, earthy browns, groovy rounded shapes"}'
bun model save metadata '{"key":"name","value":"My Chat App"}'
bun model list metadata # list all
bun model delete metadata '{"key":"name"}' # remove a key
All metadata is included in export output as a ## Metadata section. The /implement skill reads the theme value (if present) to guide CSS generation. The Easy website displays all metadata on the Stories page.
Simple's auth system provides a user table (id, name, email). In the model, represent this as an Account entity — you must add it before referencing it in relations or expansions:
bun model save entity '{"name":"Account","fields":[{"name":"id","type":"number"},{"name":"name","type":"string"},{"name":"email","type":"string"}]}'
The /implement skill maps Account to the existing user table — it does not create a separate table.
@author_id)When a method changes fields on an entity, those fields are "published" in the model. This tells /implement what the mutation function's pg_notify payload should carry — the data field includes the changed row, and the targets array tells the server which clients to notify.
Rule of thumb: publish captures what changes. Change targets capture where the change appears in document trees.
openDoc(fn, id) call and a postgres doc functionPermission paths use a DSL to express who can call a method. A path resolves to a set of user IDs — if the current user is in that set, access is granted. Multiple paths on the same method use OR logic.
In Simple, these translate to SQL permission checks in mutation functions (e.g. IF p_user_id != v_author_id THEN RAISE EXCEPTION 'permission denied').
@field->table[filter]{temporal}.target_field
| Component | Syntax | Meaning |
|---|---|---|
| Start field | @field | Begin from a column on the entity being acted on |
| Traverse | ->table | Follow a foreign key to a related table |
| Filter (literal) | [field='value'] | WHERE clause with a literal value |
| Filter (current user) | [user_id=$] | WHERE clause matching the authenticated user's ID |
| Filter (multi) | [org_id=$,role='admin'] | Multiple conditions (AND) |
| Temporal | {active} | Only rows where valid_from <= NOW() AND (valid_to IS NULL OR valid_to > NOW()) |
| Project | .target_field | Extract this column as the resolved user ID |
Direct ownership — entity has a user_id column:
@user_id
In SQL: IF p_user_id != v_row.user_id THEN RAISE EXCEPTION 'permission denied'
Organisation membership — user acts for the org that owns this entity:
@owner_id->acts_for[org_id=$]{active}.user_id
Reads: "Take the entity's owner_id, look up acts_for rows where org_id matches and the row is active, return user_id — if the current user is in that set, allow."
Multi-hop — traverse through an intermediate entity:
@venue_id->venues.org_id->acts_for[org_id=$]{active}.user_id
Role-restricted — only admins of the org:
@owner_id->acts_for[org_id=$,role='admin']{active}.user_id
bun model save permission '{"method":"Organisation.createVenue","path":"@owner_id->acts_for[org_id=$]{active}.user_id","description":"User must be active member of the organisation"}'
Methods already capture permissions and publish as the single source of truth. Checklists are integration test scenarios — they should NOT restate what methods already describe.
DO use checklists for:
AVOID in checklists:
When in doubt, ask: "Is this already expressed on a method?" If yes, don't add a checklist check for it.
For full CLI reference and detailed examples, see reference.md.