for Postgres

The read replica,
re-invented for AI.

Everything your AI agents need to build seriously fast, live analytics on top of your Postgres: query 100M+ rows in milliseconds, through an API that gives your agents the context behind every column.

Postgres in · API + MCP out · seconds behind, not hours
Why a replica

Same Postgres. Two very different replicas.

Your read replicaa Postgres standby
Masona replica built for analytics
What both do
Streams from your WAL
Keeps read load off production
Leaves Postgres the source of truth
Needs no app or schema changes
Where they differ
Aggregates over 100M rows Weekly revenue by plan, over 103M orders.
41 s, then cancelled
psql · standby.acme.internal
=> SELECT subscription_plan, date_trunc('week', placed_at), sum(amount), count(*), count(DISTINCT customer_id) FROM orders WHERE placed_at > now() - interval '12 weeks' GROUP BY 1, 2; Seq Scan on orders (rows=103482991) … 41.2 s ERROR: canceling statement due to conflict with recovery DETAIL: User query might have needed to see row versions that must be removed.
184 ms
mason
await mason.query({ model: "order_revenue", metrics: ["revenue", "orders", "customers"], groupBy: ["plan", "placed_at.week"], timeRange: "last_12_weeks" }); 12 rows · 184ms, inside its 300ms target answered from revenue_by_plan, pre-aggregated by day scoped to org Globex by the caller's token
Indexes and rollups Whatever makes the queries fast.
Read-only: build them on the primary
psql · standby.acme.internal
=> CREATE INDEX ON orders (placed_at); ERROR: cannot execute CREATE INDEX in a read-only transaction -- every index, view and rollup has to be built -- on the primary, and slows its writes too
Chosen from your workload
mason · chosen from the last 24 h of queries
pre-aggregate revenue_by_plan by day partition orders by month index orders on order_id built on the replica · nothing touches the primary
Self-describing columns For a person, or an agent.
Names and types only
psql · \d orders
order_id | uuid amount | numeric ← net of refunds? in cents? legacy_status_v2 | text tmp_backfill_flag | boolean … 39 more columns
Described and profiled, every column
order_revenue · as your agent sees it
revenue: description: Net of refunds, in the org's currency. distribution: 048,200 · median 39 · 0.4% null plan: description: The plan at the time of the order, not today's. common_values: free 61% · pro 27% · team 9% · enterprise 3% you write the descriptions · Mason measures the rest → in every API response and every MCP tool call
Row-level security Each end-user sees only their rows.
One password, everyone's rows
psql · standby.acme.internal
=> GRANT SELECT ON ALL TABLES IN SCHEMA public TO analytics; -- every customer's rows, to anything -- that holds the password
A token per end-user
mason · token for org Globex
order_revenue filtered to org Globex orders filtered to org Globex audit_log refused: the filter can't be applied a filter that can't be applied refuses the query, it never widens it
Keeps secrets out Password hashes, API keys, sessions.
Copies every column
physical replication · every byte
users.password_hash copied api_keys.secret copied sessions.token copied
Copies only the columns you name
publication mason · the columns you name
users.password_hash left out api_keys.secret left out sessions.token left out
Zero-downtime changes Changing a model people are using.
Readers locked out while it refreshes
psql · primary
=> REFRESH MATERIALIZED VIEW revenue_by_plan; -- can't run on the replica; on the primary it -- locks out every reader until it finishes
Built beside the old one, swapped in
mason · deploy
order_revenue v2 building beside v1 9 data tests passed swapped in · 0 ms of downtime
Monitoring Is it fresh? Is it fast?
Lag. The rest is yours to build
psql · standby.acme.internal
=> SELECT now() - pg_last_xact_replay_timestamp(); 00:00:00.84 -- which queries are slow, for which customer, -- against what target: yours to build
Freshness and latency, per model
mason status
model behind p95 target order_revenue 35 s 184 ms 300 ms orders 16 s 62 ms 150 ms customers 16 s 9 ms 50 ms a model that misses its target is flagged
How it works

Five steps. You do three of them.

You connect it — one command — describe the data you want to present, and query it. Mason does the replicating and the optimising — continuously, without being asked.

01you

Point it at your Postgres.

One command. Mason asks for what any logical replica asks for and nothing more, streams from a standby if you have one, and copies only the columns you publish. Nothing on production changes.

mason — ~/acme terminal
$ mason connect postgres://standby.acme.internal
↳ Postgres 16.4 · standby · wal_level = logical
publication mason: 42 tables
3 columns left out: users.password_hash, api_keys.secret, sessions.token
replication slot mason_slot
copying history: 103,482,991 rows · resumable
streaming · 16s behind primary
next: describe the data you want to present ↓
02you

Describe the data you want to present.

A logical model is a short file: the rows, what is measured over them, how fast and fresh it must be — and a description on anything a person or an agent could misread. Joins come from your foreign keys. Nothing about storage. The descriptions travel with the data into every API response and every MCP tool call.

models/order_revenue.yml logical model
description: > One row per paid order. Revenue is recognised when the order is paid, net of refunds. from: orders where: status IN ('paid', 'refunded') time_column: placed_at columns: org_id: customer_id: # joins customers: a foreign key placed_at: description: When the customer checked out, in UTC. plan: sql: subscription_plan description: The plan at the time of the order, not today's. metrics: revenue: sql: sum(amount - refunded_amount) description: Net of refunds, in the org's currency. orders: sql: count(*) customers: sql: count(DISTINCT customer_id) targets: { latency: 300ms, freshness: 1m }
03mason

Mason keeps every model current.

Changes stream in from your primary and are routed to exactly the partitions that depend on them — not the whole table, and not everything downstream. A new order lands in seconds, and is in every model that uses it about half a minute after it was placed.

replication streaming
postgres
public.orders source of truth
raw
raw.orders CDC · replicated
logical models
orders
order_revenue
customer_activity
pre-aggregations
revenue_by_plan day · 300ms
orders_today minute · 150ms
active_customers week · 400ms
6 partitions recomputed · 0 full-table scans · 35s behind postgres
04you

Query it through one clean API — or let your agent.

One call: a model, its metrics and what to group them by. Row-level security comes from a token your server mints for the signed-in user — it says what they may see, and a filter Mason can't apply refuses the query instead of widening it. Your agent reads the model's descriptions over MCP and writes the same code you would.

Claude Code mason MCP
mason MCP connected · 3 tools
You
Add a revenue page: weekly revenue, orders and customers by plan, for the last 12 weeks.
Claude
I'll read the order_revenue model first, so the page uses the definitions your team already agreed on.
mason · describe_model
→ revenue: net of refunds
→ plan: at the time of the order
→ placed_at: checkout time, UTC
mason · query
→ validated · 12 rows · 184ms
→ scoped to org Globex
✎ server/mason-token.ts+19
✎ revenue.tsx+14
Done. mason-token.ts mints a token for the signed-in user's organization, and revenue.tsx queries with it — so each customer sees only their own orders.
Reply to Claude…
server/mason-token.ts your backend
import { MasonServer } from "@mason/sdk/server"; // MASON_API_KEY stays on your server const mason = new MasonServer({ apiKey: process.env.MASON_API_KEY }); export async function GET(req) { const user = await auth(req); // short-lived and signed: this user's // organization, applied to every model const token = await mason.createToken({ scope: { org_id: user.orgId }, subject: user.id }); return Response.json({ token }); }
revenue.tsx in the browser
import { Mason } from "@mason/sdk"; // a token from your server, not the API key const mason = new Mason({ token }); const data = await mason.query({ model: "order_revenue", metrics: ["revenue", "orders", "customers"], groupBy: ["plan", "placed_at.week"], timeRange: "last_12_weeks" }); return <RevenueByPlan data={data} />;
05mason

Mason keeps it fast, from your workload.

Every query that arrives tells Mason something. It pre-aggregates what gets grouped, partitions what gets filtered by time and indexes what gets looked up — then measures each against its target, and proposes removing what nothing reads any more.

mason · p95 latency, last 24 h target
revenue by plan · 90 days target 300 ms
▲ pre-aggregated by day
2.4 s 184 ms
this month's orders · one org target 150 ms
▲ partitioned by month
900 ms 62 ms
one order, by id target 50 ms
▲ indexed order_id
420 ms 9 ms
orders_by_status a rollup · weekly report
no queries in 14 days
proposed for removal you confirm
FAQ

What Mason asks of your database.

It's a logical replica, so the questions are the ones you'd ask of any: what it needs, what it costs, how far behind it runs, and what happens when it's gone.

What does it need from Postgres?

What any logical replica needs: wal_level = logical, a publication listing the tables and columns to copy, a replication slot, and a role with REPLICATION. No superuser, no extension, no schema change. Self-hosted, RDS, Aurora, Cloud SQL, Supabase and Neon all qualify.

Will it slow my primary down?

Streaming reads the WAL your primary already writes. On Postgres 16 or later Mason can stream from a standby instead, so the primary does nothing extra at all. History is copied once, in bounded windows that can be paused and resumed.

How far behind is it?

Rows are queryable about 15 seconds after they commit, and in every model that uses them about half a minute after — measured end to end against a 190-million-row production table taking a write every half second. Seconds behind, not the hours a nightly export costs.

What if Mason gets disconnected?

Your application never notices: Mason isn't in your write path. The replication slot keeps WAL on the primary until Mason catches up, so no change is lost. Set max_slot_wal_keep_size to cap how much it may hold — past that, Postgres drops the slot and Mason copies again.

Nothing to migrate

Your data is already in Postgres. Start there.

One command against a standby, and nothing on production changes. If it isn't faster than what you have today, you've lost an afternoon.