The storefront example
examples/storefront in the repository is a complete app on Node and SQLite. It is small enough
to read in one sitting and touches everything Bounda does.
git clone https://github.com/bounda-dev/boundacd bounda && pnpm install && pnpm build && pnpm generatecd examples/storefrontpnpm testpnpm startWhat happens when an order is placed
Section titled “What happens when an order is placed”placeOrdervalidates the items, computes the total and appendsOrderPlaced.- The policy
send-confirmation-on-order-placeddispatchessendConfirmation, whose handler calls thenotifiercollaborator.bounda.config.tspicksnotifier.consolefor the demo and the tests picknotifier.memory, which records what was sent. - The policy
schedule-reminder-on-order-placeddispatchessendReminderwith a delay of a day. The reminder is a scheduled command; when it runs, the handler appendsReminderSentonly if the order is stillplaced. - The process
order-lifecyclestarts. When the order is confirmed it dispatchesfulfillOrder; if nothing completes it within 72 hours, its time-out handler cancels the order. - Two read models follow along:
order-summary, with a query written in SQL, andmy-orders.
The aggregate has no state.ts. Its state is inferred from the apply functions, so
state.status is "placed" | "confirmed" | "fulfilled" | "cancelled" | undefined in every
handler.
Things worth copying
Section titled “Things worth copying”A collaborator with two implementations. commands/send-confirmation/index.ts declares the
contract; notifier.console.ts and notifier.memory.ts implement it. The config decides:
export default defineConfig({ storage: sqlite({ path: process.env.STOREFRONT_DB ?? "./data/storefront.db" }), commands: { sendConfirmation: { notifier: { use: process.env.NOTIFIER ?? "console" } } },});A delay from the environment. A duration typed by the compiler is a literal such as "24h";
one that comes from an environment variable is a string. asDuration checks it where it is used:
await commands.sendReminder( { orderId: event.aggregateId }, { delay: asDuration(process.env.REMINDER_DELAY ?? "24h") },);Time in tests. The clock of createTestApp moves only when told to, so a reminder a day away
and a time-out three days away are two lines:
clock.advance(24 * HOUR);await app.processUntilIdle();A query in SQL. list-orders-by-customer.ts reads the table directly through client and
still gets rows typed from the view’s fields:
export const repository = ({ client, customerId }: Query.RepositoryArgs) => client.all( "SELECT * FROM bounda_order_summary WHERE customer_id = ? ORDER BY placed_at, order_id", [customerId], );Booting from the generated registry. tests/boot.test.ts starts the app twice on the same
SQLite file through boot() and reads back what the first run wrote.