Open Source Data Tools
A catalog for an Iceberg lakehouse, a Python library for Iceberg tables, and a small datastore for Node.
Three of the projects I maintain sit on the data side rather than the agentic side. Pangolin is an Apache Iceberg REST catalog written in Rust, currently 0.8.0. IceFrame is a Python library for working with Iceberg tables, currently 0.13.0 and published on PyPI. SencilloDB is an embedded JSON document store for Node.js, currently 0.7.0 and published on npm. All three are pre-1.0, and two of them are lakehouse infrastructure. The third is a much smaller tool for local application state. Each section below gives the version, what the project actually does today, and what it is not ready for.
Three layers, honestly labelled
Most of my writing and speaking is about the open lakehouse. These are the parts of it I build rather than describe, plus one small utility that predates the lakehouse work and still earns its place.
Pangolin
A Rust service that speaks the Iceberg REST protocol, so engines can find and commit to tables. Multi-tenant, with Git-style branching over catalog state. Pre-1.0, hardening fast.
IceFrame
A Python library that reads and writes Iceberg tables with a DataFrame-shaped API on top of PyIceberg, plus a read-only MCP server so agents can query them.
SencilloDB
Transactions, indexes and queries over plain JSON files in one Node process. Nothing to do with Iceberg. It exists because not every job deserves a database server.
On maturity All three are pre-1.0, and Pangolin moves fastest: it went from 0.6.0 to 0.8.0 in two days. I would rather you read the limitations here than discover them in your own environment, so each section states what is done and what is still open.
Pangolin
A multi-tenant, Iceberg-compatible lakehouse catalog written in Rust. Named for the animal: layered scales over a single coherent body, defensive by design, and built for one specific job rather than everything.
What it is
A catalog is the component that tells a query engine which tables exist and where their metadata lives, and that arbitrates commits when several writers touch the same table. Pangolin implements the Apache Iceberg REST specification, so PyIceberg, Spark, and other Iceberg clients can point at it directly.
Beyond the REST surface, it adds the pieces an operator tends to want around a catalog:
- Multi-tenancy with tenant-scoped namespaces and warehouses, checked by isolation tests running against the real authentication middleware.
- Git-style branching, tags and 3-way merge over catalog state, for experimenting against a branch before promoting it.
- Credential vending for AWS STS, Azure SAS and GCP downscoped credentials, so engines get scoped access rather than long-lived keys.
- RBAC, service users and API keys for CI, ETL and automated pipelines.
- Audit logging across more than 40 actions and 19 resource types, including authentication events.
- OpenID Connect with PKCE,
id_tokensignature validation through JWKS, discovery, and rate-limited key rotation, for Google, Microsoft, Okta or any IdP you give an issuer. - Warehouse cloud credentials encrypted at rest with AES-256-GCM, and rate limiting on the authentication endpoints per source address and per account.
- Pluggable metadata backends: PostgreSQL, SQLite, MongoDB or in-memory.
- Federated catalogs, proxying an external Iceberg REST catalog through the same endpoint.
- A management UI in SvelteKit, two CLIs, and PyPangolin, a Python SDK.
Current status
Version 0.8.0, released 11 August 2026. Pre-1.0, and a long way from where it was two days earlier. 0.7.0 and 0.8.0 were a security and production-readiness push: 0.7.0 closed a privilege escalation that let any authenticated caller mint a Root token, and 0.8.0 did the operational work. The published artifacts are a GitHub release, the alexmerced/pangolin-api:0.8.0 container image, and PyPangolin 0.8.0 on PyPI.
Almost everything I would have warned you about a week ago is now closed:
- Warehouse cloud credentials are encrypted at rest with AES-256-GCM. They used to sit in the catalog database in plaintext.
- The authentication endpoints are rate limited, per source address and per account. The login endpoint used to be freely brute-forceable.
- OpenID Connect is implemented: PKCE,
id_tokenvalidation via JWKS,iss,aud,expandnoncechecks, and discovery. - Branch creation by copy is transactional, and the API no longer returns
200on a copy that failed partway through. - The missing Iceberg operations landed:
registerTable,listViews,viewExistsanddropView. Before this,DROP VIEWappeared to succeed against a catalog that had never heard of the operation. - Backup and restore is drilled, not described. A script dumps, destroys the schema, restores and verifies. Measured against PostgreSQL 15 with 1,345 rows: 7 seconds to back up, 53 seconds to restore.
- There are real performance figures, measured server-side: 0.018ms for a readiness check, 0.023ms for
/v1/config, and 0.060ms for an authenticated catalog list. - MongoDB manages its index set and enforces the uniqueness the SQL backends get from primary keys.
The claim is backed by tests rather than inspection: 415 tests across 63 targets with zero failures, and 19 green CI jobs including an authorization matrix and a four-backend parity suite, run against live PostgreSQL, MongoDB and MinIO.
What is still open, in the order it would block a production deployment:
- GitHub logins cannot be OIDC-validated. GitHub issues no
id_tokenand publishes no JWKS, so that path still rests on the userinfo endpoint. SettingPANGOLIN_OIDC_REQUIRE=truerefuses any provider that cannot be validated. - Multi-replica operation is constrained and unproven. It works on PostgreSQL, but OAuth needs session affinity, rate limiting is per replica, and it has not been load tested or soaked.
- Token revocation fails open. If the revocation check errors, the request proceeds, so a database blip means revoked tokens are accepted again.
commitTransactionis absent on purpose, because there is no cross-table transaction underneath it and pretending otherwise would be worse.- Smaller gaps: no tamper-evident audit trail or SIEM export, HS256 JWTs with no rotation, no MFA or account lockout, no point-in-time recovery, and session tokens stored unhashed at rest.
One caveat on the repository itself: the README still opens with an older alpha framing and a 0.6.0 version line. STATUS.md is the reconciled view and is the file to trust. Anything running a version before 0.7.0 should be upgraded and its tokens rotated.
Use cases
0.8.0 widens this from "development only" to a catalog you could run for real, within a posture the project spells out.
- An open catalog for an Iceberg lakehouse you control. Not tied to a vendor, covering the endpoints the common engines actually call, with credential vending so engines get scoped access rather than long-lived keys.
- A self-hosted catalog under a constrained posture. The project names the smallest credible one: PostgreSQL, a single replica, OAuth disabled, network restricted,
PANGOLIN_ENCRYPTION_KEYset, and a backup you have actually restored using the drill script against a copy of your own data. - Development and team environments. A catalog for building and testing Iceberg pipelines, where the SQLite and in-memory backends make a throwaway instance cheap.
- Multi-tenant internal platforms where the tenants are trusted. Tenant scoping is a required parameter throughout and is covered by an authorization matrix in CI.
- Learning how a catalog works. The commit path, requirement enforcement, credential vending and multi-tenancy are readable Rust in one repository.
Where I would still hesitate: untrusted multi-tenant use, or anything needing proven multi-replica scale, since that path is documented but not load tested. For those, a managed catalog or Apache Polaris remains the safer call.
IceFrame
A DataFrame-style Python library for Apache Iceberg tables, built on PyIceberg with local execution through PyArrow and Polars.
What it is
PyIceberg is the official Python implementation of Iceberg, and it is deliberately low level. IceFrame is a wrapper that keeps PyIceberg underneath and puts a friendlier surface on top, so common table work is one call rather than several.
- A DataFrame API:
read_table,to_arrow,to_pandas,lazy,head,describe,count_rowsandscan_batches. - A query builder with pushdown.
WHERE,SELECTandLIMITare pushed into the Iceberg scan when that is sound, and anything that cannot be pushed is re-applied locally so results stay correct. - Native upserts and transactions, wrapping PyIceberg's atomic
Table.upsertand multi-operation transactions. - Maintenance: expire snapshots, remove orphan files, and compact with bin-pack, sort or approximate z-order strategies.
- Data quality constraints where nulls fail by default, usable as a gate on writes.
- Metadata tables returned as Polars frames: snapshots, files, partitions, manifests, history and refs.
- Broad ingestion from CSV, JSON, Parquet, ORC and Avro natively, plus Excel, Delta, Google Sheets, SQL, XML, APIs, HuggingFace and HTML.
- An agent surface: a read-only MCP server with row and byte caps, plus a multi-provider LLM agent.
- Catalog support for REST catalogs including Dremio, Apache Polaris and Tabular with credential vending, plus PyIceberg's
sql,memory,glue,hiveanddynamodbcatalogs by pass-through.
It ships py.typed, logs through the standard logging module rather than printing, and raises typed exceptions that subclass the built-ins they replace.
Current status
Version 0.13.0, live on PyPI, and labelled alpha in its own README. Install it with pip install iceframe.
pip install iceframe
pip install "iceframe[aws]" # S3 support
0.13.0 was a correctness release, and it is the version to be on. Three defects in earlier releases caused silent data loss or silently wrong results: compact_data_files with a filter replaced the entire table with the filtered subset rather than only the matching rows, a compound AND filter could silently drop an operand that could not be pushed down, and null values passed every data quality constraint. All three are fixed with regression tests. If you are on 0.12 or earlier, upgrade before doing anything else, and read the changelog for the behaviour changes, including remove_orphan_files now defaulting to a dry run.
The same release made the test suite runnable by anyone. Before 0.13.0, 61 of 179 tests skipped on every machine except mine because they needed a live REST catalog. The core suite now builds a local SQLite catalog with a file:// warehouse and needs no credentials and no network.
What it does not do, stated in the README as well:
- Merge-on-read delete writes are not supported. PyIceberg has no public delete-file writer, so deletes are copy-on-write. Reading tables that already contain delete files works fine.
- Joins read each joined table in full. Only the driving table gets pushdown.
- Z-order is an approximation, a hierarchical sort rather than a bit-interleaved curve, and the returned strategy says so.
- Window functions run locally in Polars, after the scan.
Local execution is the design, not a limitation being worked around. Table sizes that comfortably fit a single machine are the target. Ray-based distribution is available for the cases that do not.
Use cases
- Python data engineers working with Iceberg tables day to day. Reading, writing, upserting, evolving schemas and running maintenance without dropping to the low-level PyIceberg API for each of them.
- Agent-facing access to a lakehouse. The read-only MCP server with row and byte caps gives an AI agent a bounded way to query Iceberg tables, which is a far better answer than handing a model warehouse credentials.
- ETL and ingestion pipelines that pull from files, APIs or other systems into Iceberg, with data quality constraints acting as a write gate so bad batches fail rather than land.
- Notebook and exploratory work against tables in a REST catalog, where a Polars frame is the shape you actually want.
- Table maintenance on a schedule: snapshot expiry, orphan file cleanup and compaction, with the dry-run defaults making the destructive operations opt-in.
SencilloDB
A dependency-light JSON object store for Node.js. Sencillo is Spanish for simple, which is the whole design brief.
What it is
SencilloDB sits between a JSON file you read and write by hand and a real database server. No daemon, no ports, no schema migrations to run against a service. You point it at a file or a folder and write transactions against it, and the data on disk stays readable in an editor.
npm install sencillodb
import { SencilloDB } from "sencillodb";
const db = new SencilloDB({ file: "./app.json" });
const user = await db.transaction(async (tx) => {
await tx.ensureIndex({ collection: "users", field: "email", unique: true });
return tx.create({
collection: "users",
data: { name: "Alice", email: "alice@example.com", age: 30 },
});
});
- Transactions that are serialized and all-or-nothing. A throw inside the callback discards every change.
- Queries with
$eq $ne $gt $gte $lt $lte $in $nin $regex $exists $and $or $nor $not, dot paths, sorting,limit,skipandcount. - Secondary indexes with optional unique constraints, used for equality,
$inand range lookups. - Three storage modes: one file, one file per collection loaded on demand, or one file per index bucket when a collection gets large.
- Durability options: atomic temp-file-and-rename writes, an optional append only log with an
appendfsyncpolicy, and an advisory cross-process lock. - Operational basics: export, import, snapshot, versioned migrations, TTL expiry, change events, gzip compression and an LRU cache with a memory ceiling.
- Relations through
populate, resolving foreign keys against another collection.
It is ESM only, needs Node 18 or later, ships TypeScript types, and has one runtime dependency.
Current status
Version 0.7.0, live on npm, pre-1.0 but the most settled of the three projects here. The API surface is stable in practice and the library does a small job completely.
0.7.0 shipped on August 10, 2026 as an audit release: the source was split into modules, twelve roadmap features landed, and eighteen bugs were fixed, several of which were real correctness problems. A document in an unloaded shard crashed update and destroy. Folder mode with the append only log enabled wrote a log it never replayed, so a fresh instance saw an empty store. Collection and index names went into file paths unsanitised, so a name like ../foo escaped the store folder. Those are fixed.
Two things to know before upgrading. Returned documents are now copies rather than live references into the in-memory store, which was the source of silent state corruption; pass clone: false for the old behaviour. And stores written by 0.7.0 cannot be read by 0.6.x, because secondary indexes gained a sorted key list. Upgrades in the other direction are handled in place.
The scope is deliberately narrow, and I would rather name the boundary than let you find it. It is one writer process, data sized to what you can comfortably scan and back up as JSON, and no server-side access control, replication or remote sync. Past that, SQLite, Postgres or MongoDB is the right answer. Avoid it on network filesystems such as NFS and SMB, where the atomic rename and lock files it depends on are not dependable.
Use cases
- CLI tools that need durable local state. It installs with your package and stores data next to the project or in a user config folder, with no setup step for whoever installs your tool.
- Electron and Tauri desktop apps where the database lives beside the application and the app snapshots, migrates and compacts its own store.
- Prototypes and small internal tools that have outgrown a hand-managed JSON file but do not justify choosing and running infrastructure yet.
- Test fixtures and integration tests that should exercise real persistence, including transactions and rollback, without booting Docker or a service.
- Bots, automation scripts and build tooling that need a durable cache, a manifest or task state with one writer and a predictable data size.
Reach for something else when many processes write frequently, when you need replication or access control, or when queries need joins, aggregations or full-text search.
Which one you need
Two of these are the same lakehouse seen from opposite ends. The third is here because it is open source data tooling I maintain, not because it shares a stack.
-
01
You need somewhere for Iceberg tables to live
That is the catalog layer, and Pangolin is a real option now that 0.8.0 has shipped credential encryption, rate limiting, OIDC and a drilled restore. Run it on PostgreSQL, one replica, network restricted, with a backup you have tested. For untrusted multi-tenant use or proven multi-replica scale, Apache Polaris or a managed catalog is still the safer call.
-
02
You need to read and write those tables from Python
That is IceFrame. It talks to any Iceberg REST catalog, including Pangolin, and also to Glue, Hive, DynamoDB and a local SQLite catalog through PyIceberg. This is the piece I would put in someone's hands first, because it is published, tested offline, and useful against a catalog you already run.
-
03
You need an agent to query a lakehouse safely
IceFrame's read-only MCP server, with its row and byte caps, is the bounded surface for that. It connects the lakehouse work to the agentic tooling: an agent gets a query interface with limits rather than warehouse credentials.
-
04
You need local state in a Node process
That is SencilloDB, and it has nothing to do with the two above. One writer, JSON files you can open in an editor, transactions and indexes over them. When the job outgrows that, move to SQLite or Postgres.
All open source Pangolin is MIT, IceFrame is Apache-2.0, SencilloDB is ISC. Issues and pull requests are open on all three, and the fastest way to reach me about any of them is dev@alexmerced.com.
Common Questions
Is Pangolin ready for production?
Much closer than it was, and still pre-1.0. 0.8.0 encrypted warehouse credentials at rest, rate limited the authentication endpoints, implemented OpenID Connect, made branch creation transactional, and turned backup and restore into a drilled script with measured timings, all backed by 415 tests and 19 green CI jobs against live backends. What still blocks untrusted multi-tenant use: GitHub logins cannot be OIDC-validated, multi-replica is unproven under load, and token revocation fails open. The project documents a smallest credible posture for running it.
How much of the Iceberg REST specification does Pangolin implement?
Most of it as of 0.8.0, which added registerTable, listViews, viewExists and dropView. Namespace and table CRUD, commits with full requirement enforcement, rename, the view operations, credential vending and the OAuth token endpoint are implemented. Still absent: replaceView, renameView, and commitTransaction. The last is a deliberate refusal: there is no cross-table transaction underneath, so exposing it would let an engine rely on atomicity that does not exist.
What does IceFrame add over using PyIceberg directly?
A higher-level surface over the same engine. A fluent query builder with pushdown, Polars integration and schema inference on write, compaction strategies, a unified view abstraction, rollback by snapshot or timestamp, async operations, metadata tables as Polars frames, data quality constraints usable as a write gate, and a read-only MCP server for agents. PyIceberg stays underneath and you can drop to it whenever you want.
Which version of IceFrame should I install?
0.13.0 or later. It fixes three defects that caused silent data loss or silently wrong results in earlier versions, including a filtered compaction that replaced the whole table with the filtered subset. If you are on 0.12 or earlier, upgrade first and read the changelog for the behaviour changes.
Can IceFrame and Pangolin be used together?
Yes. Pangolin exposes an Iceberg REST catalog and IceFrame speaks to Iceberg REST catalogs, so IceFrame is a natural client for it. Neither requires the other. IceFrame works against Dremio, Apache Polaris, Glue, Hive, DynamoDB and a local SQLite catalog, and Pangolin serves any Iceberg client.
Why is a JavaScript datastore grouped with Iceberg tooling?
Because it is open source data tooling I maintain and people ask about it. SencilloDB shares no code or concepts with the lakehouse projects. It solves a smaller problem: durable, queryable local state in one Node process without running a server.
Pick a starting point
Install one, run the catalog, or see the rest of what I build.
Start with IceFrame
Published, offline-testable, and useful against a catalog you already run. One pip install and a REST catalog URL.
Read the code
Pangolin for the catalog internals, SencilloDB for a small, complete library you can read in an afternoon.
See the rest
The agentic tooling, the books, and everything else on the main page.