Skip to main content

pgfire logo

pgfire

A minimal, ~400-line Firestore-to-PostgreSQL migration bridge for Python. pgfire keeps the chainable, familiar developer experience of the Google Cloud Firestore SDK (.collection().document().where()) running on plain PostgreSQL tables with native JSONB.

Scope & intention

pgfire is not a general-purpose ORM for new projects. It's a hyper-targeted tool for one real problem: migrating an existing Firestore-backed codebase to PostgreSQL with minimal call-site rewrites, when hundreds of existing .collection().document().get() calls make a wholesale relational redesign too slow and too risky to do up front.

If you're starting something new, use SQLAlchemy or Prisma instead — they're more capable, more battle-tested, and have real tooling (type-generated clients, migrations, connection pooling) that this project deliberately does not try to reinvent. If you're doing a surgical, incremental cutover of an existing Firestore app, pgfire is built for exactly that.

How it actually works

Storage model: one Postgres table per Firestore collection. Every table has id TEXT PRIMARY KEY and data JSONB. Any field you filter/order on often can be promoted to a real GENERATED ALWAYS AS (data->>'field') STORED column for a real index — pgfire detects which columns exist via information_schema and uses them automatically; anything not promoted falls back to a data->>'field' JSONB lookup, so it works against a bare (id, data) table too.

Subcollections (users/{uid}/moods/{uid}) map to a separate table named by joining the path with __ (users__moods), with a parent_id column holding the parent document's id. This composes at any depth automatically — there's no special-casing per collection, calling .document(id).collection(name) again just repeats the same rule on whatever table you're currently on.

Plain (non-generated) timestamp columns — e.g. created_at, session_date — can't be GENERATED (Postgres rejects text->timestamp casts there; the cast isn't IMMUTABLE). pgfire's .set() populates any such column itself from a matching key in the document you pass in, so you don't have to maintain it by hand.

What's implemented

  • db.collection(name) / .document(id) — chainable refs, matching Firestore's shape
  • .get() / .set(data, merge=False) / .update(data) / .delete()
  • .where(field, op, value) with ==, !=, <, <=, >, >=, in — chainable, AND-combined
  • .order_by(field, direction), .limit(n)
  • .stream() (generator) / .get() (list) on a query
  • .count() — COUNT(*) server-side, no row fetch
  • .aggregate(**named_filters) — multiple COUNT(*) FILTER (WHERE ...) conditions in a single round trip; each value is (field, op, value) or a raw SQL boolean expression string
  • Subcollections, composable to any depth
  • .batch() — multiple writes applied together
  • db.transaction() — a BEGIN ... COMMIT context manager (row locking is on you — take it out yourself with SELECT ... FOR UPDATE inside the block)

What's not implemented

  • array_contains / array-membership filters
  • Cursor-based pagination (start_after / start_at) — use .limit() with an .order_by() on an indexed column instead
  • Connection pooling — PostgresClient holds one plain psycopg2 connection. Put a pooler (AWS RDS Proxy, PgBouncer) in front of it for any real concurrent traffic; don't share one PostgresClient across threads/requests without one.
  • Compile-time type checking / a generated client — pgfire coerces values for JSONB comparisons at runtime (see pgfire/utils.py); there's no static schema to check your calls against. Lean on integration tests.
  • Foreign-key enforcement — the data payload isn't validated against anything; nothing stops a dangling reference the way a real FK would.

Audit your own call sites against this list before assuming parity on anything not explicitly covered above.

Quick start

from pgfire import PostgresClient

db = PostgresClient("host=localhost port=5432 dbname=mydb user=me password=secret sslmode=require")

# Document access - same shape as the Firestore SDK
ref = db.collection("users").document("usr_12345")
ref.set({"email": "a@b.com", "user_type": "patient"})
snap = ref.get()
if snap.exists:
    user = snap.to_dict()

# Queries
admins = db.collection("users").where("user_type", "==", "admin").where("is_demo", "==", False).get()

# Server-side aggregation - no row fetch, no Python-side counting loop
counts = db.collection("users").where("user_type", "==", "patient").aggregate(
    total="TRUE",
    onboarded=("status", "==", "onboarded"),
)
# -> {"total": 30166, "onboarded": 30114}

# Subcollections
invite_ref = db.collection("companies").document("co_1").collection("invites").document()
invite_ref.set({"email": "invitee@example.com", "used": False})

Production trade-offs

Dimension pgfire's behavior What you need to add
Connection pooling None — one plain connection per PostgresClient AWS RDS Proxy or PgBouncer in front
Type safety Runtime coercion, no generated client Integration tests on every call site you port
Data integrity No foreign-key enforcement A monitoring/cleanup job for orphaned references, if you need one
Concurrency Synchronous, one connection Fine behind a pooler with standard sync workers (e.g. Gunicorn); don't share a client across concurrent requests without one
Realtime No onSnapshot-equivalent Keep any feature depending on Firestore's live listeners on Firestore, or build your own realtime layer (websockets/SSE) before migrating it

Testing

pip install -e ".[test]"
pytest

Tests run against a real Postgres instance — set PGFIRE_TEST_DSN to a connection string for a disposable database (CI spins one up as a service container; see .github/workflows/test.yml).

License

MIT.

Metadata

Release files for pgfire 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pgfire 0.1.2
File Size Uploaded
pgfire-0.1.2.tar.gz 15.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pgfire 0.1.2
File Interpreter ABI Platform
pgfire-0.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 27.8 kB

Release files / pgfire-0.1.2.tar.gz

Download URL pgfire-0.1.2.tar.gz
Size 15.4 kB
Tags Source
SHA-256 checksum
How to use checksums
566be4c40538bbffce6a323d5aef8f8ed2c801f8de32a434ee5776ea919e85a1
BLAKE2b-256 checksum
How to use checksums
17dc1b4702e58e2c750034edc9a89960e3d2e4b524a704d265813191e491db24
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.

Transparency log

Release files / pgfire-0.1.2-py3-none-any.whl

Download URL pgfire-0.1.2-py3-none-any.whl
Size 12.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
010d6eceda3ea73ad1d3fac8569a122b6cf18b88e917c60d0425058568e1ebd9
BLAKE2b-256 checksum
How to use checksums
f7869cfc3efdfbf6a5e79ae2cd4c43659ee8f7eba4833370a41bde5bfcb276a3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.

Transparency log

Release history Release notifications | RSS feed

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

This release

0.1.2 This release

2 release files

0.1.1

2 release files

0.1.0

2 release files

0.0.2

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page