{"id":38598795,"url":"https://github.com/fraktalio/fstore-sql","last_synced_at":"2026-01-17T08:31:39.688Z","repository":{"id":103209273,"uuid":"595280969","full_name":"fraktalio/fstore-sql","owner":"fraktalio","description":"PostgreSQL as event store - event sourcing \u0026 event streaming","archived":false,"fork":false,"pushed_at":"2025-11-01T10:46:10.000Z","size":1649,"stargazers_count":39,"open_issues_count":0,"forks_count":4,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-11-01T12:18:50.605Z","etag":null,"topics":["event-sourcing","event-streaming","postgresql","sql"],"latest_commit_sha":null,"homepage":"","language":"PLpgSQL","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fraktalio.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-01-30T19:08:09.000Z","updated_at":"2025-11-01T10:46:14.000Z","dependencies_parsed_at":"2024-02-09T22:27:37.675Z","dependency_job_id":"ffe1463f-f075-487f-864e-e502cce07b3f","html_url":"https://github.com/fraktalio/fstore-sql","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/fraktalio/fstore-sql","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fraktalio%2Ffstore-sql","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fraktalio%2Ffstore-sql/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fraktalio%2Ffstore-sql/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fraktalio%2Ffstore-sql/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fraktalio","download_url":"https://codeload.github.com/fraktalio/fstore-sql/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fraktalio%2Ffstore-sql/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28504364,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-17T06:57:29.758Z","status":"ssl_error","status_checked_at":"2026-01-17T06:56:03.931Z","response_time":85,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["event-sourcing","event-streaming","postgresql","sql"],"created_at":"2026-01-17T08:31:39.597Z","updated_at":"2026-01-17T08:31:39.674Z","avatar_url":"https://github.com/fraktalio.png","language":"PLpgSQL","readme":"# fstore-sql (event-store, based on `postgres`)\n\nThis project offers a seamless SQL model for efficiently prototyping event-sourcing and event-streaming by using Postgres database.\n\n**Check the [schema.sql](schema.sql) and [extensions.sql](extensions.sql)! It is all there!** No additional tools, frameworks, or programming languages are required at this level.\n\n## Table of contents\n\u003c!-- TOC --\u003e\n* [Run Postgres](#run-postgres)\n  * [Requirements](#requirements)\n* [Examples of usage](#examples-of-usage)\n  * [Event Sourcing](#event-sourcing)\n    * [1. Registering a simple decider `decider1` with two event types it can publish: 'event1', 'event2'](#1-registering-a-simple-decider-decider1-with-two-event-types-it-can-publish-event1-event2)\n    * [2. Appending two events for the decider `f156a3c4-9bd8-11ed-a8fc-0242ac120002`.](#2-appending-two-events-for-the-decider-f156a3c4-9bd8-11ed-a8fc-0242ac120002)\n    * [3. Get/List events for the decider `f156a3c4-9bd8-11ed-a8fc-0242ac120002`](#3-getlist-events-for-the-decider-f156a3c4-9bd8-11ed-a8fc-0242ac120002)\n  * [Event Streaming](#event-streaming)\n    * [4. Registering a (materialized) view `view1` with 1 second pooling frequency, starting from 28th Jan.](#4-registering-a-materialized-view-view1-with-1-second-pooling-frequency-starting-from-28th-jan)\n    * [5. Appending two events for another decider `2ac37f68-9d66-11ed-a8fc-0242ac120002`.](#5-appending-two-events-for-another-decider-2ac37f68-9d66-11ed-a8fc-0242ac120002)\n    * [6a. Stream the events to concurrent consumers/views](#6a-stream-the-events-to-concurrent-consumersviews)\n    * [6b. Stream the events to concurrent consumers / edge-functions (views)](#6b-stream-the-events-to-concurrent-consumers--edge-functions-views)\n* [Design](#design)\n* [fmodel](#fmodel)\n    * [fmodel-kotlin | fmodel-ts | fmodel-rust | fmodel-java](#fmodel-kotlin--fmodel-ts--fmodel-rust--fmodel-java)\n    * [FModel Demo Applications](#fmodel-demo-applications)\n* [Try YugabyteDB](#try-yugabytedb)\n* [References and further reading](#references-and-further-reading)\n\u003c!-- TOC --\u003e\n\nThis model is enabling and supporting:\n\n- `event-sourcing` data pattern (by using Postgres database) to durably store events\n    - Append events to the ordered, append-only log, using `entity id`/`decider id` and `decider` type as a key\n    - Load all the events for a single entity/decider, in an ordered sequence, using the `entity id`/`decider id` and `decider` type as a\n      key\n    - Support optimistic locking/concurrency\n- `event-streaming` to concurrently coordinate read over a stream of messages from multiple consumer instances/views\n    - Support real-time concurrent consumers to project events to view/query models\n    - Acknowledge that event with `decider_id` and `offset` is successfully processed by the view / ACK\n    - Acknowledge that event with `decider_id` is NOT processed by the view, and the view will process it again automatically / NACK\n    - (Optionally) Acknowledge that event with `decider_id` is NOT processed by the view, and the view will process it again after delay / SCHEDULE NACK\n \nEvery decider/entity stream of events represents an independent `kafka-like` **partition**. The events within a **partition** are ordered. There is no ordering guarantee across different partitions.\n\n![CQRS](.assets/cqrs.png)\n\n**The API** is a set of SQL functions that you can use to interact with the database. You can use them in your application. The API is what you would expect from a typical event-sourcing and event-streaming database.\n\n| SQL function / API                |    event-sourcing    |   event-streaming   |                                                                                                           description |\n|:----------------------------------|:--------------------:|:-------------------:|----------------------------------------------------------------------------------------------------------------------:|\n| `register_decider_event`          |  :heavy_check_mark:  |         :x:         |                                                                Register a decider and event types that it can publish |\n| `append_event`                    |  :heavy_check_mark:  |         :x:         |                                                                Append/Insert new event to the database `events` table |\n| `get_events`                      |  :heavy_check_mark:  |         :x:         |                                                                                       Get/List events for the decider |\n| `get_last_event`                 |  :heavy_check_mark:  |         :x:         |                                                                                        Get last event for the decider |\n| `register_view`                   |         :x:          | :heavy_check_mark:  |                                                                                   Register a view to stream events to |\n| `stream_events`                   |         :x:          | :heavy_check_mark:  |                                                                        Stream events to the view/concurrent consumers |\n| `ack_event`                       |         :x:          | :heavy_check_mark:  |                           Acknowledge that event with `decider_id` and `offset` is successfully processed by the view |\n| `nack_event`                      |         :x:          | :heavy_check_mark:  |             Acknowledge that event with `decider_id` is NOT processed by the view, and the view will process it again |\n| `schedule_nack_event`             |         :x:          | :heavy_check_mark:  | Acknowledge that event with `decider_id` is NOT processed by the view, and the view will process it again after delay |\n| `scedule_events` (cron extension) |         :x:          | :heavy_check_mark:  |                                                                                       Schedule events to be published |\n\n\n\n## Run Postgres\n\nIt is a Supabase Docker image of Postgres, with extensions installed:\n\n - [`pg_cron`](https://github.com/citusdata/pg_cron) and \n - [`pg_net`](https://github.com/supabase/pg_net).\n\n### Requirements\n\nNotice that we only need these two extensions to publish events to edge-functions/HTTP endpoints/serverless applications, as explained in section `6b` below.\nIf you do not need to publish events directly to your serverless applications, **vanilla Postgres will work just fine!**\n\nYou can run the following command to start Postgres in a Docker container:\n\n```shell\ndocker compose up -d\n```\n\n\n## Examples of usage\n\nThese examples are using SQL to interact with the database. Hopefully, you will find them useful, and you can use them in your application.\n\nImport the [schema.sql (imported by default)](schema.sql) and [extensions.sql (not imported!)](extensions.sql) into your database.\n\n\n### Event Sourcing\n\n#### 1. Registering a simple decider `decider1` with two event types it can publish: 'event1', 'event2'\n\nThe `deciders` table controls the decider and event names/types that can be used in the events table itself through composite foreign keys.\nIt must be populated before events can be appended to the main table called `events`.\n\n```sql\nSELECT *\nfrom register_decider_event('decider1', 'event1', 'description1', 1);\nSELECT *\nfrom register_decider_event('decider1', 'event2', 'description2', 1);\n```\n\n#### 2. Appending two events for the decider `f156a3c4-9bd8-11ed-a8fc-0242ac120002`.\n\nMultiple constraints are applied to `events` table to ensure bad events do not make their way into the system.\nThis includes duplicated events, incorrect naming (event and decider names cannot be misspelled, and the client cannot insert an event from the wrong decider), ensured sequential events, disallowed delete, and disallowed update.\n\n\u003e Notice how `previous_id` of the second event points to `event_id` of the first event (effectively implementing optimistic locking).\n\n```sql\nSELECT *\nfrom append_event('event1', '21e19516-9bda-11ed-a8fc-0242ac120002', 'decider1', 'f156a3c4-9bd8-11ed-a8fc-0242ac120002',\n                  '{}', 'f156a3c4-9bd8-11ed-a8fc-0242ac120002', null, 1);\nSELECT *\nfrom append_event('event2', 'eb411c34-9d64-11ed-a8fc-0242ac120002', 'decider1', 'f156a3c4-9bd8-11ed-a8fc-0242ac120002',\n                  '{}', 'f156a3c4-9bd8-11ed-a8fc-0242ac120002', '21e19516-9bda-11ed-a8fc-0242ac120002', 1);\n```\n\n#### 3. Get/List events for the decider `f156a3c4-9bd8-11ed-a8fc-0242ac120002`\n\n```sql\nSELECT *\nfrom get_events('f156a3c4-9bd8-11ed-a8fc-0242ac120002', 'decider1');\n```\n\n### Event Streaming\n\n#### 4. Registering a (materialized) view `view1` with 1 second pooling frequency, starting from 28th Jan.\n\nThe `View` must be registered before events can be streamed to it.\nThis streaming is kafka-like, in that it is modeling the concept of partitions and offsets.\nEvery unique stream of events for the one deciderId/entityId is a partition. \n`Lock` table is used to prevent concurrent access/reading to the same partition, guaranteeing that only one consumer can read from a partition at a time / guaranteeing the ordering within the partition on the reading side.\n\nYou can configure the `view` to publish event(s) every 1 second, starting from 28th Jan, 2023 with lock/ACK timeout of 300 seconds (if you dont acknowledge that you processed the event in 300 sec, the lock will be released and event will be published again, automatically).\n\n\n\u003e Notice how `lock` for the two events with `decider_id`=`f156a3c4-9bd8-11ed-a8fc-0242ac120002` is created in the\n\u003e background (using triggers).\n\n\n```sql\nSELECT *\nfrom register_view('view1', '2023-01-28 12:17:17.078384', 300, 1, 'https://localhost:3000/functions/v1/event-handler');\n```\n\n#### 5. Appending two events for another decider `2ac37f68-9d66-11ed-a8fc-0242ac120002`.\n\nThe alone existence of the View is changing how `append_event` works. It is now creating a new event, but also updating a lock table.\n\n - `offset` / current offset of the event stream for `decider_id`\n - `offset_final` / an indicator if the offset is final / offset will not grow anymore\n\n\u003e Notice how `previous_id` of the second event is pointing to `event_id` of the first event.\n\n\u003e Notice how additional `lock` for the registered view and two new events\n\u003e with `decider_id`=`2ac37f68-9d66-11ed-a8fc-0242ac120002` created in the background (using triggers).\n\n```sql\nSELECT *\nfrom append_event('event1', 'f7c370aa-9d65-11ed-a8fc-0242ac120002', 'decider1', '2ac37f68-9d66-11ed-a8fc-0242ac120002',\n                  '{}', 'f156a3c4-9bd8-11ed-a8fc-0242ac120002', null, 1);\nSELECT *\nfrom append_event('event2', '42ee177e-9d66-11ed-a8fc-0242ac120002', 'decider1', '2ac37f68-9d66-11ed-a8fc-0242ac120002',\n                  '{}', 'f156a3c4-9bd8-11ed-a8fc-0242ac120002', 'f7c370aa-9d65-11ed-a8fc-0242ac120002', 1);\n```\n\n#### 6a. Stream the events to concurrent consumers/views\n\n`stream_events` function is used to stream events to the view.\nOn every event being read a lock table is updated to acquire a lock on that partition.\nYou can:\n\n - unlock the partition with `ack-event` function / acknowledge that the event with `decider_id` and `offset` is processed by the view\n - unlock the partition with `nack-event` function / acknowledge that the event with `decider_id` is NOT processed by the view, and the view should try to process it again / offset is not updated\n - schedule the partition for retry with `schedule_nack_event` function / acknowledge that the event with `decider_id` is NOT processed by the view, and the view should try to process it again after some time/offset is not updated\n\n\u003e Notice that this query can run in a loop within your application. \n\n\n```sql\n-- Get first 100 events \nSELECT * from stream_events('view1', 100);\n\nSELECT * from ack_event('view1', 'f156a3c4-9bd8-11ed-a8fc-0242ac120002', 1);\n\n-- ACK other 99 events, and call `stream_events` again to get the next 100 events.\n-- If you do not ACK the events in 300 seconds as configured on the `view` table, they will be processed again on the next call to `stream_events`.\n```\n\n#### 6b. Stream the events to concurrent consumers / edge-functions (views)\n\nImport the [extensions.sql](extensions.sql) into your database.\n\nIt is very similar to the `6a` case. The difference is that the cron job will run `SELECT * from stream_events('view1');` for you, and publish event(s) to your edge-functions/http endpoints automatically. So, the database is doing all the job. \n\nThe `cron` job is managed(created/deleted) by triggers on the `view` table. So, whenever you register a new View, the cron job will be created automatically.\n\n\n## Design\n\nThe SQL functions and schema we provide will help you to persist, query, and stream events in a robust way, but the\n**decision-making** and **view-handling** logic would be something that you would have to implement on your own.\n\n - The decision-making process is a **command handler** responsible for handling the command/intent and producing new events/facts that can be saved in the database by using `append_event` SQL function. Command handler can be implemented in any programming language, Kotlin, TypeScript, Rust, ...\n   - We call this function a **decide**.\n   - You can run it as an edge function on [Supabase](https://supabase.com/docs/guides/functions) or [Deno](https://deno.com/deploy).\n\n![event-sourcing](.assets/event-sourcing.png)\n\n - The view-handling process is an **event handler** that is responsible for handling the event/fact and producing a new view/query model. Event handler uses `stream_events` SQL function from your application to fetch/pool events, or `stream_events` SQL function is triggered by the cron job on the DB side and event(s) are published/pushed to your event handlers/HTTP endpoints/edge functions. \n   - We call this function an **evolve**.\n   - You can run it as an edge function on [Supabase](https://supabase.com/docs/guides/functions) or [Deno](https://deno.com/deploy).\n   - `pg_crone` and `pg_net` extensions are used to schedule the event publishing process and send the HTTP request/`event` to the edge function (view).\n\n![event-streaming](.assets/event-streaming.png)\n\n\n## fmodel\n\n#### [fmodel-kotlin](https://github.com/fraktalio/fmodel) | [fmodel-ts](https://github.com/fraktalio/fmodel-ts) | [fmodel-rust](https://github.com/fraktalio/fmodel-rust) | [fmodel-java](https://github.com/fraktalio/fmodel-java)\n\n'fmodel' is a set of libraries that aims to bring functional, algebraic, and reactive domain modeling to Kotlin / TypeScript / Rust / Java. It is inspired by DDD, EventSourcing, and Functional programming communities.\n\n💙 Accelerate the development of compositional, ergonomic, data-driven, and safe applications 💙\n\n| Command                                                                                                |                                                   Event                                                   |                                                                                                          State |\n|:-------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------:|---------------------------------------------------------------------------------------------------------------:|\n| An intent to change the state of the system                                                            |            The state change itself, a fact. It represents a decision that has already happened            |                                             The current state of the system. It has evolved out of past events |\n| ![command](.assets/command.svg)                                                                        |                                        ![event](.assets/event.svg)                                        |                                                                                    ![state](.assets/state.svg) |\n| -                                                                                                      |                                                     -                                                     |                                                                                                              - |\n| Decide                                                                                                 |                                                  Evolve                                                   |                                                                                                          React |\n| A pure function that takes command and current state as parameters, and returns the flow of new events | A pure function that takes event and current state as parameters, and returns the new state of the system | A pure function that takes event as parameter, and returns the flow of commands, deciding what to execute next |\n| ![decide](.assets/decide.svg)                                                                          |                                       ![evolve](.assets/evolve.svg)                                       |                                                                              ![react](.assets/orchestrate.svg) |\n\n#### FModel Demo Applications\n|        |                                                                      Event-Sourced                                                                       | State-Stored   |\n| :---   |:--------------------------------------------------------------------------------------------------------------------------------------------------------:|     :---:      |\n| `Kotlin` (Spring) |                                          [fmodel-spring-demo](https://github.com/fraktalio/fmodel-spring-demo)                                           | [fmodel-spring-state-stored-demo](https://github.com/fraktalio/fmodel-spring-state-stored-demo) |\n| `Kotlin`(Ktor)   |                                            [fmodel-ktor-demo](https://github.com/fraktalio/fmodel-ktor-demo)                                             |    todo     |\n| `TypeScript`     |                                                                           todo                                                                           |    todo     |\n| `Rust`           | [fmodel-rust-demo](https://github.com/fraktalio/fmodel-rust-demo) |    todo     |\n\n\n\n## Try YugabyteDB\n\nAlternatively, you can use YugabyteDB instead of Postgres. It is fully compatible with Postgres.\n\nYugabyteDB is a high-performance, cloud-native distributed SQL database that aims to support all Postgres features. It\nis best fit for cloud-native OLTP (i.e. real-time, business-critical) applications that need absolute data correctness\nand require at least one of the following: scalability, high tolerance to failures, and globally distributed deployments.\n\n\nYou can [download](https://docs.yugabyte.com/preview/quick-start/install) as ready-to-use packages or installers for\nvarious platforms.\n\n```shell\n./bin/yugabyted start --master_flags=ysql_sequence_cache_minval=0 --tserver_flags=ysql_sequence_cache_minval=0\n```\n\nAlternatively, you can run the following command\nto [start YugabyteDB in a Docker](https://docs.yugabyte.com/preview/quick-start/create-local-cluster/docker/) container:\n\n```shell\ndocker run -d --name yugabyte  -p7000:7000 -p9000:9000 -p5433:5433 -p9042:9042\\\n yugabytedb/yugabyte:latest bin/yugabyted start\\\n --daemon=false --master_flags=ysql_sequence_cache_minval=0 --tserver_flags=ysql_sequence_cache_minval=0\n```\n\n\n## References and further reading\n\n- (Marco Pegoraro) https://github.com/marcopeg/postgres-event-sourcing\n- (Matt Bishop) https://github.com/mattbishop/sql-event-store\n- [FModel](https://fraktalio.com/fmodel/)\n- [Supabase](https://supabase.io/)\n\n---\nCreated with :heart: by [Fraktalio](https://fraktalio.com/) \n\nExcited to launch your next IT project with us? Let's get started! Reach out to our team at `info@fraktalio.com` to begin the journey to success.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffraktalio%2Ffstore-sql","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffraktalio%2Ffstore-sql","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffraktalio%2Ffstore-sql/lists"}