Build beside the engine, not inside it.
Your service, theme or module talks to Tillwire through public doors only. Each door has a contract in the repository and a test that keeps it equal to the code.
# build, seed a demo store, serve it $ go build -o bin/engine ./cmd/engine $ sh tools/demo.sh bin/engine ./data store.yaml $ engine serve --config store.yaml --data ./data --templates theme $ curl localhost:8081/healthz ok
/healthz answers ok when the store is ready.A demo store in three steps.
Build the engine
cd engine go build -o bin/engine ./cmd/engine
Needs Go 1.26.1 or later.
Start a demo store
sh tools/demo.sh ../engine/bin/engine \ ../local/data ../local/local.yaml
Seeds a catalogue in about 45 seconds.
Make an API token
engine api token create \ --integration my-service \ --scopes orders.read,catalogue.read
The token shows once; the store keeps only its digest.
259 operations. One way to call them.
| Operations | 259 in 31 scopes: the same operations as /admin. A scope is an exact match. |
|---|---|
| Description | GET /api/v1/openapi.json, OpenAPI 3.1, with no token. |
| Errors | One body: error, message, code, field, request_id. Use code: it is stable and the list only grows. |
| Retries | Idempotency-Key on a POST; the answer is kept for 24 hours. |
| Paging | limit and after, with a Next-Cursor header and a Link header. |
| Rate limits | A one-minute window per token, 60 calls by default, with RateLimit headers. |
| Stability | One version, /v1. Operations and fields are added, never removed. |
The 31 scopes
- orders.read
- orders.write
- quotes.write
- catalogue.read
- catalogue.write
- forms.read
- forms.write
- announcements.read
- promotions.read
- promotions.write
- reviews.read
- reviews.write
- mail.read
- mail.write
- payments.read
- payments.write
- content.read
- content.write
- subscriptions.read
- subscriptions.write
- customers.read
- customers.write
- shipping.read
- shipping.write
- store.read
- store.write
- stock.read
- stock.write
- events.read
- webhooks.read
- webhooks.write
A write scope does not give the read scope of the same group: ask for each scope you need.
23 events. Read them or receive them.
Read the feed at GET /api/v1/events when your service cannot take calls, or register a webhook. Each webhook is signed with HMAC-SHA256, tried 8 times, logged and can be replayed.
Orders
orders.readorder.placedAn order was confirmed and waits for its paymentorder.paidAn order was paidorder.packedAn order was packedorder.shippedAn order was shipped, or the shipment was correctedorder.deliveredAn order was deliveredorder.cancelledAn order was cancelledorder.refundedAn order was refunded in full
Shipments
orders.readshipment.in_transitA shipment is on its way, for the first timeshipment.out_for_deliveryA shipment is with the driver, for the first timeshipment.ready_for_collectionA shipment is at its pickup point or lockershipment.deliveredA shipment was delivered
Catalogue
catalogue.readproduct.createdA product was addedproduct.updatedThe owner or an integration edited a product or a variantproduct.review.approvedA person approved a product’s reviewproduct.review.sent_backA person sent a product’s review back with a noteproduct.review.look_againAn approved product changed, and a person must look againproduct.review.photo_askedA person asked for a new photo of a productproduct.review.retiredA person approved a product’s retirepicture.approvedA version of a picture was approvedpicture.rejectedA version of a picture was rejectedpicture.version_requestedA picture tool was asked for a new version
Stock
stock.readstock.lowA variant fell to its reorder level or below it
Customers
customers.readcustomer.createdThe store kept an email address for the first time
From EventTypes in the operations contract on main. To read a type, a token needs events.read and the group shown.
Add your own feature in four files.
An example: product notes for your staff. It gets its own table, its own screen in /admin, a place in the menu and its own staff right. No engine file changes, so the next upstream update merges cleanly.
- 01
Your table
The engine runs it after its own migrations, in one transaction. Your names start with your prefix.
CREATE TABLE x_acme_notes_notes ( product_id INTEGER NOT NULL, note TEXT NOT NULL, author TEXT NOT NULL, created_at TEXT NOT NULL ); - 02
Your module
Register the table, a staff right and the screen. A bad registration stops the start, so a mistake never reaches a buyer.
func init() { sql, _ := fs.Sub(files, "migrations") ext.RegisterMigrations("acme-notes", sql) ext.RegisterRight("acme-notes", ext.Right{ Name: "edit", Group: "catalogue", Label: "Edit product notes", }) ext.RegisterAdmin(ext.Module{ Name: "acme-notes", Title: "Product notes", Right: "edit", Menu: true, Handler: http.HandlerFunc(notes), }) }
- 03
Your screen
Answer HTML and the engine puts it in its own admin page: its sign-in, its layout, its security headers.
func notes(w http.ResponseWriter, r *http.Request) { u, _ := ext.CurrentUser(r) w.Header().Set("Content-Type", "text/html") fmt.Fprintf(w, "<p>Notes for the team, %s</p>", html.EscapeString(u.Email)) }
- 04
Your binary
A copy of the engine’s own main package with one more import line. Build it, and the module is in.
import ( "ecomlite/engine/internal/cli" _ "ecomlite/engine/acme" // the one new line )
Your screen, inside the back office
At /admin/x/acme-notes/, in the admin menu for the people whose roles hold your right. Owners always have it.
An illustration of the example module. Module screens use the engine’s admin layout.
What you can change, and how.
The whole shop front
A theme on the storefront contract: every page, every mail, all the CSS. Or your own app on the API.
Theme or APIYour own features in /admin
Your tables, screens, menu entries and staff rights, as a module in your fork.
engine/extYour own rules
Checks before a product goes live, holds on pictures, your part of a privacy erasure.
engine/extYour own services
Anything outside the engine: read and write with 259 operations, react to 23 events.
API and webhooksA new courier
Your own HTTP service on the courier protocol, proved with one certify command.
courier/1Payments, mail transports
Built into the engine. A new one is a change to the engine itself, with a conformance suite to pass.
Engine changeNew API routes from a module
Not in the extension API yet. Build them in your own service on top of the API.
Not in ext v1The admin layout itself
Module screens live inside the engine’s admin layout; the layout is the engine’s.
Not in ext v1From the extension API and the integration guide on main, 21 October 2026.
A fork with its own modules.
Keep your code in its own folder and never edit engine files. Each module registers in init. With nothing registered, the engine does not change; a test proves it.
your-fork/ ├─ engine/ upstream: never edited │ ├─ ext/ the one public surface │ └─ internal/ no promise ├─ engine/acme/ your modules └─ engine/cmd/acme-shop/ your main package
RegisterMigrationsYour own SQL tables, under your own name prefix.RegisterAdminYour own /admin screens, inside the engine’s sign-in and layout.RegisterRightStaff rights that roles hold like any other right.RegisterEraserYour part of a privacy erasure, in the same transaction.RegisterApprovalCheckRead-only checks before a product goes live.RegisterPictureHoldHold a picture from the shop, with a reason.Two more hooks exist, a code generator and a picture importer. The engine does not call them yet.
Rules a module can count on
- A bad registration stops the start, so a broken module never serves a buyer.
- A check, a hold and the code generator each get 2 seconds and read-only access.
- A module that is late, fails or panics fails closed: the act is refused.
- Your tables live in the same database, so backups and restores include them.
- Rights of a module that is gone are removed from every role.
The module path is ecomlite/engine/ext today. It changes when the engine takes its new name; this page will show the new path then.
The rules that keep updates clean.
Never read or write the engine’s database from your service. Use the API.
Never rely on message text, page HTML or field order. Use code and the contract.
Never put a secret in store.yaml or in git. Use the environment or a file.
A module that keeps personal data registers an eraser.
Source: docs/INTEGRATING.md and engine/ext on main, 21 October 2026. The guide is in the engine repository.