Skip to content

Import and anonymisation

dynoxide import loads a DynamoDB Export (the S3 export format, JSON Lines) into a Dynoxide database. It's how you get a realistic local dataset without hand-writing fixtures, and the anonymisation rules mean production data can be masked on the way in rather than after it has already landed on your laptop.

It runs in one of two modes. File mode (--output) writes a SQLite file, vacuums it, and optionally compresses it. Serve mode (--serve or --mcp) imports into memory and starts a server on top of it, leaving nothing on disk.

Basic import

dynoxide import \
  --source ./export-data/ \
  --schema schema.json \
  --output snapshot.db

--source points at a directory following the DynamoDB Export layout:

export-data/
├── Users/
│   └── data/
│       └── 00000000.json.gz
└── Orders/
    └── data/
        └── 00000000.json.gz

--schema takes DescribeTable JSON, which is what describe-table gives you:

aws dynamodb describe-table --table-name Users > schema.json

Restrict which tables come across with --tables:

dynoxide import --source ./export/ --schema schema.json --output snapshot.db \
  --tables Users,Orders

Anonymisation

Rules live in a TOML file. Each rule picks the items it applies to with match, names the attribute with path, and says what to do with action:

[[rules]]
match = "attribute_exists(email)"
path = "email"
action = { type = "fake", generator = "safe_email" }

[[rules]]
match = "attribute_exists(phone)"
path = "phone"
action = { type = "mask", keep_last = 4, mask_char = "*" }

[[rules]]
match = "attribute_exists(ssn)"
path = "ssn"
action = { type = "hash", salt_env = "ANON_SALT" }

[[rules]]
match = "attribute_exists(notes)"
path = "notes"
action = { type = "redact" }

[consistency]
fields = ["userId", "email"]
ANON_SALT=my-secret-salt dynoxide import \
  --source ./export/ \
  --schema schema.json \
  --rules rules.toml \
  --output anonymised.db

Actions

Action What it does
fake Replaces the value with generated data. Generators: safe_email, name, first_name, last_name, phone_number, address, company_name, sentence, word
mask Keeps the last N characters and masks the rest. Takes keep_last and mask_char
hash SHA-256 with a salt read from an environment variable. salt_env is required
redact Replaces the value with [REDACTED]
null Replaces the value with NULL

Keeping values consistent

Anonymising each attribute independently breaks joins: the same user ID becomes two different values in two tables, and the data stops making sense. Fields listed under [consistency].fields get the same anonymised value everywhere they appear in a single import run.

That works because hash is deterministic - same input plus same salt gives the same output. It also means the salt is what protects the data: reuse a salt across runs and the values stay stable across runs too, which is useful for reproducible fixtures and a liability if the salt leaks. Keep it in an environment variable, and out of the rules file.

Serve mode

Import straight into memory and serve it. Nothing is written to disk, so the data disappears with the process:

# HTTP server on the imported data
dynoxide import --source ./export/ --schema schema.json --serve --port 8000

# stdio MCP server, for a coding agent
dynoxide import --source ./export/ --schema schema.json --mcp

# Both at once
dynoxide import --source ./export/ --schema schema.json --serve --mcp --mcp-port 8100

--serve and --mcp both conflict with --output. Used alone, --mcp starts a stdio MCP server; combined with --serve it starts an HTTP one on --mcp-port (8100 by default). Anonymisation applies the same way, so --rules masks the data before it reaches the in-memory database.

Pairing serve mode with --mcp-read-only gives an agent a realistic, anonymised dataset it cannot write to.

Other options

Flag Description
--force Overwrite an existing output file
--compress Compress the output with zstd, producing snapshot.db.zst (requires --output)
--continue-on-error Carry on when a batch fails instead of aborting

Every flag is in the CLI reference.

Streams and imported data

If the --schema file carries a StreamSpecification, streams are enabled on the imported table automatically. Imported items themselves generate no stream records - bulk import bypasses recording for speed - so recording starts with the first write after the import completes. See Streams.