---
url: https://docs.tailor.tech/erp-kit/develop/building-apps.md
description: >-
  The development loop for an ERP Kit app, where each kind of logic belongs, how
  to test it, and the security settings to change before go-live.
---

# 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-impl`** builds or extends a feature. It wires modules, writes resolvers and app-owned models, connects screens, and adds tests.
* **`erp-kit-app-docs`** records 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](#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](/guides/executor/overview) |

The kit relies on three rules:

1. **Write module data only through module commands.** Never reimplement a state transition, a permission check, or an audit entry in the app.
2. **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`.
3. **Map every error code.** Map each error code a command can return to a `DomainError` with a message that is safe to show, and end the `switch` with `default: 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 from `modules-db.ts`, and declares its own `permission` and `gqlPermission`.

## 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](/erp-kit/develop/security).

## 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?
