Git Storage
Every piece of corporate data — formation documents, cap tables, governance records, financial transactions — lives in a bare git repository. No PostgreSQL. No Redis by default. Just git.
Why git
Section titled “Why git”| Problem | Git solution |
|---|---|
| Immutable audit trail | Every change is a commit with SHA hash, timestamp, and author |
| Atomic operations | Multi-file commits are all-or-nothing |
| Branching workflows | Draft resolutions on branches, merge to main when approved |
| Standard tooling | Clone, inspect, backup, and mirror with git CLI |
| No migrations | JSON files in a tree — schema changes are just new fields |
| Data portability | git clone gives you everything, forever |
Git library: gix (pure Rust)
Section titled “Git library: gix (pure Rust)”The storage layer uses gix (v0.72), a pure-Rust git implementation. There is no dependency on libgit2 or any C library.
All gix operations are synchronous. The storage layer wraps them in tokio::task::spawn_blocking so they never block the async runtime:
// Read a file from the main branchtokio::task::spawn_blocking(move || { crate::git::read_file(&repo_path, "main", "formation/entity/{id}.json")}).await?Storage backends
Section titled “Storage backends”corp-storage supports three backends, selected at compile time via feature flags:
| Backend | Feature | Use case |
|---|---|---|
| Bare git repos | git | Default; everything on local filesystem |
| Redis/Valkey | kv | Low-latency reads, cloud-native deployments |
| S3-compatible | s3 | Large blob storage, offloaded from git |
The production deployment uses git + kv + s3 (all three features enabled). The default and recommended self-hosted configuration uses git only.
On-disk layout
Section titled “On-disk layout”Each workspace has a directory. Entity repos and the workspace repo live under it:
{data_dir}/{workspace_id}/├── workspace/ # Workspace-level bare git repo│ └── (HEAD, objects/, refs/, ...)│ # Contains: workspace/api_keys/{key_id}.json│ # workspace/entities.json ← entity ID index└── entities/ ├── {entity_id_1}/ # First entity (e.g., an LLC) — bare git repo │ └── (HEAD, objects/, refs/, ...) ├── {entity_id_2}/ # Second entity (e.g., a C-Corp) └── ...In production, {data_dir} is the git_data Docker volume (typically /data/repos).
The workspace store is managed by WorkspaceStore in corp-storage. It holds the entity ID index (workspace/entities.json) and API key records. The entity stores are managed by EntityStore.
Entity repo tree
Section titled “Entity repo tree”Inside each entity’s bare repo, the tree at refs/heads/main follows the StoredEntity storage paths defined in corp-storage/src/impls.rs:
formation/ entity/ {entity_id}.json # Entity metadata (name, type, jurisdiction, status) documents/ {document_id}.json # Formation documents with SHA-256 content hashes filings/ {filing_id}.json # State filings (certificate of incorporation, etc.) tax/ {tax_profile_id}.json # IRS tax classification, EINcontacts/ {contact_id}.jsonequity/ cap_tables/ {cap_table_id}.json instruments/ {instrument_id}.json grants/ {grant_id}.json safes/ {safe_id}.json valuations/ {valuation_id}.json transfers/ {transfer_id}.json rounds/ {round_id}.json holders/ {holder_id}.jsongovernance/ bodies/ {body_id}.json seats/ {seat_id}.json meetings/ {meeting_id}.json agenda_items/ {agenda_item_id}.json votes/ {vote_id}.json resolutions/ {resolution_id}.jsontreasury/ accounts/ {account_id}.json journal_entries/ {entry_id}.json invoices/ {invoice_id}.json payments/ {payment_id}.json bank_accounts/ {bank_account_id}.json payroll_runs/ {run_id}.json reconciliations/ {reconciliation_id}.jsonexecution/ intents/ {intent_id}.json obligations/ {obligation_id}.json receipts/ {receipt_id}.jsonagents/ {agent_id}.jsonwork_items/ {work_item_id}.jsonservices/ requests/ {service_request_id}.jsonEvery file is JSON. Every change is a git commit. The paths come directly from StoredEntity::storage_dir() implementations — the same trait that route handlers use when calling store.read::<Entity>() and store.write::<Entity>().
Read operations
Section titled “Read operations”Reads are tree lookups — resolve a ref, walk the tree, read a blob:
// Read a typed entity from the main branchlet entity = store.read::<Entity>(entity_id, "main").await?;
// Read all grants for an entitylet grants: Vec<EquityGrant> = store.read_all::<EquityGrant>("main").await?;
// Read an arbitrary JSON pathlet value: MyType = store.read_json("some/path.json", "main").await?;No query language, no indexes. The data is the file tree.
Write operations
Section titled “Write operations”Writes go through write_files(), which atomically overlays new files onto the existing tree:
// Write a typed entitystore.write::<Entity>(&entity, entity_id, "main", "create entity").await?;
// Write multiple files atomically (low-level)crate::git::write_files( &repo_path, "main", &[ ("formation/entity/{id}.json".to_owned(), entity_bytes), ("execution/receipts/{id}.json".to_owned(), receipt_bytes), ], "Create entity and receipt",)?;This creates a single commit that updates both files atomically.
Branch workflows
Section titled “Branch workflows”Branches enable draft-then-approve workflows:
- Draft — create a branch (e.g.,
drafts/resolution-2026-03) and commit proposed changes - Review — humans or agents inspect the branch
- Merge — merge to
mainwhen approved
The API supports branch targeting via the X-Corp-Branch header. Endpoints default to main.
Data portability
Section titled “Data portability”Because everything is standard git:
# Clone an entity's full historygit clone /data/repos/{workspace_id}/entities/{entity_id}/
# Back up the entire workspacetar czf backup.tar.gz /data/repos/{workspace_id}/
# Mirror to a remotegit remote add backup git@backup-server:{workspace_id}/entities/{entity_id}.gitgit push backup --mirrorYour corporate data is never locked in. It’s git repos you can read with any tool that speaks git.