Postgres read replicas for Search and Audit
See the code
PgStack is Postgres read replicas for Search and Audit.
PgStack replicates your data while your existing Postgres remains the source of truth. Indexing and queries run outside your application database through a separate Postgres-compatible endpoint.
Run with one Docker command, or use PgStack Cloud for managed production workloads.
Use a development Postgres database and a superuser connection for automatic setup. Check logical replication with:
SHOW wal_level;
If the result is not logical, enable it and restart Postgres:
ALTER SYSTEM SET wal_level = 'logical';
Start PgStack with your database URL:
docker run --rm --name pgstack \
-p 54321:54321 \
-e DATABASE_URL='postgres://postgres:postgres@host.docker.internal:5432/postgres' \
ghcr.io/pgstack-io/pgstack:latest
On Docker Desktop, host.docker.internal points to your computer. On Linux Docker Engine, add --add-host=host.docker.internal:host-gateway to make that name resolve to the host gateway. For a remote database, use its reachable hostname. URL-encode reserved characters in the source password.
The image supports Linux amd64 and arm64, including Apple Silicon. latest tracks stable releases.
Audit is enabled by default. It records changes made while PgStack is running. After changing a row in your source database, connect to the PgStack server running on port 54321 from another terminal on your host:
psql 'postgres://localhost:54321/pgstack'
Query audit.changes:
-- Inspect changes to one table.
SELECT operation, before, after, committed_at
FROM audit.changes
WHERE schema = 'public' AND "table" = 'products'
ORDER BY committed_at DESC
LIMIT 50;
-- Filter on values captured in the changed row.
SELECT operation, after->>'name' AS name, committed_at
FROM audit.changes
WHERE after->>'name' ILIKE '%jacket%'
ORDER BY committed_at DESC;
Search works with tables that have primary keys. Save this as pgstack.yaml, changing the table and columns to match your database:
# Search only. Set audit.enabled to true to also capture change history.
audit:
enabled: false
search:
enabled: true
tables:
- name: public.products
indexColumns: [name, description]
storeColumns: [id, name, description]
indexColumns provide the text to search. storeColumns are returned by queries and available for filtering; include primary-key columns to return them in results. Source names become Search names such as public.products → search.public_products.
Mount your configuration when starting the container:
docker run --rm --name pgstack \
-p 54321:54321 \
-e DATABASE_URL='postgres://postgres:postgres@host.docker.internal:5432/postgres' \
--mount type=bind,src="$PWD/pgstack.yaml",dst=/app/pgstack.yaml,readonly \
ghcr.io/pgstack-io/pgstack:latest
Existing rows are indexed at startup, and subsequent changes keep the index current. Once indexing completes, connect to the PgStack server running on port 54321 from another terminal on your host:
psql 'postgres://localhost:54321/pgstack'
Search with BM25 keyword ranking:
SELECT id, name FROM search.public_products
ORDER BY keyword_rank('rain jacket') LIMIT 10;
For semantic and hybrid search, add -e OPENAI_API_KEY to docker run. Without a key, indexing stays keyword-only and makes no embedding API calls.
-- Find related meaning.
SELECT id, name FROM search.public_products
ORDER BY semantic_rank('something for wet weather') LIMIT 10;
-- Combine semantic and keyword relevance.
SELECT id, name FROM search.public_products
ORDER BY hybrid_rank('waterproof rain jacket') LIMIT 10;
Rankings sort best matches first. Use stored columns in WHERE filters. With semantic indexing enabled, indexed text and semantic/hybrid queries are sent to OpenAI's text-embedding-3-small, billed to your account. Restarting rebuilds the index and can incur embedding charges again.
Use an optional pgstack.yaml file, mounted as shown in the Search example. Without it, Audit is enabled and Search is disabled. Enable either feature or both; Search requires at least one configured table when enabled. Invalid configuration is rejected before source setup. See pgstack.example.yaml.
| Variable | Required | Purpose |
|---|---|---|
DATABASE_URL | Yes | Source Postgres connection URL |
OPENAI_API_KEY | No | Embedding generation for semantic and hybrid search |
PGSTACK_PASSWORD | No | SCRAM authentication for the SQL endpoint |
Your Postgres
|
| logical replication
v
+----------------- PgStack container -------------------+
| |
| CDC --> NATS --> Processor |
| | |
| +--------+--------+ |
| | | |
| v v |
| Audit: Iceberg Search: BM25 |
| + Parquet + Lance vectors |
| | | |
| +--------+--------+ |
| | |
| v |
| Postgres-compatible server |
| with embedded DuckDB |
| |
+--------------------------+----------------------------+
|
| wire protocol
v
Postgres clients
Audit stores Parquet with Iceberg metadata. Search uses Lance for BM25 and optional vectors. DuckDB runs inside PgStack's Postgres-compatible server to execute SQL over this data.
The open-source core provides the Search replica and Audit history through SQL. PgStack Cloud adds the managed platform:
The bundled image is designed for local development and evaluation. For production workloads, PgStack Cloud provides a fully managed service.
This project is distributed under AGPL-3.0.
See CONTRIBUTING.md for development. For vulnerability reports, see SECURITY.md.
1,883 followers · starred Nov 2024
165 followers · starred Nov 2024
201 followers · starred Nov 2024
49 followers · starred Sep 2025
Postgres read replicas for Search and Audit
See the code
PgStack is Postgres read replicas for Search and Audit.
PgStack replicates your data while your existing Postgres remains the source of truth. Indexing and queries run outside your application database through a separate Postgres-compatible endpoint.
Run with one Docker command, or use PgStack Cloud for managed production workloads.
Use a development Postgres database and a superuser connection for automatic setup. Check logical replication with:
SHOW wal_level;
If the result is not logical, enable it and restart Postgres:
ALTER SYSTEM SET wal_level = 'logical';
Start PgStack with your database URL:
docker run --rm --name pgstack \
-p 54321:54321 \
-e DATABASE_URL='postgres://postgres:postgres@host.docker.internal:5432/postgres' \
ghcr.io/pgstack-io/pgstack:latest
On Docker Desktop, host.docker.internal points to your computer. On Linux Docker Engine, add --add-host=host.docker.internal:host-gateway to make that name resolve to the host gateway. For a remote database, use its reachable hostname. URL-encode reserved characters in the source password.
The image supports Linux amd64 and arm64, including Apple Silicon. latest tracks stable releases.
Audit is enabled by default. It records changes made while PgStack is running. After changing a row in your source database, connect to the PgStack server running on port 54321 from another terminal on your host:
psql 'postgres://localhost:54321/pgstack'
Query audit.changes:
-- Inspect changes to one table.
SELECT operation, before, after, committed_at
FROM audit.changes
WHERE schema = 'public' AND "table" = 'products'
ORDER BY committed_at DESC
LIMIT 50;
-- Filter on values captured in the changed row.
SELECT operation, after->>'name' AS name, committed_at
FROM audit.changes
WHERE after->>'name' ILIKE '%jacket%'
ORDER BY committed_at DESC;
Search works with tables that have primary keys. Save this as pgstack.yaml, changing the table and columns to match your database:
# Search only. Set audit.enabled to true to also capture change history.
audit:
enabled: false
search:
enabled: true
tables:
- name: public.products
indexColumns: [name, description]
storeColumns: [id, name, description]
indexColumns provide the text to search. storeColumns are returned by queries and available for filtering; include primary-key columns to return them in results. Source names become Search names such as public.products → search.public_products.
Mount your configuration when starting the container:
docker run --rm --name pgstack \
-p 54321:54321 \
-e DATABASE_URL='postgres://postgres:postgres@host.docker.internal:5432/postgres' \
--mount type=bind,src="$PWD/pgstack.yaml",dst=/app/pgstack.yaml,readonly \
ghcr.io/pgstack-io/pgstack:latest
Existing rows are indexed at startup, and subsequent changes keep the index current. Once indexing completes, connect to the PgStack server running on port 54321 from another terminal on your host:
psql 'postgres://localhost:54321/pgstack'
Search with BM25 keyword ranking:
SELECT id, name FROM search.public_products
ORDER BY keyword_rank('rain jacket') LIMIT 10;
For semantic and hybrid search, add -e OPENAI_API_KEY to docker run. Without a key, indexing stays keyword-only and makes no embedding API calls.
-- Find related meaning.
SELECT id, name FROM search.public_products
ORDER BY semantic_rank('something for wet weather') LIMIT 10;
-- Combine semantic and keyword relevance.
SELECT id, name FROM search.public_products
ORDER BY hybrid_rank('waterproof rain jacket') LIMIT 10;
Rankings sort best matches first. Use stored columns in WHERE filters. With semantic indexing enabled, indexed text and semantic/hybrid queries are sent to OpenAI's text-embedding-3-small, billed to your account. Restarting rebuilds the index and can incur embedding charges again.
Use an optional pgstack.yaml file, mounted as shown in the Search example. Without it, Audit is enabled and Search is disabled. Enable either feature or both; Search requires at least one configured table when enabled. Invalid configuration is rejected before source setup. See pgstack.example.yaml.
| Variable | Required | Purpose |
|---|---|---|
DATABASE_URL | Yes | Source Postgres connection URL |
OPENAI_API_KEY | No | Embedding generation for semantic and hybrid search |
PGSTACK_PASSWORD | No | SCRAM authentication for the SQL endpoint |
Your Postgres
|
| logical replication
v
+----------------- PgStack container -------------------+
| |
| CDC --> NATS --> Processor |
| | |
| +--------+--------+ |
| | | |
| v v |
| Audit: Iceberg Search: BM25 |
| + Parquet + Lance vectors |
| | | |
| +--------+--------+ |
| | |
| v |
| Postgres-compatible server |
| with embedded DuckDB |
| |
+--------------------------+----------------------------+
|
| wire protocol
v
Postgres clients
Audit stores Parquet with Iceberg metadata. Search uses Lance for BM25 and optional vectors. DuckDB runs inside PgStack's Postgres-compatible server to execute SQL over this data.
The open-source core provides the Search replica and Audit history through SQL. PgStack Cloud adds the managed platform:
The bundled image is designed for local development and evaluation. For production workloads, PgStack Cloud provides a fully managed service.
This project is distributed under AGPL-3.0.
See CONTRIBUTING.md for development. For vulnerability reports, see SECURITY.md.
1,883 followers · starred Nov 2024
165 followers · starred Nov 2024
201 followers · starred Nov 2024
49 followers · starred Sep 2025