{"id":15011233,"url":"https://github.com/bard/tiny-event-sourcing","last_synced_at":"2026-04-02T01:12:51.758Z","repository":{"id":57377240,"uuid":"286290299","full_name":"bard/tiny-event-sourcing","owner":"bard","description":"Minimalist zero-setup event sourcing infrastructure geared at prototyping and early data model exploration","archived":false,"fork":false,"pushed_at":"2020-08-12T21:39:01.000Z","size":141,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-03-11T15:50:51.357Z","etag":null,"topics":["event-sourcing","node","prototyping","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bard.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-08-09T18:02:44.000Z","updated_at":"2021-03-26T15:28:20.000Z","dependencies_parsed_at":"2022-09-18T11:51:05.053Z","dependency_job_id":null,"html_url":"https://github.com/bard/tiny-event-sourcing","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/bard/tiny-event-sourcing","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bard%2Ftiny-event-sourcing","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bard%2Ftiny-event-sourcing/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bard%2Ftiny-event-sourcing/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bard%2Ftiny-event-sourcing/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bard","download_url":"https://codeload.github.com/bard/tiny-event-sourcing/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bard%2Ftiny-event-sourcing/sbom","scorecard":{"id":225756,"data":{"date":"2025-08-11","repo":{"name":"github.com/bard/tiny-event-sourcing","commit":"1e7ef543ccb89d6017e384bcebda33a56b711eed"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":1.3,"checks":[{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Code-Review","score":0,"reason":"Found 0/21 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":0,"reason":"license file not detected","details":["Warn: project does not have a license file"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Vulnerabilities","score":0,"reason":"54 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-968p-4wvh-cqc8","Warn: Project is vulnerable to: GHSA-67hx-6x53-jw92","Warn: Project is vulnerable to: GHSA-h5c3-5r3r-rr8q","Warn: Project is vulnerable to: GHSA-rmvr-2pp2-xj38","Warn: Project is vulnerable to: GHSA-xx4v-prfh-6cgc","Warn: Project is vulnerable to: GHSA-93q8-gq69-wqmw","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg","Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275","Warn: Project is vulnerable to: GHSA-gxpj-cx7g-858c","Warn: Project is vulnerable to: GHSA-w573-4hg7-7wgq","Warn: Project is vulnerable to: GHSA-2j2x-2gpw-g8fm","Warn: Project is vulnerable to: GHSA-fjxv-7rqg-78g4","Warn: Project is vulnerable to: GHSA-ww39-953v-wcq6","Warn: Project is vulnerable to: GHSA-pfrx-2q88-qq97","Warn: Project is vulnerable to: GHSA-43f8-2h32-f4cj","Warn: Project is vulnerable to: GHSA-rc47-6667-2j5j","Warn: Project is vulnerable to: GHSA-qqgx-2p2h-9c37","Warn: Project is vulnerable to: GHSA-896r-f27r-55mw","Warn: Project is vulnerable to: GHSA-9c47-m6qq-7p4h","Warn: Project is vulnerable to: GHSA-29mw-wpgm-hmr9","Warn: Project is vulnerable to: GHSA-35jh-r3h4-6jhm","Warn: Project is vulnerable to: GHSA-952p-6rrq-rcjv","Warn: Project is vulnerable to: GHSA-f8q6-p94x-37v3","Warn: Project is vulnerable to: GHSA-xvch-5gv4-984h","Warn: Project is vulnerable to: GHSA-w7rc-rwvf-8q5r","Warn: Project is vulnerable to: GHSA-r683-j2x4-v87g","Warn: Project is vulnerable to: GHSA-5fw9-fq32-wv5p","Warn: Project is vulnerable to: GHSA-px4h-xg32-q955","Warn: Project is vulnerable to: GHSA-3j8f-xvm3-ffx4","Warn: Project is vulnerable to: GHSA-4p35-cfcx-8653","Warn: Project is vulnerable to: GHSA-7f3x-x4pr-wqhj","Warn: Project is vulnerable to: GHSA-jpp7-7chh-cf67","Warn: Project is vulnerable to: GHSA-q6wq-5p59-983w","Warn: Project is vulnerable to: GHSA-j9fq-vwqv-2fm2","Warn: Project is vulnerable to: GHSA-pqw5-jmp5-px4v","Warn: Project is vulnerable to: GHSA-hj48-42vr-x3v9","Warn: Project is vulnerable to: GHSA-hrpp-h998-j3pp","Warn: Project is vulnerable to: GHSA-p8p7-x288-28g6","Warn: Project is vulnerable to: GHSA-c2qf-rxjj-qqgw","Warn: Project is vulnerable to: GHSA-44c6-4v22-4mhx","Warn: Project is vulnerable to: GHSA-4x5v-gmq8-25ch","Warn: Project is vulnerable to: GHSA-4rq4-32rv-6wp6","Warn: Project is vulnerable to: GHSA-64g7-mvw6-v9qj","Warn: Project is vulnerable to: GHSA-52f5-9888-hmc6","Warn: Project is vulnerable to: GHSA-jgrx-mgxx-jf9v","Warn: Project is vulnerable to: GHSA-72xf-g2v4-qvf3","Warn: Project is vulnerable to: GHSA-7p7h-4mm5-852v","Warn: Project is vulnerable to: GHSA-38fc-wpqx-33j7","Warn: Project is vulnerable to: GHSA-j8xg-fqg3-53r7","Warn: Project is vulnerable to: GHSA-6fc8-4gx4-v693","Warn: Project is vulnerable to: GHSA-3h5v-q93c-6h6q","Warn: Project is vulnerable to: GHSA-c4w7-xm78-47vh","Warn: Project is vulnerable to: GHSA-p9pc-299p-vxgp"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-17T03:46:41.748Z","repository_id":57377240,"created_at":"2025-08-17T03:46:41.748Z","updated_at":"2025-08-17T03:46:41.748Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31293682,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-02T01:05:07.454Z","status":"ssl_error","status_checked_at":"2026-04-02T00:56:46.496Z","response_time":53,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: 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","node","prototyping","typescript"],"created_at":"2024-09-24T19:39:49.118Z","updated_at":"2026-04-02T01:12:51.729Z","avatar_url":"https://github.com/bard.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Tiny Event Sourcing\n\nMinimalist zero-setup event sourcing infrastructure geared at prototyping and early data model exploration.\n\n## Motivation\n\nIn the early stages of developing a service, you're probably not sure what data to capture and what to leave out. If you capture a lot just in case, figuring where it all fits in the data model (and each of its iterations) becomes a burden that is hard to justify for potentially useless information; if you leave out a lot, you might later realize you needed it.\n\nThe Event Sourcing architecture has a property that is invaluable at this stage: it allows to capture information and delay organizing it until it's needed. Everything that happens in the system is represented as _events_ that are appended to a durable and immutable _log_. _Subscribers_ receive events and build _read models_ based on them. Read models are queried to e.g. present information to the user.\n\nAt any point in development, you can stop the system, modify the event subscribers so that they start using a dormant piece of data, replay the log, let subscribers rebuild the read models, and **it will be as if you had known how to use that information since day #1**.\n\nThis library lets you quickly structure a prototype around Event Sourcing. It has zero runtime dependencies on other services (saves both log and state snapshots to disk files) and makes no attempt at being suitable for production (read models are kept entirely in memory and there is no log compaction).\n\n## Installation\n\n```sh\nyarn add tiny-event-sourcing\n```\n\nOr:\n\n```sh\nnpm install tiny-event-sourcing\n```\n\n## How to use\n\nThe 10000ft view:\n\n1. define one or more domain events\n2. initialize the event log\n3. define one or more read models\n4. initialize a state store for each read model\n5. process past and future events\n6. get events into the system\n\nSay you're building a bookmarking service; user can POST web page URLs; the service fetches additional information such as title and favicon URLs. Here's how it could look like (in TypeScript — ignore types if you're using plain JavaScript):\n\n### 1. Define domain events\n\n```typescript\ninterface BookmarkEvent {\n  type: 'bookmark'\n  url: string\n}\n\ninterface InfoEvent {\n  type: 'info'\n  url: string\n  title: string\n  faviconUrl: string\n}\n\ntype DomainEvent = BookmarkEvent | InfoEvent\n```\n\n### 2. Initialize the event log\n\n```typescript\nimport { initLog } from 'tiny-event-sourcing'\n\nconst log = await initLog\u003cDomainEvent\u003e({\n  filename: '/tmp/bookmark-service/log.ndjson',\n})\n```\n\n### 3. Define read models\n\n```typescript\ninterface BookmarksReadModel {\n  byUrl: {\n    [url: string]: {\n      url: string\n      title: string\n      faviconUrl: string\n    }\n  }\n}\n\ninterface InfoFetcherReadModel {\n  // no specific state here\n}\n```\n\n### 4. Initialize state stores for each read model\n\n```typescript\nimport { initStateStore, fsStoreBackend } from 'tiny-event-sourcing'\n\nconst bookmarksReadModel = await initStateStore\u003cBookmarksReadModel\u003e({\n  storeBackend: fsStoreBackend\u003cBoomarksReadModel\u003e(\n    '/tmp/example/read-model.json',\n    { byUrl: {} },\n  ),\n})\n\nconst infoFetcherReadModel = await initStateStore\u003cInfoFetcherReadModel\u003e({\n  storeBackend: fsStoreBackend\u003cInfoFetcherModel\u003e(\n    '/tmp/example/info-fetcher.json',\n    {},\n  ),\n})\n```\n\n### 5. Process events\n\n```typescript\nimport { catchupSubscribe } from 'tiny-event-sourcing'\nimport { fetchPageInfo } from 'some-http-library'\n\n// When a \"bookmark\" event is detected, retrieve page information\n// and trigger an \"info\" event.\n\ncatchupSubscribe(log.events(), async (event) =\u003e {\n  // Skip already processed events\n\n  if (bookmarksReadModel.getVersion() \u003c= event.index) {\n    return\n  }\n\n  // Events of type \"bookmark\" will come from a REST API\n  // endpoint below\n\n  if (event.type === 'bookmark') {\n    const { url } = event\n    const { title, faviconUrl } = await fetchPageInfo(url)\n\n    // Append the page information to the log so it can\n    // be processed\n\n    log.append({ type: 'info', url, title, faviconUrl })\n\n    // The info fetcher isn't storing any state so just bump the version\n\n    infoFetcherReadModel.update(infoFetcherReadModel.getState(), event.index)\n  }\n})\n\n// When an \"info\" event is detected, write to the bookmark\n// read model.\n\ncatchupSubscribe(log.events(), async (event) =\u003e {\n  if (readModel.getVersion() \u003c= event.index) {\n    return\n  }\n\n  if (event.type === 'info') {\n    const oldState = readModel.getState()\n\n    // Create new state based on old state plus event\n\n    const newState = {\n      ...oldState,\n      byUrl: {\n        ...oldState.byUrl,\n        [event.url]: {\n          url: event.url,\n          title: event.title,\n          faviconUrl: event.faviconUrl,\n        },\n      },\n    }\n\n    // Store new state and bump version\n\n    readModel.update(newState, event.index)\n  }\n})\n```\n\n### 6. Get events into the system\n\nEvents of type `info` are already generated above, but `bookmark` events are what sets everything in motion:\n\n```typescript\nimport express from 'express'\n\nconst app = express()\n\napp.post('/bookmarks', express.json(), (req, res) =\u003e {\n  log.append({ type: 'bookmark', url: req.body.url })\n  res.status(202).end()\n})\n```\n\n## Tips\n\nThe [jq](https://stedolan.github.io/jq/) tool comes in handy when you're experimenting with event format and need to modify events already stored in the log. Use the `-c` flag to output ndjson format.\n\nThe [Flux Standard Action](https://github.com/redux-utilities/flux-standard-action#flux-standard-action) format works well for events: instead of `interface BookmarkEvent { type: 'bookmark', url: string }` as per the simplistic example above, consider using `interface BookmarkEvent { type: 'bookmark', payload: { url: string }}`.\n\n`catchupSubscribe` is just a convenience function over the `log.events()` asynchronous iterator. If you want finer control over pulling past events and listening for new ones, you can use the full form: `for await(const event of events) { ... }`.\n\nState files are just envelopes around the actual state data. Currently,l they only contain a `_version` field: `{ \"_version\": 123, \"state\": { ... }}`. The version doesn't increment uniformly, instead it reflects the index of the last processed event.\n\n## License\n\nMIT\n\n## Author\n\n[Massimiliano Mirra](https://massimilianomirra.com/)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbard%2Ftiny-event-sourcing","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbard%2Ftiny-event-sourcing","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbard%2Ftiny-event-sourcing/lists"}