Skip to content

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.

ProblemGit solution
Immutable audit trailEvery change is a commit with SHA hash, timestamp, and author
Atomic operationsMulti-file commits are all-or-nothing
Branching workflowsDraft resolutions on branches, merge to main when approved
Standard toolingClone, inspect, backup, and mirror with git CLI
No migrationsJSON files in a tree — schema changes are just new fields
Data portabilitygit clone gives you everything, forever

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 branch
tokio::task::spawn_blocking(move || {
crate::git::read_file(&repo_path, "main", "formation/entity/{id}.json")
}).await?

corp-storage supports three backends, selected at compile time via feature flags:

BackendFeatureUse case
Bare git reposgitDefault; everything on local filesystem
Redis/ValkeykvLow-latency reads, cloud-native deployments
S3-compatibles3Large 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.

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.

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, EIN
contacts/
{contact_id}.json
equity/
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}.json
governance/
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}.json
treasury/
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}.json
execution/
intents/
{intent_id}.json
obligations/
{obligation_id}.json
receipts/
{receipt_id}.json
agents/
{agent_id}.json
work_items/
{work_item_id}.json
services/
requests/
{service_request_id}.json

Every 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>().

Reads are tree lookups — resolve a ref, walk the tree, read a blob:

// Read a typed entity from the main branch
let entity = store.read::<Entity>(entity_id, "main").await?;
// Read all grants for an entity
let grants: Vec<EquityGrant> = store.read_all::<EquityGrant>("main").await?;
// Read an arbitrary JSON path
let value: MyType = store.read_json("some/path.json", "main").await?;

No query language, no indexes. The data is the file tree.

Writes go through write_files(), which atomically overlays new files onto the existing tree:

// Write a typed entity
store.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.

Branches enable draft-then-approve workflows:

  1. Draft — create a branch (e.g., drafts/resolution-2026-03) and commit proposed changes
  2. Review — humans or agents inspect the branch
  3. Merge — merge to main when approved

The API supports branch targeting via the X-Corp-Branch header. Endpoints default to main.

Because everything is standard git:

Terminal window
# Clone an entity's full history
git clone /data/repos/{workspace_id}/entities/{entity_id}/
# Back up the entire workspace
tar czf backup.tar.gz /data/repos/{workspace_id}/
# Mirror to a remote
git remote add backup git@backup-server:{workspace_id}/entities/{entity_id}.git
git push backup --mirror

Your corporate data is never locked in. It’s git repos you can read with any tool that speaks git.