@pgpm/object-store
Immutable versioned object storage with content-addressable IDs for PostgreSQL
Overview
@pgpm/object-store implements a content-addressable Merkle tree storage system in PostgreSQL. Objects are immutable once frozen, and their IDs are deterministically derived from their content (data + children), similar to how Git stores objects. This enables efficient structural sharing, deduplication, and tamper-evident storage.
Features
- Content-Addressable IDs: Object IDs are deterministically computed from content using UUID v5 hashing
- Merkle Tree Structure: Objects form a tree via
kids/ktreearrays, enabling structural sharing - Immutability: Frozen objects cannot be modified or deleted
- Path-Based Operations: Insert, update, remove, and query nodes using hierarchical paths
- Structural Sharing: Unchanged subtrees are reused across versions (copy-on-write)
- Pure plpgsql: No external dependencies beyond pgcrypto and uuid-ossp
Installation
If you have pgpm installed:
pgpm install @pgpm/object-store
pgpm deploy
This is a quick way to get started. The sections below provide more detailed installation options.
Prerequisites
# Install pgpm CLI
npm install -g pgpm
# Start local Postgres (via Docker) and export env vars
pgpm docker start
eval "$(pgpm env)"
Tip: Already running Postgres? Skip the Docker step and just export your
PG*environment variables.
Add to an Existing Package
# 1. Install the package
pgpm install @pgpm/object-store
# 2. Deploy locally
pgpm deploy
Add to a New Project
# 1. Create a workspace
pgpm init workspace
# 2. Create your first module
cd my-workspace
pgpm init
# 3. Install a package
cd packages/my-module
pgpm install @pgpm/object-store
# 4. Deploy everything
pgpm deploy --createdb --database mydb1
Core Concepts
Object Table
Every object has:
id(uuid) — content-addressable hash computed from data + childrenscope_id(uuid) — scoping identifier for multi-tenant usagedata(jsonb) — the object's payloadkids(uuid[]) — child object IDsktree(text[]) — child names (parallel tokids)frzn(bool) — whether the object is frozen (immutable)
Content-Addressable Hashing
Object IDs are automatically generated via a trigger that hashes the object's data and kids/ktree arrays. Two objects with identical content will always have the same ID, enabling natural deduplication and structural sharing.
Core Functions
object_store_public.insert_node_at_path(scope_id, root, path, data, kids, ktree)
Insert or replace a node at the given path, creating intermediate nodes as needed. Returns the new root ID (since all ancestors are recreated with updated children).
SELECT object_store_public.insert_node_at_path(
s_id := 'aaaaaaaa-...'::uuid,
root := '<root-id>'::uuid,
path := ARRAY['src', 'main.ts']::text[],
data := '{"content": "console.log(hello)"}'::jsonb,
kids := ARRAY[]::uuid[],
ktree := ARRAY[]::text[]
);
object_store_public.get_node_at_path(scope_id, id, path)
Retrieve the object at a given path from a root node.
SELECT * FROM object_store_public.get_node_at_path(
s_id := 'aaaaaaaa-...'::uuid,
id := '<root-id>'::uuid,
path := ARRAY['src', 'main.ts']::text[]
);
object_store_public.remove_node_at_path(scope_id, root, path)
Remove a node at the given path, returning a new root with the node removed.
SELECT object_store_public.remove_node_at_path(
s_id := 'aaaaaaaa-...'::uuid,
root := '<root-id>'::uuid,
path := ARRAY['src', 'old-file.ts']::text[]
);
object_store_public.set_data_at_path(scope_id, root, path, data)
Update the data on an existing node while preserving its children.
SELECT object_store_public.set_data_at_path(
s_id := 'aaaaaaaa-...'::uuid,
root := '<root-id>'::uuid,
path := ARRAY['src', 'main.ts']::text[],
data := '{"content": "updated content"}'::jsonb
);
object_store_public.get_all_objects_from_root(scope_id, id)
Recursively retrieve all objects in the tree starting from a root node.
SELECT * FROM object_store_public.get_all_objects_from_root(
s_id := 'aaaaaaaa-...'::uuid,
id := '<root-id>'::uuid
);
object_store_public.freeze_objects(scope_id, id)
Freeze an object and all its descendants, making them immutable.
SELECT object_store_public.freeze_objects(
s_id := 'aaaaaaaa-...'::uuid,
id := '<root-id>'::uuid
);
Testing
pnpm test
Dependencies
@pgpm/verify: Verification utilities
Related Tooling
- pgpm: PostgreSQL Package Manager for modular Postgres development.
- pgsql-test: Isolated testing environments with per-test transaction rollbacks.
Disclaimer
AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.