Tillwire Read the guide
Developers

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.

terminal
# 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
The commands come from the integration guide. /healthz answers ok when the store is ready.
Start

A demo store in three steps.

  1. Build the engine

    cd engine
    go build -o bin/engine ./cmd/engine

    Needs Go 1.26.1 or later.

  2. Start a demo store

    sh tools/demo.sh ../engine/bin/engine \
      ../local/data ../local/local.yaml

    Seeds a catalogue in about 45 seconds.

  3. 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.

Operations API

259 operations. One way to call them.

From the OpenAPI file and the integration guide, main on 21 October 2026
Operations259 in 31 scopes: the same operations as /admin. A scope is an exact match.
DescriptionGET /api/v1/openapi.json, OpenAPI 3.1, with no token.
ErrorsOne body: error, message, code, field, request_id. Use code: it is stable and the list only grows.
RetriesIdempotency-Key on a POST; the answer is kept for 24 hours.
Paginglimit and after, with a Next-Cursor header and a Link header.
Rate limitsA one-minute window per token, 60 calls by default, with RateLimit headers.
StabilityOne 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.

Events

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.read
  • order.placedAn order was confirmed and waits for its payment
  • order.paidAn order was paid
  • order.packedAn order was packed
  • order.shippedAn order was shipped, or the shipment was corrected
  • order.deliveredAn order was delivered
  • order.cancelledAn order was cancelled
  • order.refundedAn order was refunded in full

Shipments

orders.read
  • shipment.in_transitA shipment is on its way, for the first time
  • shipment.out_for_deliveryA shipment is with the driver, for the first time
  • shipment.ready_for_collectionA shipment is at its pickup point or locker
  • shipment.deliveredA shipment was delivered

Catalogue

catalogue.read
  • product.createdA product was added
  • product.updatedThe owner or an integration edited a product or a variant
  • product.review.approvedA person approved a product’s review
  • product.review.sent_backA person sent a product’s review back with a note
  • product.review.look_againAn approved product changed, and a person must look again
  • product.review.photo_askedA person asked for a new photo of a product
  • product.review.retiredA person approved a product’s retire
  • picture.approvedA version of a picture was approved
  • picture.rejectedA version of a picture was rejected
  • picture.version_requestedA picture tool was asked for a new version

Stock

stock.read
  • stock.lowA variant fell to its reorder level or below it

Customers

customers.read
  • customer.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.

Extend it

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.

  1. 01

    Your table

    The engine runs it after its own migrations, in one transaction. Your names start with your prefix.

    engine/acme/migrations/0001_notes.sql
    CREATE TABLE x_acme_notes_notes (
      product_id INTEGER NOT NULL,
      note       TEXT NOT NULL,
      author     TEXT NOT NULL,
      created_at TEXT NOT NULL
    );
  2. 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.

    engine/acme/module.go
    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),
      })
    }
  3. 03

    Your screen

    Answer HTML and the engine puts it in its own admin page: its sign-in, its layout, its security headers.

    engine/acme/notes.go
    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))
    }
  4. 04

    Your binary

    A copy of the engine’s own main package with one more import line. Build it, and the module is in.

    engine/cmd/acme-shop/main.go
    import (
      "ecomlite/engine/internal/cli"
      _ "ecomlite/engine/acme" // the one new line
    )
The result

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.

Extension points

What you can change, and how.

Yes

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 API
Yes

Your own features in /admin

Your tables, screens, menu entries and staff rights, as a module in your fork.

engine/ext
Yes

Your own rules

Checks before a product goes live, holds on pictures, your part of a privacy erasure.

engine/ext
Yes

Your own services

Anything outside the engine: read and write with 259 operations, react to 23 events.

API and webhooks
Yes

A new courier

Your own HTTP service on the courier protocol, proved with one certify command.

courier/1
By an engine change

Payments, mail transports

Built into the engine. A new one is a change to the engine itself, with a conformance suite to pass.

Engine change
Not yet

New 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 v1
Not yet

The admin layout itself

Module screens live inside the engine’s admin layout; the layout is the engine’s.

Not in ext v1

From the extension API and the integration guide on main, 21 October 2026.

engine/ext · version 1 with the 1.1 additions

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.

Rules

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.