# Developing a module on DODIL — the lifecycle

For the agent (or person) building business software for someone. Read it once before the
first conversation; come back to a phase when you reach it.

**The rule that shapes everything else: phases 0–3 produce documents, not code, and the
customer signs off on them before phase 4.** The reference modules exist so you never start
from a blank page — not so you can skip understanding this customer.

```
0 FRAME       what are they asking for? is there already a system here?
1 DISCOVER    the module catalog -> the reference module's spec
2 UNDERSTAND  actors, stories, workflows, entities, master data, rules
3 DESIGN      map it onto the platform -> ERP_<MODULE>.md -> sign-off
4 BUILD       copy the platform layer, generate the business layer
5 TEST        a test per story, per role, per invariant
6 PROVISION   dev before prod; merge role catalogs
7 DEPLOY      git push -> CI -> registry -> CD
8 VERIFY      the app reaches its own data; a real person signs in
9 OPERATE     change -> back to 2; next module -> back to 0
```

**Modular in design, incremental in delivery, shared in data, one app by default.**

### Four rules that hold in every phase

1. **Interactive by default.** Every decision the customer can make is a short question with
   choices; every change you make is shown before and after. Work silently only on what was
   already agreed.
2. **The repo remembers, not you.** State lives in `ERP_<MODULE>.md` (status, people, decisions,
   changes), not in your memory or the chat. Another agent — or another person — must be able to
   pick up from the file alone. See *Starting a session*.
3. **Test copy first, live only on an explicit yes.** The customer usually cannot tell which
   database they are changing. You always can, and you always say. See *Environments*.
4. **Show it on localhost.** Whenever something can be seen, run it locally against the test copy
   and let the customer look before anything goes live.

### Starting a session

Before anything else, every session:

1. Reads every `ERP_*.md` in the repo — the **Status** block first, then **People**, **Change
   requests**, and **Decisions**.
2. Works out who it is talking to. If it is not obvious, ask one light question: *"Before we
   continue — who am I speaking with, and what's your part in the business?"*
3. Brings the file up to the current template (`/modules/template.md`) if it is older: adds any
   missing **Status**, **People**, **Environments** or **Change requests** section in the
   template's form, filled from what the file and the repo already say. Content stays; only the
   shape changes.
4. Opens with a three-line *where we are*: what is live, what is being worked on, what is waiting
   on the customer. In that person's words.

### When the person changes

Projects outlive conversations. The developer who signed off the design hands over to the owner;
the owner delegates to an office manager. Signals: the vocabulary shifts ("schema" becomes "the
list"), the questions change, or they say so.

- **Recalibrate, don't restart.** Ask the new person the two Phase 0 calibration questions — what
  their part in the business is, and whether they want to weigh in or have you decide — and record
  both under **People**, with the words they use. Do this before acting on their first request.
- **Retell the state in their words.** A business user gets "your quotes now lock once sent", not
  "status transitions guard the write path". Never carry the previous person's jargon forward.
- **Earlier decisions stand** unless the new person changes them — and a change is a change
  request (Phase 9), recorded with who asked.
- **Authority is theirs to state.** If the new person asks for something that reverses a signed-off
  decision, say so plainly and ask whether they can make that call or whether someone else should
  confirm.

---

## 0 · FRAME

### Listen for capabilities, not labels

People say "an app", "a system", "a portal", "an ERP", "something to replace the spreadsheet".
The word tells you almost nothing. *"An app that tracks our customers, sends invoices and handles
support tickets"* is three modules whatever it is called. Listen for the nouns and verbs of their
business and map them to candidate modules using `customer_words` in the catalog.

**Speak their word back.** If they said "app", it is their app. Never make a customer learn
"ERP", "module", "master data" or "entity" — those are your working words, not theirs.

### Calibrate to the person — ask about the situation, not their competence

Two questions do the job, and both feed the design:

- **"What do you use for this today?"** — spreadsheets, an old tool, nothing. It shows their
  experience *and* what you are replacing.
- **"Should I make the technical calls and explain them as I go, or would you like to weigh in?"**
  — asks about involvement, not skill. "You decide" is a fine answer.

The answer changes how much you explain and how many options you offer. It never changes the
quality of the design underneath.

### Recommend a sequence, not a big bang

Name the **first module** — the one that hurts most or unlocks most (`first_module_fit` in the
catalog) — and show the rest as a roadmap in the customer's words.

| Decision | Default |
|---|---|
| Build order | one module at a time |
| Deployment | **one app, a router per module**; split only for a stated reason (public vs private surface, independent scaling, a trust boundary) |
| Data | **shared from day one** — one bucket, one sign-in pool, one master-data register |

The real danger for a first-time builder is not a monolith. It is module one and module two each
inventing their own customer list. So if anything on the roadmap touches money (invoicing,
purchasing, accounting), the **first** module references a shared business partner instead of
keeping its own customer table. Check `depends_on` in the catalog.

### Detect an existing system — don't ask, look

| Signal | Where |
|---|---|
| apps of the same system | `dodil ignite app list` / `app get` → `labels.group` |
| the sign-in already in use | `app get` → `user_pool` |
| where the data lives | `app get` → `env.BUCKET` |
| **what each module decided** | `ERP_*.md` at the root of the customer repo — the most reliable signal |

If a system exists: reuse its pool, bucket, master data and repo, and read every `ERP_*.md`
before designing anything.

### Hold the conversation, don't hand over a questionnaire

Most customers are not system builders. For them, how you ask decides whether you learn anything.

- **One topic per turn, one to three questions.** Never a numbered list of six open questions — a
  non-technical customer answers the first two and the rest are lost.
- **Offer choices with a recommended default**, plus "something else". "Everyone sees everything
  (recommended for a team of five)" is answerable; "what visibility model do you need?" is not.
- **Say back what you recorded** after each answer, in one line, so a wrong assumption is caught
  now rather than at sign-off.
- **Put anything the customer must read in the message itself, before the question.** A
  multiple-choice prompt can cover the text around it; a summary placed there goes unseen.
- **Review the design in parts** (who uses it and the screens → how the main thing flows → what
  happens on its own → what comes later), each confirmed before the next. Offer a visual page
  when a part is long.
- **Explain a decision you made for them in their terms**, one sentence: "quotes expire by date,
  so nothing has to run overnight".

With an experienced builder the same rules loosen: batch related questions, use their vocabulary
(a process person says "workflow" and "approval step"; a developer says "schema"), and skip what
they have already decided. Notice the words they use — it is a better signal than asking.

**Output:** a short scope note back to the customer — what you heard, the modules it maps to, the
recommended first module and order, and how involved they want to be.

## 1 · DISCOVER

`/modules/index.json` → the candidate module → `/modules/<id>/spec.yaml`.

**If the catalog says `spec: pending`,** there is no reference vocabulary for that module yet:
Phase 2 is done from scratch with the customer, in the house style of the modules that *do* have
specs (the entry's `start_from` names them). Write the customer's design file as usual — and if the
module is one the catalog expects to exist, write its spec too, from
[`/modules/module.template.yaml`](/modules/module.template.yaml), so the next engagement starts
from something. The spec gives you the
reference **actors, stories, workflows, entities, master data, jobs and seams** — a vocabulary to
correct with the customer, and `customer_varies`: the questions that most often change the design.

The reference **code** is for phase 4. Do not open it to decide what to build.

## 2 · UNDERSTAND THE BUSINESS

The only creative phase, and the whole value of the engagement.

Work through the reference spec *with* the customer. Ask the `customer_varies` questions in their
language, one or two at a time. For each reference story: keep, change, or drop. Then ask what is
missing. A novice will not volunteer system stories — ask "is there anything that should happen
on its own, on a schedule, or when something else happens?"

**Propose, do not interrogate.** Where a question carries `standard:` or `typical:`, read it out
and ask them to correct it. "Most firms like yours pay net 30, and the EU late-payment directive
assumes 30 unless you agree otherwise — is that you?" gets a truer answer, faster, than "what are
your payment terms?" — because people rarely recall a policy on demand and can always react to
one. An open question asked of someone who has never had to write the answer down produces a
guess, and the guess becomes a config row nobody revisits.

Two kinds of answer, and never blur them:

- **`standard:`** is a named authority — a directive, an accounting standard, a statute. The
  customer can look it up and hold you to it. Say which one, so *adopt it* and *deliberately
  differ from it* are both available to them. Most will adopt; the ones who differ usually have
  a reason worth writing into the design.
- **`typical:`** is what we have seen. No survey, no authority, no implication that a customer
  doing otherwise is wrong. It is conversation, and presenting it as more than that is how an
  implementation ends up defending a choice nobody ever made.

Record the answer either way — "they keep the standard" is as much a decision as departing from
it, and in six months nobody remembers which was which.

### 2a · Actors
Who uses it and what each may do and see. Becomes the role catalog.

### 2b · Stories — three kinds, each with acceptance criteria

- **Person** — someone does something through a screen.
  > **CRM-06** · As the commercial director, I want quotes above our discount threshold routed to
  > me, so that margin is protected. *Given* a 15% threshold and an 18% quote, *when* the rep
  > submits it, *then* it waits in my queue and the rep cannot approve it.
- **System** — the system acts on its own, on a schedule or an event.
  > **CRM-10** · Every 6 hours, the system re-scores open leads. *Given* a lead active an hour ago,
  > *when* the run completes, *then* its score reflects it, and running it twice changes nothing.
- **Integration** — the work crosses into another module, which owns it.
  > **CRM-20** · When a quote is accepted, a draft invoice appears in invoicing for the same
  > customer and amount.

Prefix story ids with the module (`CRM-06`) so they stay unambiguous in tests, commits and other
modules' designs. The Given/When/Then **is** the phase 5 test.

### 2c · Workflows
For each entity with a lifecycle: states, transitions, who may move it, and gates (thresholds,
approvals, locks). This is where "every customer has their own process" is captured.

### 2d · Entities
In business terms first: meaning, **natural key**, key attributes, relationships, owner. Decide
keys now — DataK3 has no sequences.

### 2e · Master data
For every entity: **shared** (one module is the sole writer, all others reference it) or
**module-owned** (table prefixed with the module: `crm_`, `gl_`). A foreign key is accepted and
never enforced, so "sole writer" is the integrity mechanism — decide it, don't assume it.

### 2f · Rules and controls
Every business rule and **where it is enforced** — almost always a guarded write path, not a
database constraint.

### 2g · Non-functional
Volumes, concurrency (16 concurrent reads per bucket), retention, residency, consent, and who
operates it after go-live.

## 3 · DESIGN

| From phase 2 | Becomes |
|---|---|
| Entities | tables, natural PKs; prefix = module-owned, no prefix = shared |
| Relationships that need walking | a typed graph (filter on `rel`) |
| "Find similar" | a `VECTOR` column |
| Person stories | routes **and** screens — the UI is derived from the stories |
| System stories | **jobs**: trigger, what runs, **where it runs** — Ignite has no scheduler: an always-on worker app with its own loop, or an external scheduler calling a route |
| Integration stories | a call to the owning module's route, never a write into its tables |
| Actors | permissions `<module>:<object>:<verb>` |

### Write `ERP_<MODULE>.md`

One file per module at the root of the customer's repo (`ERP_CRM.md`, `ERP_INVOICING.md`). Another
module finds it with a glob. Template: `/modules/template.md`. Same sections in every file.

The filename says ERP because it is for tooling. The **content** uses the customer's words.

### Sign-off — a hard stop

Show the customer the design in plain language — their actors, their stories, their screens, what
runs on its own, what is deliberately left out — and **wait for a yes** before phase 4. Record it in
the file's Decisions section with the date.

## 4 · BUILD

**Build it to the pattern: [/modules/pattern.md](/modules/pattern.md)** — the anatomy of a module
(three artifacts, one app with a router per module, prefixed tables and shared master data, natural
keys, idempotent writes, rules enforced on the write path, workflows tuned by rows rather than
code, jobs that are derived away where possible, screens from stories, a test per story). It is
short, and it is what keeps the fifth module looking like the first.

Copy the platform layer from **[`/code/platform-starter`](/code/platform-starter)** verbatim —
`db.py`, `sa_token.py`, `PLATFORM.md`, the app skeleton with its `POST /` self-check, an
idempotent `migrate.py`, the Dockerfile and the deploy files. `auth.py` is copied and then
extended with this customer's visibility rules, which is the one file in that layer that
legitimately grows.

Generate the business layer from `ERP_<MODULE>.md` — not from a reference package's routes. The
module packages (`crm-suite-app`, `gl-suite-app`, …) are worked examples to read; copying their
business logic over a generated system is how a customer ends up with somebody else's business.
Put the story id in each route's docstring.

## 5 · TEST

Against a dev bucket:

- **Acceptance** — one test per story, from its Given/When/Then. A story without a test is unverified.
- **Access** — per role: right rows, others' rows are *not found*, excluded actions refused.
- **Invariants** — totals net to zero, locked records refuse edits, re-imports are idempotent.

## 6 · PROVISION

### Environments — a test copy and a live copy, always

| | Test copy (dev) | Live |
|---|---|---|
| Bucket | `<customer>-dev` | `<customer>` |
| App | `<customer>-dev`, or **localhost** against the dev bucket | `<customer>` |
| Sign-in pool | `<customer>-dev` with test users | `<customer>` with real staff |
| Data | seed and test data; safe to wipe | the business's real records |
| Who changes it | you, freely, as work progresses | you, **only after the customer says "go live"** |

- **Local `.env` points at the test copy, never at live.** Live credentials exist only as secret
  references in `.dodil/deploy.yaml`. A local process that can write to live is how a customer's
  records get damaged by a test.
- **Say which one, every time.** "I've added the field on your *test copy*"; "this will change
  your *live* app". The customer should never have to ask.
- **Reading live is allowed, writing is not** — reading real counts ("you have 214 sent quotes")
  makes impact feedback concrete.
- **Preview on localhost first.** `uvicorn main:app --reload` plus the UI dev server against the
  test copy is the fastest way to let the customer see a change. Give them the URL and what to
  click. Deploy the dev app only when localhost is not enough (sign-in, phone, someone else
  needs to look).

### Creating things

- Dev first, live when the first release is approved.
- **Joining an existing system: merge the role catalog, never replace it.** `appid roles set`
  replaces the whole catalog; a module that writes only its own roles silently strips every other
  module's access. `roles get` → merge → `roles set --file`.
- Tables: create only the module's prefixed tables and any new shared master data (with its owner
  recorded in the register).
- **A pool with no `redirect_uris` cannot log anybody in.** Attaching `user_pool` to an app is
  half the job: the gateway's front door only opens for a callback the pool allows, so every app
  in the pool needs its callback added to the allowlist —

      https://<app>-<org>-<port>.ignite.dodil.cloud/.dodil/auth/callback

  and `appid settings set` REPLACES the settings block, so read it first and re-pass what should
  stay (`allow_signup`, TTLs), exactly like the role catalog. Adding the fifth module means five
  URIs in that list, not one. A pool missing them answers

      {"error":"redirect_uri is not in the pool's allowlist (settings.redirect_uris)"}

  which arrives at the person trying to sign in, not at you.
- **Create at least one user, and say so.** With `allow_signup: false` — the right setting for a
  business system — nobody can create their own account. A pool with a correct allowlist and no
  users still admits no one. Invite them (no password; they set one through the reset flow).

## 7 · DEPLOY

`git push` → CI → registry → CD from `.dodil/deploy.yaml`. There is no separate deploy command.

## 8 · VERIFY

- **The app reaches its own data** — ship a `POST /` self-check that opens the bucket with the
  app's own credential; `ignite invoke` calls it. A public-URL 401 proves only the gateway is up.
- **The gateway strips forged identity headers.**
- **A real person signs in**, once per role. You cannot do this — you never set passwords — so tell
  the customer it is their step, and don't call the module done until it has happened.
  This is the step most easily skipped, because the other two pass without it: `ignite invoke`
  and a service-client token both go round the browser login entirely. Five apps were deployed,
  self-checked, and exercised end to end through their APIs here before anyone discovered the
  pool had no `redirect_uris` and no users — the login had never once been tried. If you cannot
  hand someone a URL they can actually sign into, the module is not verified.

## 9 · OPERATE AND EVOLVE

### Change requests — a conversation, then autonomous execution

Customers ask for changes in passing: *"can we add a site-visit step before quoting?"*, *"office
should not see margins"*, *"a customer can have two VAT numbers"*. Most touch an entity or a
workflow, and most customers cannot tell what that implies. The job is to make the implication
visible **before** anything changes, then do all of it without further hand-holding.

**1 · Understand — interactively.** Restate it in their words and ask one to three questions with
choices. Probe the edges they will not think of:
- *What about the ones that already exist?* (existing records)
- *Who is allowed to do this?* (roles)
- *What should happen automatically?* (system stories)
- *Does anything else depend on it?* (other modules, reports, the PDF, imports)

**2 · Locate.** From `ERP_<MODULE>.md`: which stories, workflow transitions, entities, screens,
jobs and seams are touched. Read live counts where it helps.

**3 · Show the impact — plain language, before any work.** Send it as an ordinary chat message
of its own, *then* ask for the go-ahead. The approval question may only refer to what the customer
has already been shown: never "as described above" unless the description was actually sent, and
never impact that lives only in your notes or the CR you are about to write. Get the real counts
(read-only) *before* this message, not after the yes. This shape:

> **What you asked for:** a *Site visit* step between Enquiry and Quoted.
>
> **What changes for your team**
> - The pipeline board gets a new column, *Site visit*.
> - A deal can't be quoted until the visit is logged. *(new rule — you asked for this)*
>
> **What happens to what you already have**
> - Your **37 open deals** in Enquiry stay where they are.
> - Your **12 deals** already in Quoted are not moved back.
>
> **What doesn't change:** quotes, prices, the PDF, reports.
>
> **How it goes live:** first on your test copy — I'll show you on localhost — then live when you
> say so. Nothing is deleted; undoing it means removing the column again.
>
> **Shall I go ahead?**

Name the migration honestly. On this platform `ALTER TABLE … ADD COLUMN` is the only cheap change
— and it currently unsettles reads on that table for minutes afterwards, so it happens on the test
copy first and at a quiet hour on the live one. A rename or type change is *add new → backfill →
switch reads → drop old later*; nothing is dropped in the same release that stops using it. If existing records need a value, say which value and why.

**4 · Record.** On a yes: add `CR-<n>` under **Change requests** (date, who asked, who approved,
summary), and amend the stories, workflows, entities and reference diff it touches. The design
file changes first, the code second — so it never describes a system that no longer exists.

**5 · Execute — autonomously.** No further questions unless something contradicts what was agreed:
update code, add or amend the story tests, migrate the **test copy**, run the whole suite, start
localhost, and tell the customer what to look at.

**6 · Go live — on their word.** After they have seen it: export or snapshot the affected live
tables, migrate live, deploy, run the self-check, and report back in one short message — what
changed, how many records were touched, and how to undo it. Mark the CR *live*.

A change request that turns out to be a new capability (*"can it also send invoices?"*) is not a
change — it is the next module, and goes back to Phase 0.

### Also

- **Rollback:** `ignite version rollback` returns the app to an earlier deployed version; the data
  is not versioned with it. That is why step 6 exports
  first.
- **Recurring work** — whatever Phase 2 named, and where it runs.
- **The next module** → back to Phase 0, as "joining an existing system": read every `ERP_*.md`
  first.
