No plugin system to learn. Write normal code in the same repository.
Pick an event, write what should happen. Waits of days or weeks survive restarts and deploys.
workflows.ts
Durable// lib/rules/workflows.ts
{
event: "cart.abandoned",
name: "recover-abandoned-cart",
run: async (payload, step) => {
await step.sleep("settle", "3d")
await step.run("send-offer", () =>
queueEmail({ template: "cart-recovery", to: payload.email })
)
},
}Every event goes through one path. A second listener system was deliberately left out, because two delivery paths caused double-counting bugs before.
Events are sent after the database write is saved, so a rolled-back write never triggers one.
Each one is a file you can open. Where the compiler checks your work, it says so.
Over forty events are available, including order.paid, cart.abandoned and return.approved. A workflow is an event name and a function. Use step.sleep to wait days, step.run to do something once. It is normal code you write in the repository.
Events are typed, so a workflow bound to a name that doesn't exist fails the build.
Add a new block type in one place and TypeScript lists every file you still need to update: the registry, the admin form and the renderer. A half-finished block will not build.
Add a kind without a renderer and the build stops.
The product page has named sections: gallery, details, cart, content and related. To add a size guide or a made-to-order notice, register a module and pick its section. You do not fork the page.
Admin modules extend a shared BaseCRUD rather than reimplementing pagination, soft delete, activity logging, and permission checks each time. A new entity gets the same table behaviour, the same audit trail, and the same archive-versus-delete semantics as the twelve modules already built on it.
Includes are guarded with satisfies Prisma.<Model>DefaultArgs, so schema drift is a type error.
Stripe is the only payment provider wired up today. There is a PaymentProvider interface to implement if you want to add another, and webhook verification already sits behind it.
An unknown provider name throws instead of silently falling back to Stripe.
Send store events to an external system. Each delivery is logged, there is a test endpoint, and endpoints that keep failing are disabled automatically.
Delivery attempts are logged, so a silently dead endpoint becomes visible.
Six newsletter providers work through one interface: Klaviyo, Mailchimp, ConvertKit, Buttondown, Loops and Beehiiv. Storage, analytics and AI work the same way. A service you have not configured hides its screens instead of failing halfway.
A form field with no database column behind it fails to compile. The checks cover thirty-two schemas and apply to code you add too.
Ratchets fail CI when coverage regresses.
What it does not do, said plainly.
These are seams inside a codebase you own. There is no public plugin API. The project ships from main and internal boundaries still move between releases — so an extension you write lives in your fork, and upgrading means reading a diff rather than trusting a compatibility promise. If you need a versioned extension contract that a vendor maintains for you, this is the wrong tool and a headless platform is the right one.