Most teams supporting a real product end up running two systems: a transactional database (Postgres, Spanner, MySQL) for the live application and a warehouse or lakehouse (BigQuery, Snowflake, Databricks SQL) for the analytics, kept in sync by a CDC pipeline. You store the data twice, you operate a pipeline that breaks, and neither side lets you branch or version data the way you version code.
The split has been the default architecture for a decade, and it is starting to come apart. A new category of database has emerged to replace it: the lakebase. The name is Databricks', coined in Matei Zaharia's VLDB 2025 keynote, and the industry is converging on the architecture even where it skips the name. Snowflake bought Crunchy Data to ship Snowflake Postgres beside its warehouse, and ClickHouse calls the same convergence composable HTAP. A lakebase, in Databricks' framing, is "a new, open architecture that combines the best elements of transactional databases with the flexibility and economics of the data lake." It separates compute from storage and keeps the data in low-cost object storage in open formats, with the transactional engine running as an integrated layer on top. That is the vision. The products shipping it run the transactional engine next to the lake rather than integrated with it. The operational data is a separate copy, kept in sync by a pipeline, not the open files themselves.
Penca is what the "lakebases" shipping today aspire to be. It does two things they do not. First, it stores your data exactly once. The files that back your lakehouse are the same files that back production, with derived indexes, not duplicate copies, bridging the transactional and analytical access patterns. Second, it version-controls that data at the row level, so you can audit, time-travel, and revert production data like code. Today's lakebases deliver neither. They keep a synced analytical copy, their branches fork the operational tier alone, and their versioning stops at the snapshot level. Penca is a single logical database, stored in a single set of files, with every table versioned at the row level. You can fork the entire database (not just the operational tier) from any previously committed state (not just from snapshots). The open-source primitives to build this exist today, and Penca is what happens when you compose them correctly.
This document is the technical brief. It covers Penca's architecture, the design choices behind it, what's working today, and what's deliberately out of scope. It assumes you're a database-literate reader, and it spends its depth where the open risk is. Whether a lakebase should exist is no longer in question; Databricks, Snowflake, and ClickHouse settled that. What still needs proving is how one set of columnar files can serve an application's reads and writes, and how MVCC holds up outside the database that usually provides it. Those two mechanisms get most of the pages that follow. The implementation is closed source today; we plan to open-source the core Penca library soon.
The problem with a split system
The split persists because OLTP and OLAP have opposite shapes: your application writes single rows at low latency and reads them by key, while your analytics scans and aggregates millions. Postgres is good at the first and chokes on the second; a warehouse is the reverse. So you run both and stitch them together, and the stitch costs you in two ways.
You store everything twice, behind a pipeline that breaks. Every row lands in Postgres, then gets copied by CDC into the warehouse. You pay for the same data twice (Snowflake and BigQuery price it openly at ~$23 per TB-month), and the CDC connector that keeps the two copies in sync is among the most fragile infrastructure in the stack: schema migrations break it, high write volume lags it, and it pages someone on a Sunday.
Neither side versions its data. Postgres and the warehouse both treat a
table as the present moment, overwriting old values in place. With no commit
graph to branch off, every experiment needs a full sandbox copy that takes
hours or days to stand up, so teams run three where they wanted thirty. With no
history to inspect, "who changed this row, and when" is a question the database
cannot answer, and rolling back a bad write is manual surgery with no git revert to reach for. Audit tables get bolted on to recover a fraction of what
the database should have kept natively.
Where today's lakebases stop short
The lakebase is supposed to end both pains at once. Separate compute from storage, keep one copy of the data in open files, and the duplicate bill and the fragile pipeline disappear with it; put branching on top, and you finally get the version control the split stack never had. That is the promise in Zaharia's keynote, and it is the right promise. The implementations shipping today stop short of it on every count.
They still store the data twice. Databricks' Lakebase serves lakehouse data to its Postgres engine through synced tables, which its own docs describe as creating "a managed copy" of the source table. Going the other way, Lakehouse Sync copies the operational data back into the lakehouse as a second table. Compute and storage were separated, but the data still lives in two places, and you still pay for both.
That sync is still CDC, with all of CDC's ceilings. Both directions are managed pipelines. Lakehouse Sync decodes the Postgres write-ahead log through change data capture to push operational data out; a synced table pulls lakehouse data the other way, and its live (Triggered or Continuous) modes make you turn on Change Data Feed first. It is the same mechanism, and the same failure modes, as the Debezium-and-Fivetran pipeline the lakebase was supposed to retire: it lags under write-heavy load, it stalls on schema changes, and its replication throughput is a hard ceiling rather than a knob you turn. Calling it "native" moved the operational burden inside the vendor; it did not remove the architectural limit. Under the hood, the unified database is still two systems with a pipeline between them.
Their branching forks only half the system. The branching that sells these
systems is real: Databricks' Lakebase forks a branch by
copy-on-write
on the Postgres storage layer, the model Neon pioneered
and Databricks acquired. But it forks the operational tier and only the
operational tier. Lakebase's transactions commit to its Postgres relations, not
to the lakehouse tables, so a branch isolates the Postgres copy while the
lakehouse tables sit in a separate layer the branch never forks. In a system
built to unify the two workloads, a branch covers one of them, and giving a
branch its missing analytical half means configuring a synced-table pipeline for
it, per branch, which drops you right back into the same CDC. And even there the
unit is the branch, a snapshot in time, not the row. Whole-branch
point-in-time restore rewinds everything at once, and there is no per-row
history beneath it. You cannot ask "who changed this row, and when," you cannot
revert one row while keeping the rest, and audit is still something you bolt
on. That is tolerable when a human runs a handful of experiments. It breaks down
in the agent era, when thousands of unattended writes hit production and the
question becomes which one corrupted a row, and how to undo just that one
without rolling back the rest.
There is a half-full reading of that list. By shipping a Postgres beside the lakehouse and making branches a headline feature, the category's biggest players settled the questions that needed a market to settle: teams want their operational database attached to the lakehouse, branching is a workflow people pay for, and a transactional engine over separated, bottomless storage is operationally proven, by Neon in production for years before Databricks bought it. What they skipped is the engineering. Can one set of open files genuinely serve both workloads, and can version control genuinely reach the row? Those are Penca's two claims, the primitives to deliver them have finally matured, and the rest of this document is spent earning them.
Penca's architecture
The architectural premise is simple: your data should live in exactly one place, in open formats, and you should be able to branch and audit it like code. Everything else falls out of that.
Penca is designed as a single, logical database that serves both transactional and analytical workloads from one set of tables. Hot data lives in Postgres for transactional, ACID writes. Cold data lives in object storage as open columnar files registered with an Iceberg REST catalog. We can plug directly into your object storage and catalog or host everything for you. The query engine resolves reads across both tiers via a merge-on-read algorithm, so analytical queries always see the latest committed state regardless of where the data physically sits. There is no replication step. There is no eventual-consistency window.
The hierarchy under the database is catalog → branch → schema → table.
A catalog is the top-level isolation boundary (typically per environment:
dev, staging, prod). Underneath each catalog, branches are first-class. Every
catalog has a main branch by default, and branching off main creates an
isolated view of the entire catalog: every schema, every table. A branch
isn't a copy. It's a view of shared underlying data with isolated writes.
This is the same model as Git, applied to a database.
Below the branch level, schemas are pure namespaces (the same as Postgres), and tables are the data primitive. Each table has a Postgres-style schema (typed columns) and is internally an auditable store: a composition of three append-only logs — upsert, delete, and commit — that gives you full insert/update/delete semantics with native row-level history. The auditable store is the basis of Penca's multi-version concurrency control (MVCC).
The system runs as five gRPC microservices: query, write, lifecycle, metadata, and a cache manager. A Flight SQL gateway fronts the query and write services for SQL clients (JDBC, ODBC, ADBC). The whole thing is written in Rust on top of tonic for gRPC and DataFusion for query execution. The architecture is sketched below; you can ignore the boxes if you don't care how the services compose and read the rest of this document without losing the plot.
Three things deserve highlighting because they're the load-bearing architectural choices:
Object storage is the unified permanent layer. Both transactional and analytical workloads resolve to one set of Iceberg tables in object storage. That is the only place customer data lives at rest. Postgres is hot-only and ephemeral: it holds recent writes that haven't yet been flushed to cold, and that's it. If a Penca instance is torn down and re-instantiated, the catalog reconstitutes from the bucket and queries resume against the same data. This is the "bottomless storage" design Neon popularized for Postgres, applied to a unified OLTP+OLAP system.
Branches share data via metadata, not copies. Each branch runs its own isolated set of Penca pods and its own Postgres. The new branch's Postgres starts empty. The query engine merges three things at read time: the new branch's hot tier (its Postgres), the new branch's cold tier (its own log files in object storage, also initially empty), and the source branch's cold tier as of the fork point. No data is copied between branches. We come back to the branch creation flow in detail below. (See Status and roadmap for what's shipped today versus the target.)
MVCC is implemented at the application layer. Every mutation is logged at
the row level: an UPDATE appends a new version to the upsert log instead of
overwriting it (tagged with a transaction ID, author, and timestamp); a
DELETE appends a tombstone. The current state of a row is computed at read
time by walking the logs, latest version or delete wins. Putting MVCC in
Penca's layer instead of the hot tier database has two payoffs. It isn't tied to one engine: Postgres is the hot tier today, but
nothing in the data model depends on it, so we can swap it for a better fit
without changing semantics. And it gives you full row-level history natively,
with no external audit table, which makes time-travel (as_of any prior
transaction), rollback, and fully ACID, long-lived (but bounded) transactions
first-class rather than bolted on. The
Row-level MVCC section goes deeper.
The next three sections go deeper on each pillar.
Unified object storage
Penca keeps your data as a single set of open files in object storage and serves production straight from them. There is no second copy and no CDC pipeline: the files that back your lakehouse are the same files your application reads and writes. Any database engineer should read that claim with suspicion, because the opposite shapes of the two workloads are the reason the split stack exists at all. Columnar files in object storage are built for scans, and an application needs single-row reads and writes in milliseconds. So before anything else, this section owes you the mechanism.
The shape is a log-structured merge tree, one level up. Penca never asks an application write to touch a columnar file synchronously. A write commits to the hot tier, a plain Postgres instance, at ordinary Postgres write latency. A background lifecycle process flushes committed data out of the hot tier into immutable columnar segments in object storage and registers them in the catalog. The query engine merges the two tiers at read time, the hot tier shadowing the cold, so every query through Penca sees the latest committed state no matter where a row physically sits. Databases have trusted this shape for decades: it is an LSM tree with Postgres as the memtable, your bucket as the segment store, and compaction running as a lifecycle pass. The hot tier is a buffer holding what has not yet flushed, not a second copy of the dataset. The novel part is not the mechanics but where the bottom level lives. The durable layer of this LSM tree is not a proprietary file on a local disk; it is your lakehouse.
Where it lives. Object storage. By default, we host the bucket and you
get the convenience. The interesting option is the alternative: bring your
own bucket. Point Penca at an S3 bucket in your AWS account. We never get
read or write access to your AWS account; we use scoped credentials you
provision. Your data at rest lives only in your storage. If you decide to
stop using Penca, your data is already where you keep your data. The "exit"
is aws s3 sync to wherever else you want it, against the same bucket you'd
already be syncing from.
How the rest of your lakehouse reads it. Penca registers its cold-tier snapshot segments as Iceberg tables in a REST catalog. That catalog is pluggable: point Penca at your own (the one Spark, Trino, Snowflake, or DuckDB already read from), or use one we host. The same segments Penca serves production from appear as standard Iceberg tables to the rest of your stack, with no copy and no separate export step. Those external reads see the cold tier, so they are current as of the last flush; a query through Penca merges the hot tier on top and stays fully current. Your operational store and your lakehouse are one set of tables.
How point reads resolve. A columnar file is built for scans, not for fetching one row by key, yet that key lookup is the read pattern an application leans on. Recent rows are still in the hot tier's Postgres, indexed the ordinary way; for everything that has aged into the cold tier, Penca writes index files alongside each segment, so a lookup resolves to the one segment and row that holds the value and reads only that, instead of scanning. The indexes are derived artifacts, rebuildable from the segments they describe, so they accelerate the single copy of your data rather than constituting a second one. That is what lets one set of open files serve point reads and scans alike, with no row-oriented duplicate kept for the application's sake.
What we cache. Penca is not a pure object-storage database. The hot tier is a Postgres instance that holds recent writes and the catalog metadata. A per-branch cache manager populates a local SSD cache with frequently accessed cold segments. Memory caches the working set. All of this is ephemeral: if you tear down a Penca instance, the caches go away and reconstitute from your bucket on the next request. The persistent layer is your bucket alone.
This means the truthful claim is "we don't need to store your data at rest." We do touch your data: we read it from your bucket into our SSD cache and memory while serving queries, and we write recent committed data into our Postgres before flushing to your bucket. But none of those layers is the permanent home of your data. Only the bucket is.
What this implies for cost. When you bring your own bucket, your AWS bill covers your storage. Penca's bill covers compute. The two are unbundled. You can negotiate S3 pricing with AWS directly. You can move to R2 or GCS if their pricing improves. You can use S3 Glacier or Intelligent Tiering on data we don't read hot. The compute side scales with usage; at rest, when no queries are running, compute scales to zero.
Branching
Branches are the feature that makes the version-controlled framing concrete.
The mental model is Git mapped onto a database: every catalog has a main,
you can fork a new branch from any committed transaction on main, you
write into the new branch in isolation, and you merge it back atomically
when you're done. The implementation is what makes that mental model work
without copying data, even on multi-terabyte tables.
The per-branch infrastructure isolation described in this section is the target architecture. Today, branches share a single Postgres instance and one bucket (zero data copy, but no compute isolation yet); see Status and roadmap.
The branch creation flow. When you call create_branch(name, base, fork_as_of=T),
three things happen:
- Flush base's hot tier to cold up to T. All data on the base branch
committed at or before transaction
Tis flushed from base's Postgres to base's cold tier segments in object storage. This is part of the normal storage lifecycle, just bounded to ensure the base's cold tier contains the complete state atT. - Record the new branch's metadata. A row in the catalog records the
new branch's name, its parent (
base), and the fork point (T). That's it. No data tables are copied; no file in object storage is duplicated. - Spin up isolated infrastructure for the new branch. Each branch runs its own set of Penca pods and its own Postgres instance. The new branch's pods come up, its Postgres initializes empty, and the branch is ready for reads and writes. (Target architecture; see Status and roadmap.)
The clever bit is that the new branch doesn't need a copy of the data, even at petabyte scale, because the query engine handles the read-time merge across branches. We come back to this below.
Reads on the new branch. A query against the new branch is resolved by the query engine in three layers, merged at read time:
- The new branch's hot tier (its own Postgres). Initially empty; accumulates as the new branch is written to.
- The new branch's cold tier (its own log files in object storage). Also initially empty; accumulates as the lifecycle flushes hot to cold on the new branch.
- The base branch's cold tier as of
fork_as_of=T. Read directly from base's segments in object storage. The base branch is unaffected by this read; segments are immutable once written.
The merge resolves to the latest non-deleted version per row, with the
new branch's writes shadowing the base's state where they overlap. This
is the same merge-on-read algorithm the query engine uses for the
single-branch case (hot + cold), generalized to also include the base
branch's cold tier at the fork point. The new branch sees the world as a
union of "what base looked like at T" plus "what I've done since."
Writes on the new branch. Mutations go to the new branch's own logs (upsert, delete, transaction) in its own Postgres, eventually flushed to its own cold tier segments in its own object-storage bucket. The base branch is never touched. This is the cleanest possible isolation guarantee: a runaway query on a branch can't lock base's Postgres, because branches don't share a Postgres. A bad write on a branch can't corrupt base's cold tier, because the segments live in different buckets.
Cross-schema atomicity. A Penca branch spans every schema in the
catalog. This matters because real applications write to multiple schemas
in one transaction, and branches need to preserve those semantics. On
main, a transaction that inserts into public.users and
analytics.events commits both rows atomically. On a branch, the same
transaction does the same thing, with the same atomicity. Cross-branch
reads are never valid (you read one branch at a time), which keeps the
isolation model clean.
Branch creation time. The cost of create_branch is dominated by
pod and Postgres spin-up. That's measured in minutes, not seconds. The
flush step (step 1 above) is usually small for a healthy system: the
lifecycle runs continuously, so the hot tier rarely has a large backlog.
The metadata write (step 2) is milliseconds. The infrastructure
provisioning (step 3) is where the time goes.
This is an honest trade-off. A "branches in 50ms" claim requires giving up either hot tier isolation (so a runaway branch can affect production) or write isolation (so branches share a Postgres and can lock each other). We've chosen full infrastructure isolation per branch, which costs spin-up time and buys correctness. For the use case (real experiments on real production data), minutes is the right point on the trade-off curve. Two-day data copies become two-minute branch creations, and the resulting branches are real isolation, not a shared sandbox with asterisks.
Concurrency and merging. Within a branch, writes serialize through
the branch's Postgres. Across branches, there's no contention: each
branch has its own infrastructure, its own write logs. The only place
branches interact is at merge time. merge_branch resolves the source
branch's current state via set-based SQL and writes it into the target's
logs as one atomic transaction. Consumers of the target either see all
the changes or none. Row-level conflicts (the same row updated on both
sides since the fork) resolve last-writer-wins by default; configurable
strategies are on the roadmap.
When a branch is no longer needed, delete_branch flushes its hot tier
to cold, soft-deletes the branch in the catalog (via the same auditable
store tombstone used for row deletes), and spins down its pods and
Postgres. The cold tier segments are reclaimed by a later lifecycle
pass, so the branch stays recoverable during the grace window.
Row-level MVCC
Penca implements multi-version concurrency control in its own read and write algorithms, over the append-only logs in the hot tier, rather than inheriting it from the storage engine that holds them. Concurrency control is a hot tier concern by construction. Transactions run only there, and the lifecycle flushes to cold only what has settled behind the oldest open transaction, so no live transaction ever depends on data that has left the hot tier. Of everything in this design, implementing MVCC ourselves is the choice that most needs defending. Postgres already ships a battle-tested MVCC, and reimplementing concurrency control above the database is the kind of decision that usually ends badly. The defense is that a version mechanism implemented inside one tier can only version that tier. Neon's copy-on-write lives in the Postgres storage layer, which is exactly why a Lakebase branch forks the Postgres copy and nothing else. Penca's unit of versioning has to follow a row through its whole life, from the hot tier into the columnar segments in your bucket, and it has to survive the hot tier being swapped for a different engine, so it cannot live inside any one component. It has to live in the layer that owns them both, and it has to be built from a primitive simple enough to trust. Append-only logs are that primitive: the history of every row is the data structure itself, and every question about past state (what changed, when, who did it, and how to get back) is a read.
The storage shape. Inside the auditable store, an UPDATE is an
append to the upsert log; a DELETE is an append to the delete log; and
every commit is recorded in the commit log. The current state of a
row at any point in time is computed by walking the upsert and delete
logs and picking the latest non-deleted version per primary key. Nothing
is ever overwritten in place.
ACID transactions, long-lived but bounded. Because the version store is an append-only log, concurrency control is multi-version by construction. A transaction reads a consistent snapshot, the committed state as of the moment it began: it ignores any version written by a transaction that committed later or is still open. Its own writes append new versions tagged with its transaction ID, invisible to everyone else until that ID lands in the commit log. Commit is a single atomic append to that log; abort leaves the uncommitted versions unreferenced, for compaction to reclaim. Nothing is updated in place to lock, so reads and writes never block each other.
This is what makes transactions both ACID and long-lived. A
multi-step migration, a backfill, or an expensive step in an agentic workflow
can run inside one transaction over minutes without holding a lock or blocking
production. In Postgres a long-lived transaction pins the cleanup horizon and
bloats every table; here it simply pins immutable row versions in the hot tier logs, which
costs nothing extra. What keeps transaction length bounded is a server-side
MAX_TX_TIMEOUT_SECONDS: a transaction open past it is aborted, so no single
transaction can pin rows in the hot tier logs forever.
Auditability falls out of this. The question "who changed this row, when, and what was it before" is answerable by walking the upsert log filtered to that row's primary key. The result is the full mutation history, tagged with the transaction that produced each version, which itself is tagged with author and timestamp. This works for any row, any table, at any time, without you having configured anything. The same mechanism that gives you correct read semantics also gives you the audit trail.
Time-travel falls out of this. The same walk, parameterized by a
transaction ID or timestamp, gives you the state as of any point in
history. "What did the user table look like at midnight UTC on March 14"
is a single query with an as_of parameter. The cost is proportional to
the depth of the version chain for the rows you read, not to the size of
the table.
Restore falls out of this. Walking the log gives you the state at a
prior commit. Restoring to that state is a merge: write the log entries
that would produce the prior state as a new commit. The user-facing
operation is the database equivalent of git revert. The granularity is
the row: you can restore the whole table to a prior state, or a subset of
rows, or even a single row. The mechanism is the same as branch merging,
scoped to a different operation.
What this costs. None of it is free, but the lifecycle keeps the cost off the hot path. The snapshot step materializes current state into read-optimized snapshot segments, so a regular read merges the hot tier over pre-resolved segments and never replays history. Only a time-travel read reaches into the unsnapshotted persist segments, the raw history, and pays the version-resolution cost at read time. What remains is a storage cost: every version retained is a version stored. Compaction bounds it, periodically rewriting cold segments to apply the retention policy below, so version chains stay as deep as you have chosen to keep them and no deeper.
Retention. Storing every version of every row forever isn't always
what you want. Retention is configured at three levels (catalog, schema,
table) with two independent fields: retain_max_versions (how many old
versions to keep per row) and retention_duration_seconds (how long to keep
them). A row version is eligible for removal during compaction only when it
exceeds both bounds; until a pass removes it, it stays available for
time-travel.
The trade-off is honest: retention erodes the audit trail. If you set
retain_max_versions=10, the eleventh prior version of any row is gone
forever after the next compaction. Most teams want this for write-heavy
tables; some teams, especially in regulated industries, want
retain_max_versions=NULL (keep everything) on certain tables. The
configuration is per-table, so you can have both.
No lock-in: every interface is a standard
Every interface Penca exposes is an industry standard with multiple independent implementations. The point isn't that we like standards aesthetically; it's that this is what makes "you can try it out and then leave" a real claim instead of a marketing claim.
SQL via Flight SQL. The Flight SQL gateway speaks Arrow Flight SQL, an open wire protocol from the Apache Arrow project. It exposes the database to any JDBC, ODBC, or ADBC client. BI tools, notebooks, data quality checkers, schema visualizers: anything that speaks one of those drivers connects. DML (INSERT, UPDATE, DELETE) translates to gRPC mutations under the hood. Transactions, branching, and audit operations are exposed via Flight SQL action endpoints.
gRPC for programmatic access. Clients use two services: query
(reads) and write (mutations). Together they cover catalog and schema
management, branching, transactions, streaming reads, and lifecycle
operations. The metadata, lifecycle, and cache manager services are internal —
query and write call into them, not clients. The gRPC interfaces are
defined in standard .proto schemas; any gRPC client in any language can
talk to the system. A Python, Go, Rust, Java, or TypeScript client is
just a code-gen away.
Iceberg tables at rest. Penca's cold-tier segments are Parquet, registered as Iceberg tables in an open REST catalog. Spark, Trino, Snowflake, and DuckDB read them as first-class tables, not just files. Or skip the catalog and read the Parquet directly: the same file Penca serves a query from is readable by DuckDB, Polars, pandas, Spark, or any other Arrow-aware tool. No exporter, no re-encoding, no proprietary intermediate format. If Penca the company goes away, your data is still there, still readable, still standard Iceberg.
The practical implication: there is no exit migration. The data is where you keep your data. The wire protocols are the wire protocols. Your tools work. You stop paying Penca for compute. That's the entire exit story.
Status and roadmap
The engine is real. Today, Penca covers the full write path (catalog, branch, and
schema management, transactions, data mutations), the full
read path (merge-on-read across hot and cold tiers), time-travel queries
(as_of), row-level audit, the storage lifecycle (persist, compact,
snapshot, retention), Iceberg catalog export, and standard SQL via JDBC/ODBC/ADBC. End-to-end
integration tests exercise the full lifecycle.
A few things on the roadmap we're excited about:
- Full branch isolation. Branches currently share a single Postgres instance and a single bucket: zero data copy, but also no compute isolation. We're working on lifting the branching abstraction up one level to infrastructure, so each branch gets its own pods, Postgres, and bucket.
- Lance integration. Penca's engine already fully supports Lance internally, where its fast random access and modern encodings give us a significant performance gain over Parquet. We write Parquet at rest today because that's what Iceberg supports. Iceberg's File Format API (finalized February 2026, shipped in 1.11.0) makes file formats pluggable and explicitly names Lance as a target; we plan to upstream Lance support so Penca can serve from Lance while staying fully Iceberg-compatible.
- Postgres wire protocol. Today Penca speaks Arrow Flight SQL over JDBC/ODBC/ADBC. We plan to add the Postgres wire protocol so any Postgres client or driver connects with no changes, in the same spirit as open storage formats: onboarding (and offboarding) becomes a connection-string swap, not a migration.
Design partners. Penca is currently onboarding a small number of
design partners. The criteria is a team running production on a split
system today (OLTP + warehouse/lakehouse + CDC), feeling the pain daily, and
willing to give feedback during the design-partner phase. Partners get
direct access to the engineering team, dedicated support, and a voice in
the roadmap. Penca gets sharp signal from teams who actually live the
problem. If that's you, write to [email protected].
Prior art and acknowledgments
Penca stands on the shoulders of open-source giants worth naming explicitly. None of the constituent technologies are Penca's invention. The contribution is the composition.
- Apache DataFusion powers the query execution and SQL planner.
- Apache Arrow is the in-memory format throughout the query engine and the wire format for both the Flight SQL gateway and the gRPC services.
- Parquet is the default Iceberg-compatible, cold tier file format. Support for Lance, Vortex, and Nimble is on the roadmap.
- Postgres is the hot tier. The current deployment is standard Postgres with no custom extensions; the only Penca-specific code is in how we implement MVCC at the application layer.
- Nessie explored branching for Iceberg tables. Penca extends that work to a unified OLTP+OLAP system.
- Neon introduced isolated branches, scalable compute, and bottomless storage to Postgres. Penca applies the same architectural pattern to a unified system rather than a single-tier database.
- Dolt brought Git-style branching to OLTP (relational SQL); LakeFS brought it to OLAP (data lakes on object storage). Penca uses similar vocabulary because the abstraction is genuinely the same, applied across both tiers.
The compositional thesis has a lineage of its own. Andy Pavlo flagged the commoditization of query engines years ago, and Wes McKinney's The Road to Composable Data Systems tells the story of how the whole analytical stack decomposed into open, interchangeable parts. Penca carries that story past analytics, composing those parts into a lakebase that stores data once and versions it at the row.
This document will get more specific as Penca's design hardens through
the design-partner phase. Comments, corrections, and disagreements to
[email protected].