Database virtualization proxy with copy-on-write semantics. Use when setting up, running, debugging, or integrating Mori — a transparent proxy that routes reads to production and captures writes in a local Shadow database.
Mori is a transparent proxy that sits between your application and a production database. Your application connects to Mori using a standard database driver — same wire protocol, no code changes beyond swapping a connection string. Reads are served from the real production database. Writes are captured in a local database instance (the Shadow) that starts as a schema-only clone of production. The result: developers test against real production data, mutate freely, and never risk the production database. Break something, reset, start over.
These hold at all times, without exception.
Prerequisites: Go 1.21+, Docker (for Shadow containers — all engines except SQLite and DuckDB).
| Term | Definition |
|---|---|
| Prod | Upstream production database. Read-only from Mori's perspective. Source of truth for unmodified data. |
| Shadow | Local database instance (same engine/version as Prod). Holds all local mutations. Starts schema-only with zero rows. |
| Proxy | The Mori process. Intercepts wire protocol traffic, classifies queries, routes them, merges results. |
| Delta Map | Set of (table, primary_key) pairs identifying locally modified rows. When a row is delta, Shadow holds the authoritative version. |
| Tombstone Set | Set of (table, primary_key) pairs identifying locally deleted rows. Tombstoned rows are filtered from Prod results. |
| Schema Registry | Tracks structural differences between Shadow and Prod schemas introduced by local DDL (added/dropped/renamed columns, type changes). |
| Hydration | Copying a row from Prod into Shadow so it can be mutated locally. Happens transparently on UPDATE/DELETE of Prod-only rows. |
| Merged Read | A read where results from both Prod and Shadow are combined, with delta/tombstoned rows filtered from Prod results. |
| Engine | Protocol | Shadow |
|---|---|---|
| PostgreSQL | pgwire | Docker container |
| CockroachDB | pgwire | Docker container |
| MySQL | MySQL wire | Docker container |
| MariaDB | MySQL wire | Docker container |
| MS SQL Server | TDS | Docker container |
| SQLite | pgwire (embedded) | Local file (no Docker) |
| DuckDB | pgwire (embedded) | Local file (no Docker) |
| Redis | RESP | Docker container |
| Firestore | gRPC | Emulator container |
go build -o mori ./cmd/mori
Interactive mode walks through engine, provider, and credentials:
./mori init
Non-interactive with a connection string:
./mori init --from "postgres://user:pass@host:5432/mydb?sslmode=require" --name my-db
This saves the connection to mori.yaml. No containers or connections are created yet.
./mori start [connection-name] --port 9002 --verbose
On first start, Mori creates the Shadow database (schema-only clone of Prod), computes sequence offsets, and begins accepting connections. If only one connection exists, the name is optional.
Swap your application's connection string to localhost:<proxy-port>. Same credentials, same database name. The app won't know the difference.
mori init Save connection config to mori.yaml
|
mori start Create Shadow (first run), start proxy, accept connections
|
[app runs] CRUD operations routed transparently
|
mori stop Persist state, shut down proxy
|
mori reset Wipe Shadow data + metadata, clean slate
mori initAdd a database connection to mori.yaml.
| Flag | Default | Description |
|---|---|---|
--from, -f | — | Connection string (non-interactive mode) |
--name | — | Connection name (used with --from) |
--image | — | Custom Docker image for the Shadow container (persisted in mori.yaml) |
mori start [name]Start the proxy for a named connection. If only one connection exists, name is optional.
| Flag | Default | Description |
|---|---|---|
--port, -p | 9002 | Port for the proxy to listen on |
--verbose | false | Log all intercepted queries and routing decisions |
--mcp | false | Enable MCP server for AI tool integration |
--mcp-port | 9000 | Port for the MCP server |
mori stopGracefully shut down the proxy. State is persisted automatically.
mori resetWipe all local mutations and restore a clean view of production.
| Flag | Default | Description |
|---|---|---|
--hard | false | Also re-sync schema from production (use when Prod schema changed since init) |
Soft reset: truncates Shadow tables, clears Delta Map, Tombstone Set, Schema Registry, resets sequence offsets. Hard reset: drops and re-creates Shadow tables from current Prod schema.
mori statusDisplay current state: engine info, connection details, delta row counts, tombstone counts, schema diffs, sequence offsets.
mori inspect <table>Deep dive on a single table's state — delta rows, tombstoned rows, schema differences.
mori logStream proxy activity (queries, routing decisions, timing).
| Flag | Default | Description |
|---|---|---|
--tail | — | Show last N log entries |
mori lsList all configured connections from mori.yaml.
mori rm <name>Remove a connection from mori.yaml.
mori configView or edit the current configuration.
mori dashLaunch the interactive TUI dashboard.
Every incoming query follows this path:
App -> Proxy (wire protocol) -> Classifier -> Router -> Execute (strategy) -> Merge -> Return
The Classifier parses each query to determine its operation type, affected tables, and extractable primary keys. The Router uses this classification plus current delta/tombstone state to pick a routing strategy.
| Strategy | When Used | What Happens |
|---|---|---|
| ProdDirect | SELECT on tables with no deltas/tombstones | Forward to Prod, return result directly. Zero overhead. |
| MergedRead | SELECT on tables with deltas or tombstones | Query both Prod and Shadow, filter delta/tombstoned rows from Prod, merge results. |
| JoinPatch | JOIN involving tables with deltas | Execute JOIN on Prod, patch delta columns from Shadow. |
| ShadowWrite | INSERT | Execute on Shadow only. Uses offset-based sequences to avoid PK collisions with Prod. |
| HydrateAndWrite | UPDATE on a Prod-only row | Copy the row from Prod to Shadow (hydration), then apply the UPDATE on Shadow. |
| ShadowDelete | DELETE | Delete from Shadow (if present), add to Tombstone Set so the row is filtered from Prod reads. |
| ShadowDDL | ALTER, CREATE, DROP | Execute DDL on Shadow only. Update Schema Registry to track divergence. |
| Transaction | BEGIN, COMMIT, ROLLBACK | Open read-only Prod transaction + read-write Shadow transaction. Commit promotes staged deltas. Rollback discards them. |
Understanding routing helps explain query behavior. For example, a SELECT on a table you've never modified goes straight to Prod (ProdDirect). Once you UPDATE a row in that table, subsequent SELECTs use MergedRead to combine Prod and Shadow results.
Mori stores all state in a .mori/ directory at the project root.
.mori/
mori.yaml # Connection configs (engine, host, port, user, password, database, ssl_mode)
config.json # Runtime config (prod connection, shadow port, active connection)
shadow/ # Shadow container metadata (Docker IDs, port mappings)
state/
delta.json # Delta Map: { "table_name": ["pk1", "pk2", ...] }
tombstones.json # Tombstone Set: { "table_name": ["pk1", "pk2", ...] }
schema_registry.json # Schema divergence tracking (added/dropped/renamed columns per table)
tables.json # Table metadata (PK columns, PK types per table)
sequences.json # Sequence offsets per table
log/ # Query logs, routing decisions
state/delta.json): shows which tables have locally modified rows and which PKs. If a table appears here, reads on it use MergedRead.state/tombstones.json): shows locally deleted rows. These PKs are filtered from Prod results.state/schema_registry.json): shows DDL differences. Non-empty means Shadow schema diverges from Prod.state/tables.json): PK columns and types per table. Used by the classifier and merge engine.state/sequences.json): offset values for auto-increment columns. Prevents INSERT PK collisions.Mori includes an MCP (Model Context Protocol) server for AI agent integration. Start it alongside the proxy:
./mori start my-db --mcp --mcp-port 9000
The MCP server registers engine-specific tools based on your database type:
SQL engines (PostgreSQL, CockroachDB, MySQL, MariaDB, MSSQL, SQLite, DuckDB):
db_query — Execute a SQL query. Parameter: query (string, required).Redis:
redis_command — Execute any Redis command (free-form).redis_get — Get the value of a key.redis_hgetall — Get all fields and values of a hash.redis_keys — Find keys matching a pattern.Firestore:
firestore_get — Retrieve a document by collection and document ID.
firestore_list — List documents in a collection (default 25, max 100).
firestore_query — Query with field filters (==, !=, <, <=, >, >=, in, array-contains).
Endpoint: http://127.0.0.1:9000/mcp
Behavior: All tools go through the proxy's full routing/merge logic, same as application connections. Reads from Prod, writes to Shadow.
To connect an MCP client (e.g., Claude Code), add to your MCP settings:
{
"mcpServers": {
"mori": {
"url": "http://127.0.0.1:9000/mcp"
}
}
}
docker ps.--port to pick another..mori/ directory: Run mori init first to configure a connection.mori stop before starting a different connection.mori status to see which tables have deltas/tombstones. Tables with deltas use MergedRead instead of ProdDirect.mori inspect <table> to see exactly which rows are modified/deleted locally.--verbose when starting the proxy to log every query's classification and routing decision.mori log for recent query activity and any errors.state/sequences.json. Shadow sequences start at max(prod_max * 10, prod_max + 10_000_000) to avoid PK collisions. If Prod has grown significantly since init, offsets may need recalculating via mori reset --hard.mori reset --hard to re-sync.docker ps -a | grep mori-shadow # List Mori Shadow containers
docker logs <container-id> # Check container logs
docker restart <container-id> # Restart a stuck container
--hard resetUse mori reset --hard when:
mori init (new tables, altered columns)Use soft mori reset (no flag) for routine testing cycles — it's faster and preserves the schema.
LIMIT $1 is not rewritten for over-fetching during merged reads.UPDATE ... WHERE id IN (...) with many IDs) is slower than single-row operations.docs/mori.md — covers routing logic, merge engine internals, design decisions, and the 4-layer write guard in detail.docs/testing.md — tracks E2E test coverage per engine.docs/mintlify/ — MDX-based docs covering concepts, engine guides, CLI reference, and advanced topics.