Appearance
Building Apps
This page describes how ERP Kit expects an app to be built, so that your team, your coding agent, and the ERP Kit skills follow the same conventions. The skills installed in your project under node_modules/@tailor-erp/kit/skills/ are the authority for your version.
The development loop
Two skills cover day-to-day work. Use them in any order, one story at a time:
erp-kit-app-implbuilds or extends a feature. It wires modules, writes resolvers and app-owned models, connects screens, and adds tests.erp-kit-app-docsrecords the feature as actor, business flow, story, screen, and resolver docs. Run it when the specification changes, not after every edit.
Each iteration runs these commands:
| Step | Directory | Command |
|---|---|---|
| Change models, modules, or resolvers, then regenerate types | apps/<app>/backend | pnpm run generate |
| Deploy to your development workspace (also publishes the frontend) | apps/<app>/backend | pnpm run deploy |
| Refresh the frontend GraphQL schema after an API change | apps/<app>/frontend | pnpm run generate |
| Typecheck and test | backend and frontend | pnpm run typecheck, then the tests in Testing |
Keep each run to one module or one story.
Where logic goes
| You need | Put it in |
|---|---|
| An operation a module already offers, such as creating a sales order | A resolver that calls the module command |
| Extra attributes on a module entity, such as a model code on an item | The module's custom fields, declared when the module is defined |
| Logic that combines several operations | An app resolver, with app-local functions where needed |
| Data that is yours alone, with its own lifecycle, such as a returns case | An app-owned table plus resolvers |
| A reaction to a data change | A module event handler or an app executor |
The kit relies on three rules:
- Write module data only through module commands. Never reimplement a state transition, a permission check, or an audit entry in the app.
- Use one transaction per resolver. Pass the same transaction to every command and app write that must succeed or fail together, and throw inside the transaction callback when a command returns
ok: false. - Map every error code. Map each error code a command can return to a
DomainErrorwith a message that is safe to show, and end theswitchwithdefault: throw result.error satisfies never, so that a new error code fails typecheck instead of slipping through.
The createRole resolver in the user-management template (backend/src/resolver/createRole.ts) shows this shape.
Composing modules
Modules are composed once in backend/src/modules.ts with each module's define...Module function, with dependencies passed explicitly. Their tables are re-exported from backend/src/modules-db.ts so the SDK discovers them. Resolvers import from @/modules, never from modules-db.ts.
Read a module's contract before you wire it. It is faster than letting the agent discover it from source:
bash
pnpm exec erp-kit doc module <module-name>
pnpm exec erp-kit doc module <module-name> command <command-name>App-owned data
- Use custom fields to add attributes to a module entity. They are declared on the module definition, and the module's commands still own the row.
- Use an app-owned table when the records are independent, several per parent, or have their own lifecycle. It lives in
backend/src/tailordb/<model>.ts, relates to module tables through the module object, is re-exported frommodules-db.ts, and declares its ownpermissionandgqlPermission.
Testing
| Layer | Checks | Location and command |
|---|---|---|
| Backend story | Business rules, public API, permissions, persisted result, rollback | backend/src/tests/story/<flow>/<actor>--<story>.test.ts. Runs against the deployed development backend: pnpm test:integration in the backend |
| Frontend component | Rendering, input, validation, actions, failure recovery | <name>.test.tsx next to the component: pnpm test in the frontend |
| Browser E2E | The main path of a flow through the screens | frontend/e2e/tests/<flow>.spec.ts: pnpm test:e2e in the frontend |
- Query after a mutation to check what was persisted. Do not trust the mutation's response alone.
- Create test data inside each test with a unique suffix. Use seeds only for master data and sign-in users.
- Test permissions with a token that has the actor's permissions. A test that passes with an admin machine user proves nothing about ordinary users.
When story and resolver docs exist, erp-kit app generate code scaffolds the missing resolvers and story tests; you fill in the logic and assertions. Before calling a story done, check that docs, code, and tests still agree:
bash
pnpm exec erp-kit app check --path apps/<app>
pnpm exec erp-kit app sync-check --path apps/<app>Security
The templates are set up for a first deploy by one developer. Before anyone else signs in, go through Security and sign-in.
Review checklist
Ask these at the end of every story:
- Which ERP Kit modules are wired, and is each defined once in
modules.ts? - Is any module table written outside a module command?
- Does every resolver map the full error union and throw inside its transaction?
- Is app-only data a custom field or an app-owned table, and is that the right choice?
- Which story tests exist, and do they run against the deployed development backend?