2-Minute Guide
The fastest way to understand DoubleDino is to run it.
In a couple of minutes, you'll start a Transformation Proxy, connect it to a database, run a real query, and see the data come back transformed.
No agents. No SDKs. No application changes.
Before you start
You need:
- The
doubledinobinary. - Access to a database you want to query.
- A destination database if you want to try Clone mode.
We'll use PostgreSQL for the first example.
Start a Transformation Proxy
DoubleDino is configured with environment variables.
For a first run, these are all you need:
export DD_MODE=proxy
export DD_SOURCE_URL='postgres://postgres:password@localhost:5432/postgres'
export DD_SECRET_KEY='replace-with-a-long-random-secret'
export DD_AUTH_KEY='replace-with-a-secret-api-key'
The four settings are:
| Setting | Purpose |
|---|---|
DD_MODE | Starts DoubleDino in proxy or clone mode |
DD_SOURCE_URL | Connection URL for the source |
DD_SECRET_KEY | Controls deterministic transformation |
DD_AUTH_KEY | API key required by clients |
Keep both keys private.
Now start DoubleDino:
./doubledino
The default HTTP port is 8080.
DoubleDino runs entirely inside your infrastructure:
In Proxy mode, DoubleDino connects to the source in read-only mode. Queries go to DoubleDino, and the returned data is transformed before it reaches the client.
Is it running?
Check /meta:
- cURL
- Node.js
- Bun
- Python
curl -G "http://localhost:8080/meta" \
-H "x-api-key: $DD_AUTH_KEY"
const response = await fetch('http://localhost:8080/meta', {
headers: {
'x-api-key': process.env.DD_AUTH_KEY
}
});
console.log(await response.json());
const response = await fetch('http://localhost:8080/meta', {
headers: {
'x-api-key': Bun.env.DD_AUTH_KEY
}
});
console.log(await response.json());
from urllib.request import Request, urlopen
import json
import os
request = Request(
"http://localhost:8080/meta",
headers={"x-api-key": os.environ["DD_AUTH_KEY"]}
)
with urlopen(request) as response:
print(json.load(response))
You should get:
{
"status": "online",
"type": "postgres"
}
That's it. DoubleDino is running.
Query production
Now query the source through DoubleDino.
For PostgreSQL, MySQL, and MariaDB, use the normal query language of the underlying database.
For example:
SELECT * FROM orders WHERE id=1
Send it to /query:
- cURL
- Node.js
- Bun
- Python
curl -G "http://localhost:8080/query" \
--data-urlencode 'q=SELECT * FROM orders WHERE id=1' \
-H "x-api-key: $DD_AUTH_KEY"
const url = new URL('http://localhost:8080/query');
url.searchParams.set('q', 'SELECT * FROM orders WHERE id=1');
const response = await fetch(url, {
headers: {
'x-api-key': process.env.DD_AUTH_KEY
}
});
console.log(await response.json());
const url = new URL('http://localhost:8080/query');
url.searchParams.set('q', 'SELECT * FROM orders WHERE id=1');
const response = await fetch(url, {
headers: {
'x-api-key': process.env.DD_AUTH_KEY
}
});
console.log(await response.json());
from urllib.parse import urlencode
from urllib.request import Request, urlopen
import json
import os
url = "http://localhost:8080/query?" + urlencode({
"q": "SELECT * FROM orders WHERE id=1"
})
request = Request(
url,
headers={"x-api-key": os.environ["DD_AUTH_KEY"]}
)
with urlopen(request) as response:
print(json.load(response))
The important part
Suppose the real production record looks like this:
{
"id": 1,
"user_id": 1,
"order_uid": "ORD-123456",
"total_amount": "460.29",
"internal_memo": "Internal tracking metadata routing checkpoint memo log."
}
DoubleDino can return:
{
"id": 1,
"user_id": 1,
"order_uid": "KLP-986307",
"total_amount": "591.87",
"internal_memo": "Ehfwfzjx tracking metadata niglwzp ybquybxupp iyyg his."
}
The query ran against the real production source.
The developer received transformed data.
The useful structure remains. Values are transformed rather than simply replaced with [REDACTED].
Deep JSON is transformed too
Real production data is rarely flat.
DoubleDino recursively handles JSON and JSONB values, including nested objects and arrays.
For example:
{
"customer": {
"name": "John Smith",
"contacts": [
{
}
]
}
}
The surrounding structure remains intact while values that are transformed are transformed at any depth:
{
"customer": {
"name": "Qksl Hyodn",
"contacts": [
{
}
]
}
}
This matters because sensitive data often lives several levels deep inside application payloads, metadata, arrays, or JSONB columns.
DoubleDino does not require you to flatten that data first.
The same value stays consistent
Transformation is deterministic.
If the same input is processed using the same secret key, the same transformed output is produced.
For example:
Production Transformed
users.id 123 users.id 847
orders.user_id 123 orders.user_id 847
payments.user_id 123 payments.user_id 847
The values above are examples only.
Because the transformation is consistent, relationships and joins can continue to work with transformed production data.
That means your developers can still work with realistic data rather than a collection of disconnected placeholders.
Proxy or Clone?
There are two ways to use DoubleDino.
Transformation Proxy gives you access to current production context without returning the original sensitive values.
Clone creates a standalone transformed database for development, testing, or reproducing an issue.
Use Proxy when you need
- Current production state.
- Live production context.
- To investigate something happening right now.
- To query production without receiving the original sensitive values.
Use Clone when you need
- A development database.
- QA or test data.
- A standalone production-like dataset.
- To reproduce a production issue locally.
Try Clone mode
Clone uses the same binary and the same deterministic transformation.
Set the mode to clone and add a destination:
export DD_MODE=clone
export DD_SOURCE_URL='postgres://postgres:password@localhost:5432/postgres'
export DD_DEST_URL='postgres://postgres:password@localhost:5432/destination'
export DD_SECRET_KEY='replace-with-a-long-random-secret'
Then start it:
./doubledino
DoubleDino reads the source, transforms the data, and writes it to the destination.
The source and destination must be the same type:
PostgreSQL → PostgreSQL
MySQL → MySQL
MariaDB → MariaDB
MongoDB → MongoDB
When the clone finishes, DoubleDino exits.
Other sources
DoubleDino supports:
The same basic model applies across the supported sources, although the query endpoint and syntax differ.
MongoDB
MongoDB uses source-specific query syntax with the collection name followed by a semicolon:
curl -G "http://localhost:8080/query" \
--data-urlencode 'q=users;{"is_active": true}' \
-H "x-api-key: $DD_AUTH_KEY"
Redis
Redis uses /redis:
curl -G "http://localhost:8080/redis" \
--data-urlencode "key=cache*" \
-H "x-api-key: $DD_AUTH_KEY"
Logs
Logs use /logs:
curl -G "http://localhost:8080/logs" \
--data-urlencode "lines=5" \
-H "x-api-key: $DD_AUTH_KEY"
PostgreSQL, MySQL, and MariaDB use /query with their normal query syntax.
JSON and JSONB values are handled recursively, including nested data.
One binary. One source. Transformed production data that stays useful.
Run it against production when you need the context.
Clone it when you need the database.