{"id":51292999,"url":"https://github.com/verberktstan/bitten","last_synced_at":"2026-06-30T12:02:14.097Z","repository":{"id":362140128,"uuid":"1257364558","full_name":"verberktstan/bitten","owner":"verberktstan","description":"Bi-temporal append-only DB server","archived":false,"fork":false,"pushed_at":"2026-06-10T19:17:36.000Z","size":81,"stargazers_count":1,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-10T21:09:20.060Z","etag":null,"topics":["bi-temporal","clojure","database","db","event-driven","server","sqlite"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"epl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/verberktstan.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-06-02T15:57:00.000Z","updated_at":"2026-06-10T19:21:25.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/verberktstan/bitten","commit_stats":null,"previous_names":["verberktstan/bitten"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/verberktstan/bitten","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/verberktstan%2Fbitten","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/verberktstan%2Fbitten/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/verberktstan%2Fbitten/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/verberktstan%2Fbitten/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/verberktstan","download_url":"https://codeload.github.com/verberktstan/bitten/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/verberktstan%2Fbitten/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34965643,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-30T02:00:05.919Z","response_time":92,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["bi-temporal","clojure","database","db","event-driven","server","sqlite"],"created_at":"2026-06-30T12:02:13.414Z","updated_at":"2026-06-30T12:02:14.089Z","avatar_url":"https://github.com/verberktstan.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Bitten\n\n**Bitten** is a portmanteau of **bi-temporal** in the past tense. It is a small, append-only database server that tracks two independent time axes for every fact it stores.\n\n## What it does\n\nSome databases record what is true *right now*. Bitten records:\n\n- **Valid time** - when a fact was true in the real world (e.g. a contract started on 1 January, even if you didn't enter it until March).\n- **Transaction time** - when the fact was written into the database.\n\nThis means you can ask questions like *\"what did we know about user Alice on 1 June, as of the snapshot we had in September?\"*, and get a deterministic answer even after retroactive corrections have been applied.\n\nFacts are never updated or deleted. Every change is a new row; retractions are explicit. The log is the truth.\n\n## Rationale\n\nBitten makes audit trails and temporal queries structural: the schema enforces append-only writes, and every query is implicitly bi-temporal. There is no separate audit table to maintain or forget to update.\n\nThe implementation is intentionally small; a single SQLite table. The core model stays legible and the storage backend can be swapped without touching application logic.\n\n## Stack\n\n- **[Babashka](https://babashka.org/)** - GraalVM-native Clojure scripting; fast startup, no JVM warm-up.\n- **SQLite** via the `org.babashka/go-sqlite3` pod - embedded, zero-infrastructure persistence.\n- **EDN over TCP** - newline-delimited protocol; any EDN-capable client can talk to the server.\n\n## Running\n\n```bash\nbb start                   # default: port 5432, db file bitten.db\nPORT=6432 bb start         # custom port\nDB_PATH=/data/my.db bb start   # custom db path\n```\n\nThe server prints a ready line and blocks until interrupted (`Ctrl-C`). The database file is created and migrated automatically on first start.\n\n### Wire protocol\n\nOne EDN map per line in, one EDN map per line out.\n\n**Transact** - upsert one or more records. Each record is a flat map with `:db/entity` plus attribute keys. Only changed attributes are written.\n\n```edn\n{:op :transact :records [{:db/entity \"user/1\" :user/name \"Alice\" :user/email \"a@example.com\"}]}\n;; =\u003e {:status :ok :data 1}   ; data is the tx-id, nil for a no-op\n```\n\n**Query** - return live facts for an entity as a flat map.\n\n```edn\n{:op :query :e \"user/1\"}\n{:op :query :e \"user/1\" :as-of-valid \"2024-06-01\"}\n;; =\u003e {:status :ok :data ({:db/entity \"user/1\" :user/name \"Alice\" ...})}\n```\n\n**Ping**\n\n```edn\n{:op :ping}\n;; =\u003e {:status :ok :data :pong}\n```\n\nErrors return `{:status :error :message \"...\"}`.\n\n### Logic layer (`src/db.clj`)\n\n| Function | Description |\n|---|---|\n| `query` | Returns live facts as a seq of flat maps, each with `:db/entity`. |\n| `upsert!` | Writes only the attributes that changed; optionally retracts attributes absent from the incoming record (`:missing-keys :retract`). |\n| `retract!` | Retracts all live facts for a seq of entity IDs. Inserts one retraction row per `(entity, attribute, value)` triple. Entities that are already retracted or do not exist are silently skipped. Returns the tx-id, or `nil` for a no-op. |\n\nStorage is abstracted behind an `IStorage` protocol (`src/storage.clj`). The SQLite implementation lives in `src/sqlite.clj`. To add a new backend, implement the three-method protocol in a new file. No changes to `src/db.clj` or the server are needed.\n\n## Testing\n\n```bash\nbb test\n```\n\nTests use real SQLite temp files (one per test, cleaned up in a `finally` block). There is no mocking of the storage layer.\n\n## Development\n\nFor Emacs/CIDER, use `M-x cider-jack-in-universal` to start a Babashka REPL for development purposes. It handles launching and connecting in one step.\n\nTo start a nREPL server directly:\n\n```bash\nbb nrepl-server 1667\n```\n\nOr use the project task, which additionally writes a `.nrepl-port` file so CIDER can auto-detect the port:\n\n```bash\nbb nrepl\n```\n\n## License\n\nCopyright © 2026 Stan Verberkt\n\nDistributed under the EPL License. See LICENSE.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fverberktstan%2Fbitten","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fverberktstan%2Fbitten","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fverberktstan%2Fbitten/lists"}