Skip to content
v0.3

Build an app

An app is how new behaviour arrives without the kernel learning anything about you. It runs beside the kernel, drives it through the same port an external integration uses, and can be installed and removed without leaving a wound.

Read apps & extension for where apps sit among the four extension points.

A manifest, and it is the whole contract. What it may declare falls into four groups:

  • What it may reach. The scopes it wants consent for. Installing the app grants them; it reaches exactly those and gets a named forbidden for anything else. The check is the kernel’s, not the app’s honesty.
  • Where it appears. Slots it renders into, in the storefront or the admin; blocks the merchant places, repeats and configures on a page; and a whole admin screen the app owns, reached from its own entry in the admin menu. The screen’s content is the app’s own component — not a form generated from the manifest.
  • When it acts. Events it reacts to, scheduled jobs the platform runs for it, actions an operator triggers from the app’s own page, and decisions it answers while the kernel is still deciding.
  • What it keeps and what it brings. Its own data; the configuration an operator fills in per store; custom fields it adds to the store’s registry; notification types, and the channel they can be sent over; the outbound connections it is allowed to open; and its own message catalogs, so its words reach an operator in the operator’s language.

Every one of these is on demand. An app declares only what it uses — most use two or three groups, and an app that declares none of them is a manifest with a name on it.

⚠️ This is the shape, not the index. Do not read the groups above as a count of what a manifest can carry: they are how the keys sort, and keys get added. The exhaustive, current list is the manifest schema itself, in @forgeco/contracts — Forge is source-available, the schema is what actually validates your manifest, and it cannot fall behind the platform the way a sentence can.

The scopes in the manifest are what an operator sees on the consent screen, so they are a promise as much as a permission. catalog.read and catalog.product.write are very different sentences to show someone.

An app that asks for more than it uses will be installed by fewer people, and correctly so.

When an app has data that belongs to it, it gets isolated storage. It does not add columns to the kernel’s tables and does not write to them.

That is what makes uninstall clean: the app’s own storage goes with it, and the kernel’s tables were never touched to begin with.

⚠️ Clean is not the same as undone. What an app created through the port — the hundred products an importer published — is the store’s data now, and stays. Custom fields the app declared are archived rather than deleted, because orders already placed reference their definitions. Uninstall removes the app’s footprint, not the history it took part in.

⚠️ Data that belongs to a kernel entity — an extra attribute on a product, a field the checkout collects — is not app data. That is a custom field: it rides in the item’s metadata and travels with the order snapshot like everything else. An app can declare custom fields in its own manifest, and they are materialised into the store’s registry at install, beside the ones the merchant defined by hand — same registry, marked with the app that brought them.

An app’s code runs in-process with the kernel. That is what makes decisions, slots and operator actions possible, and it means the trust boundary is installation, not runtime sandboxing.

So:

  • Consent is the gate. What an app may reach is fixed at install and enforced by the kernel on every call. It cannot widen its own grant at runtime.
  • Everything is on the record. An app’s calls appear in the audit trail under the app’s identity, the refusals included.
  • Installing an app is trusting its code, the way installing a plugin always is. Read the manifest, and install apps whose source you can see.
  • ⚠️ In-process also means sharing the process’s fate. The loader refuses an app that is incompatible, but a compatible app that is badly written can hurt the instance it runs in — hold a transaction open, leak memory, block the event loop. Consent bounds what an app may REACH; it does not bound how well it behaves. That is the honest limit of the model, and it is why the previous point is not a formality.

A seed script, a demo-data generator, an importer, a one-off migration driver — anything that drives the port. Write it as an app rather than a loose script.

Not for tidiness: the app model already gives it install, consent, scope and isolated storage, and a script that lives outside gets none of those and grows its own worse versions of all four.

  • Touch the database directly. There is one write path; an app uses it like everybody else.
  • Assume it is the only app. Slots are shared, events are broadcast, the admin menu lists every app that brought a screen, and another app may be doing something adjacent.
  • Hold a transaction open. If you are answering a decision, you are inside one — be quick and be deterministic.