f { store }

Supabase Starter Kit For Event-Driven Applications

Foundation for building compositional, safe and ergonomic applications

#EventSourcing #EdgeFunctions

Backend - Supabase

Supabase SQL migration scripts and TypeScript edge functions included.

SQL table schemas, Row-level security, SQL functions and TypeScript/Deno edge functions in source code.
$ supabase start

Frontend - Admin Application

Technology: Next.js 14, React, TypeScript, TailwindCSS, Shadcn/Ui

Register deciders and views. Control the event streams. Browse events and observe the business metrics.
$ npm run dev

Order Management included!

Event Modeling

Statistics included!

Supported by TimescaleDB Apache 2.0 Edition extension

Statistics

Build Command Handlers

Focus on the business logic - the Decider, and run it as the Edge function. Combine many deciders to compose and evolve command handling capabilities.

Deno.serve(async (cmd: Request) => await edgeCommandHandler(
    cmd,
    commandAndMetadataSchema,
    restaurantDecider.combine(orderDecider),
    ),
);

Build Event Handlers

Focus on the business logic - the View, and run it as the Edge function. Combine many views to compose and evolve event handling capabilities.

Deno.serve(async (evt: Request) => await edgeEventHandler(
    evt,
    eventAndMetadataSchema,
    restaurantView.combine(orderView),
    new RestaurantViewStateRepository(
      authenticatedClient(evt),
    )));

Register Command Handlers

Specify the event types that comand handler can publish.

Control the design, so `bad` events are not stored by mistake.

Register Event Handlers

Stream events to concurent consumers, and track the progress.

Control how the events are consumed, replayed, and projected.

Achieve complete automation of information flow and business processes

#Discover #Design #Develop #Deploy

Book an appointment

Event Modeling

Event Modeling

Event Modeling is an effective technique to discover a domain. It is a method of describing systems using an example of how information has changed within them over time. Specifically this omits transient details and looks at what is durably stored and what the user sees at any particular point in time. These are the events on the timeline that form the description of the system.


We can show, by example, what a system is supposed to do from start to finish, on a time line and with no branching.

Specification By Example

Specification By Example is a collaborative approach to software analysis and testing. It is the fastest way to align people from different roles on what exactly we need to build and how to test it.


The requirements are presented as scenarios. A scenario is an example of the system’s behavior from the users’ perspective, and they are specified using the Given-When-Then structure to create a testable specification.


The specification is written in the form of executable tests that are automated and can be run continuously. Tests cover the full range of scenarios, from happy paths to errors.

specification by example
decider kotlin

f { model } / Software Model

The f { model } library is implementing the event model in a general way. It promotes clear separation between data and behaviour.

The flow is parametrized with Command, Event, and State parameters. The responsibility of the business is to specialize in their case by specifying concrete Commands, Events, and State. For example, Commands=CreateOrder, AddItemToOrder; Events=OrderCreated, ItemAdded, State=Order(with list of Items).


Edge Functions

You can run three types of functions on the platform:

1. Command-handlers (deciders) - responsible for handling commands and producing new events/decisions.

2. Event-handlers responsible for:
  a) handling events and evolving the materiliazed view(s) state based on these events
  b) automating integration with other systems, like payment providers.

3. Query-handlers (views) - responsible for querying the materilized view(s) state.


Moreover, it`s worth noting that these functions can be executed in a conventional manner, either on the server or directly within the database. They are not restricted solely to edge processing.

CQRS

Decider / Command Handler

A pure command handling algorithm, responsible for making decisions/events based on the commands and the current state. It does not produce any side effects, such as I/O, logging, etc. It is written in the TypeScript programming language and utilizes type narrowing to make sure that the command is handled exhaustively.


The infrastructure is already implemented for you and it is supporting the Edge scenario, in where we use Supabase API to store and fetch events, so you can focus on the business logic and the core domain model!


export const restaurantDecider: Decider<
RestaurantCommand,
Restaurant | null,
RestaurantEvent
> = new Decider<RestaurantCommand, Restaurant | null, RestaurantEvent>(
(command, currentState) => {
  switch (command.kind) {
    case "CreateRestaurantCommand":
      return (currentState === null ||
          currentState.restaurantId === undefined)
        ? [
          {
            version: 1,
            decider: "Restaurant",
            kind: "RestaurantCreatedEvent",
            id: command.id,
            name: command.name,
            menu: command.menu,
            final: false,
          },
        ]
        : [
          {
            version: 1,
            decider: "Restaurant",
            kind: "RestaurantNotCreatedEvent",
            id: command.id,
            name: command.name,
            menu: command.menu,
            reason: "Restaurant already exist!",
            final: false,
          },
        ];
    case "ChangeRestaurantMenuCommand":
      return (currentState !== null &&
          currentState.restaurantId === command.id)
        ? [
          {
            version: 1,
            decider: "Restaurant",
            kind: "RestaurantMenuChangedEvent",
            id: currentState.restaurantId,
            menu: command.menu,
            final: false,
          },
        ]
        : [
          {
            version: 1,
            decider: "Restaurant",
            kind: "RestaurantMenuNotChangedEvent",
            id: command.id,
            menu: command.menu,
            reason: "Restaurant does not exist!",
            final: false,
          },
        ];
    case "PlaceOrderCommand":
      return (currentState !== null &&
          currentState.restaurantId === command.id)
        ? [
          {
            version: 1,
            decider: "Restaurant",
            kind: "RestaurantOrderPlacedEvent",
            id: command.id,
            orderId: command.orderId,
            menuItems: command.menuItems,
            final: false,
          },
        ]
        : [
          {
            version: 1,
            decider: "Restaurant",
            kind: "RestaurantOrderNotPlacedEvent",
            id: command.id,
            orderId: command.orderId,
            menuItems: command.menuItems,
            reason: "Restaurant does not exist!",
            final: false,
          },
        ];
    default:
      // Exhaustive matching of the command type
      const _: never = command;
      return [];
  }
},
(currentState, event) => {
  switch (event.kind) {
    case "RestaurantCreatedEvent":
      return { restaurantId: event.id, name: event.name, menu: event.menu };
    case "RestaurantNotCreatedEvent":
      return currentState;
    case "RestaurantMenuChangedEvent":
      return currentState !== null
        ? {
          restaurantId: currentState.restaurantId,
          name: currentState.name,
          menu: event.menu,
        }
        : currentState;
    case "RestaurantMenuNotChangedEvent":
      return currentState;
    case "RestaurantOrderPlacedEvent":
      return currentState;
    case "RestaurantOrderNotPlacedEvent":
      return currentState;
    default:
      // Exhaustive matching of the event type
      const _: never = event;
      return currentState;
  }
},
null,
);
                
cqrs

export const restaurantView: View<RestaurantView | null, RestaurantEvent> =
new View<RestaurantView | null, RestaurantEvent>(
  (currentState, event) => {
    switch (event.kind) {
      case "RestaurantCreatedEvent":
        return { restaurantId: event.id, name: event.name, menu: event.menu };
      case "RestaurantNotCreatedEvent":
        return currentState;
      case "RestaurantMenuChangedEvent":
        return currentState !== null
          ? {
            restaurantId: currentState.restaurantId,
            name: currentState.name,
            menu: event.menu,
          }
          : currentState;
      case "RestaurantMenuNotChangedEvent":
        return currentState;
      case "RestaurantOrderPlacedEvent":
        return currentState;
      case "RestaurantOrderNotPlacedEvent":
        return currentState;
        // deno-lint-ignore no-case-declarations
      default:
        // Exhaustive matching of the event type
        const _: never = event;
        return currentState;
    }
  },
  null,
);
                

View / Event Handler

A pure event handling algorithm, responsible for evolving the state of the view/projection. It does not produce any side effects, such as I/O, logging, etc. It is written in the TypeScript programming language and utilizes type narrowing to make sure that the event is handled exhaustively.

Query Handler

As your event handler is responsible for evolving the state of the materilized view, the query handler is responsible for querying the materilized view state. Supabase will create the API on top of the Postgres database, so you can query the materilized view state. You can choose to create an edge function to handle the query or use the Supabase API directly.

Process Manager

Event Handlers track the progress of the process by storing the position/offset of the last event that was processed successfully. This is what makes them Process Managers, and they can be used to implement Sagas, Orchestrators, and other process management/integration patterns.