Pongo, a Mongo-style API that lands on PostgreSQL JSONB
Pongo - Mongo but on Postgres and with strong consistency benefits
At a glance
- What is it?
- Pongo is a TypeScript library that translates the MongoDB collection API into native PostgreSQL queries over JSONB columns, with a fixed table layout, optimistic versioning and optional schema migrations. The point is not to replace PostgreSQL, it is to give Mongo muscle memory on a database that already offers transactions.
- Who is it for?
- Pongo suits teams that know the MongoDB API and want it on PostgreSQL, either because the organisation has standardised on one database or because the surrounding data lives there and joins matter, and it suits people who want transactions and ACID guarantees without designing a relational schema for a document-shaped workload.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 7 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The MongoDB call translated into a PostgreSQL statement
Pongo is described as Mongo but on PostgreSQL and with strong consistency benefits, and the mechanism is stated plainly in the README: it takes the MongoDB API and translates it to native PostgreSQL queries. The README points to Marten, FerretDB and AWS DocumentDB as similar concepts.
The translation is concrete rather than rhetorical. A MongoDB update using the push operator:
const users = pongoDb.collection<User>('users');
await users.updateOne({ _id: someId }, { $push: { tags: 'character' } });becomes a PostgreSQL update that manipulates the JSONB column directly:
UPDATE "users"
SET data = jsonb_set(data, '{tags}', (COALESCE(data->'tags', '[]'::jsonb) || to_jsonb('character')))
WHERE _id = '137ef052-e41c-428b-b606-1c8070a47eda';And a query against a nested array of objects becomes a JSON path predicate:
const result = await users
.find({ 'address.history': { $elemMatch: { street: 'Elm St' } } })
.toArray();SELECT data
FROM "users"
WHERE jsonb_path_exists(
data,
'$.address.history[*] ? (@.street == "Elm St")'
);The second example is the one to look at carefully, because it shows what you gain. That query is expressed in a document shape that Mongo developers read without thinking, and it runs as a native PostgreSQL jsonb_path_exists, which means the query planner and the existing PostgreSQL indexes are in play.
The installation is a single npm command that pulls the library together with the Postgres driver and its types:
npm install @event-driven-io/pongo pg @types/pgOne table per collection, with version and partition columns
The storage model is a fixed table layout, and it explains what the library can and cannot do. The README gives the DDL it uses for every collection:
CREATE TABLE IF NOT EXISTS "YourCollectionName" (
_id TEXT PRIMARY KEY,
data JSONB NOT NULL,
metadata JSONB NOT NULL DEFAULT '{}',
_version BIGINT NOT NULL DEFAULT 1,
_partition TEXT NOT NULL DEFAULT 'png_global',
_archived BOOLEAN NOT NULL DEFAULT FALSE,
_created TIMESTAMPTZ NOT NULL DEFAULT now(),
_updated TIMESTAMPTZ NOT NULL DEFAULT now()
)Every collection is a table with the same eight columns. The document itself is the data column, stored as JSONB, which the README contrasts with the plain text storage of the traditional JSON type, noting that the binary format brings advantages in performance and storage efficiency.
The other columns are the interesting part, because they are what turns a document table into something you can run a real system on. The _version column starts at 1 and gives you optimistic concurrency control, so two writers can be detected rather than silently overwriting. The _partition column starts at the literal value 'png_global' and is what makes PostgreSQL table partitioning usable, since a partitioned table needs a partition key. The _archived boolean is a soft delete flag. The _created and _updated timestamps are timestamptz columns maintained by the library.
The metadata column is a second JSONB document alongside the data, which is the natural place for things the schema does not want to mix with the user's own fields.
This is the design decision the whole library rests on. You get Mongo's flexible document shape, but the rows still live in PostgreSQL, so the database features the README lists, partitioning, logical replication and other PostgreSQL capabilities, apply to your data.
Two ways in, the native client and the MongoDB compliant shim
Pongo can be used in two ways, and the difference matters for existing code.
The native client imports pongoClient and ObjectId from the package root, connects with a standard PostgreSQL connection string, and gives you a db and then a typed collection:
import { pongoClient, ObjectId } from '@event-driven-io/pongo';
type User = { name: string; age: number };
const connectionString =
'postgresql://dbuser:[email protected]:3211/mydb';
const pongo = pongoClient(connectionString);
const pongoDb = pongo.db();
const users = pongoDb.collection<User>('users');The collection is generic over the document type, so TypeScript knows what insertOne and findOne return without a cast.
The second way is the MongoDB compliant shim, imported from @event-driven-io/pongo/shim, which exposes a MongoClient and lets you write the same calls against it. The README describes it as the option for code that already speaks Mongo, so you can change the import and the connection and leave the call sites alone.
The operations themselves are the familiar ones in both cases. Inserting uses insertOne, and the returned object has an insertedId. Updating uses updateOne with a filter and an update document containing operators like $set. Deleting uses deleteOne. Reading uses findOne for a single document and find for a list, and the shim example ends the find with a .toArray() call, which is the Mongo cursor shape.
The ObjectId type is exported in both entry points, so a document can carry a generated identifier the same way it would in Mongo:
const cruella = { _id: ObjectId(), name: 'Cruella', age: 40 };The practical appeal is migration. A codebase written against the MongoDB driver can be pointed at PostgreSQL by changing the import and the connection string, and the README lists reuse of MongoDB muscle memory as one of the reasons to use Pongo.
Schema creation on first write, or as a deployment step
The table creation policy is configurable, and the README describes both ends of it.
By default, Pongo creates a collection's table on the first operation on that collection. That is convenient, and it is the behaviour that makes a quickstart work with a single insert, but it also means the application user needs DDL rights in production, which many teams do not want to grant.
The alternative is to register the collections during startup and call migrate() explicitly:
pongoDb.collection<User>('users');
await pongoDb.schema.migrate();Registering the collection and then migrating creates the table in a deployment step rather than at first use.
The runtime behaviour is then set with a schema option on the application. With schema: { autoMigration: 'EnsureMigrated' }, Pongo only checks the migration history and throws PendingMigrationsError when a migration is missing, so a deployment that has not been migrated fails loudly instead of silently creating a table. With None, Pongo neither checks nor migrates, which is the setting for a production process that has already run the migration as a separate step.
The guide at src/docs/schema-migrations.md covers SQL previews, dry runs, transactions and upgrading from the older collection.schema.migrate() call, which is the detail to read if you are moving an existing project onto the newer scheme.
The samples/ directory holds two runnable examples, samples/simple-ts/ and samples/cloudflare/, and the repository root has setup.sh and setup.ps1 for preparing a local environment, plus a .nvmrc file pinning the Node version.
The argument for a document layer on a relational database
The README's Why section names the problem directly. MongoDB is a decent database, yet it has issues around ACID compliance and licensing, which can cause hardship for project scenarios and organisation policies. Both are the kind of objection that arrives from a compliance team rather than from an engineer, which is why a tool that keeps the Mongo API while moving the data into PostgreSQL has a real audience.
The listed benefits are worth reading as a set of reasons rather than a list of adjectives. Strong consistency comes from using a battle-tested and widely used PostgreSQL ACID compliant database. Easier integration comes from PostgreSQL being what the rest of the system already uses. Reuse of MongoDB muscle memory comes from the compatible API, which is what makes migration of existing projects easier. Cutting boilerplate and nested data management comes from not having to spread a document across normalised tables. Operating more easily than native PostgreSQL JSON queries is the point of the translation, since those queries are powerful but not the most accessible. A performance boost comes from JSONB indexing capabilities, and the remaining PostgreSQL capabilities, partitioning, logical replication, are named directly.
The honest limits follow from the same design. This is a translation layer for a defined subset of the MongoDB API, so anything it does not translate is a gap rather than a bug. The document shape also hides the rows, so the relational strengths of PostgreSQL are available for the infrastructure around the data rather than for the data itself. And the JSONB advantages depend on how you index, since an unindexed jsonb_path_exists is a scan.
The project is licensed Apache-2.0, published as @event-driven-io/pongo on npm, with a documentation site at event-driven-io.github.io/Pongo, an introductory blog post explaining the reasoning, and a Discord community linked from the README header.
Editorial conclusion
Pongo suits teams that know the MongoDB API and want it on PostgreSQL, either because the organisation has standardised on one database or because the surrounding data lives there and joins matter, and it suits people who want transactions and ACID guarantees without designing a relational schema for a document-shaped workload. It is the wrong choice if you need the full MongoDB feature set, since this is a translation layer for a defined subset of the API, and it is the wrong choice if you want PostgreSQL's relational strengths, because the table layout deliberately hides rows inside a JSONB document. Before adopting it, read src/docs/schema-migrations.md for the migration and dry run behaviour, decide between the native client and the MongoDB compliant shim, and note the release cadence, with 0.16.11 from 2026-02-10 the most recent tag against a last push on 2026-10-16.
Frequently asked questions
How do I install Pongo?
Install it as an npm module with npm install @event-driven-io/pongo pg @types/pg, which brings in the Postgres driver and its type definitions alongside the library. It is published under the @event-driven-io scope.
How does Pongo store documents in PostgreSQL?
Each collection becomes a table with a fixed layout: an _id text primary key, a data JSONB column holding the document, a metadata JSONB column, and _version, _partition, _archived, _created and _updated columns. The README notes that JSONB stores data in binary form, unlike the plain text JSON type.
What is the difference between the Pongo client and the shim?
The native client exports pongoClient and ObjectId from @event-driven-io/pongo, while the shim at @event-driven-io/pongo/shim exposes a MongoClient so existing Mongo code can keep the same call sites. The README presents the shim as the MongoDB compliant option.
How do I control when Pongo creates collection tables?
By default the table is created on the first operation on a collection. To create them in a deployment step, register the collection and call pongoDb.schema.migrate(), then run the application with schema: { autoMigration: 'EnsureMigrated' } so Pongo checks the migration history and throws PendingMigrationsError when a migration is missing.
Why would I use Pongo instead of MongoDB?
The README lists strong consistency through a widely used ACID compliant PostgreSQL, easier integration with the rest of an existing PostgreSQL system, reuse of MongoDB muscle memory, less boilerplate for nested data, simpler operation than native PostgreSQL JSON queries, and JSONB indexing performance.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/event-driven-io-pongo)Was this useful?