Skip to content

Export Import

Matt Dula edited this page Apr 18, 2026 · 2 revisions

Export / Import

Portable workspace dumps. Makes the "user owns their data" ethos real.

Round-trip

%%{init: {"look": "handDrawn", "theme": "dark"}}%%
flowchart LR
    A[(Workspace A<br/>source)] -->|GET /export| Doc[["nakatomi-&lt;slug&gt;-&lt;date&gt;.json"]]
    Doc -->|POST /import| B[(Workspace B<br/>target)]
    Doc -.->|stays portable| Any[any Nakatomi<br/>instance]

    subgraph Rules[Import rules]
        direction TB
        R1[natural-key match<br/>external_id → email/domain/slug]
        R2[UUIDs regenerated<br/>no pk collisions]
        R3[polymorphic refs rewritten<br/>notes, tasks, activities, memory_links]
    end
Loading

Export

Owners and admins only.

curl -s http://localhost:8000/export \
  -H "Authorization: Bearer nk_..." \
  -o nakatomi-dump.json

Response is a single JSON doc:

{
  "schema_version": 1,
  "nakatomi_version": "0.1.0",
  "exported_at": "2026-04-18T15:12:00+00:00",
  "workspace": {...},
  "custom_field_definitions": [...],
  "pipelines": [{"stages": [...], ...}, ...],
  "contacts": [...],
  "companies": [...],
  "deals": [...],
  "activities": [...],
  "notes": [...],
  "tasks": [...],
  "relationships": [...],
  "memory_links": [...],
  "files": [...],            // metadata only — bytes not included
  "webhooks": [{"secret": "[redacted on export]", ...}],
  "counts": {"contacts": 42, "companies": 18, "deals": 7, ...}
}

Pass ?include_timeline=true to also dump every timeline event. Omitted by default because it can be very large.

What's NOT included

  • File bytes — only metadata. Fetch via GET /files/{id} on the source side and re-upload on the target. File bytes as a tarball are on the roadmap.
  • Webhook secrets — redacted. Mint fresh ones on the target.
  • Operational state — audit log, webhook deliveries, ingest runs, idempotency keys. These are local to a deployment, not portable.
  • Users and memberships — users are global (not workspace-scoped). Import creates/updates CRM rows only; human access is configured separately on the target.

Import

Owners and admins only.

curl -X POST http://localhost:8000/import \
  -H "Authorization: Bearer nk_..." \
  -H "Content-Type: application/json" \
  -d @nakatomi-dump.json

...but wrap the doc in {"doc": {...}, "dry_run": false}:

jq -n --slurpfile d nakatomi-dump.json '{doc: $d[0], dry_run: false}' \
  | curl -X POST http://localhost:8000/import \
      -H "Authorization: Bearer nk_..." \
      -H "Content-Type: application/json" \
      --data @-

Response:

{
  "created": {"contacts": 42, "companies": 18, "deals": 7, ...},
  "updated": {"contacts": 0, ...},
  "skipped": {"notes": 1},
  "warnings": [
    "webhook 'slack-relay' imported without a secret — mint a new one via PATCH"
  ],
  "dry_run": false
}

Matching rules

Merge-upsert, not replace. The importer picks existing rows by stable natural keys:

Entity Match order
contact external_id → lowercased email
company external_id → lowercased domain
deal external_id (no fallback — deals without external_ids create)
activity external_id only
task external_id only
note (entity_type, entity_id, body) tuple — exact match or create
relationship full edge tuple (source, target, relation_type)
pipeline slug
stage (pipeline, slug)
custom_field_definition (entity_type, name)
webhook url
memory_link full tuple
file sha256

ID translation

Source UUIDs are not preserved. Two reasons:

  1. Two imports of the same data shouldn't collide by primary key.
  2. Cross-workspace portability means the target can be a different workspace on the same DB.

An id_map tracks source_id → new_id as rows land. When a deal references a contact, pipeline, or company that was just imported, the reference gets rewritten. Same for polymorphic (entity_type, entity_id) pairs on notes, tasks, activities, and memory_links.

Dry run

{"doc": {...}, "dry_run": true}

The importer runs the full pipeline inside a SAVEPOINT and rolls it back before returning. You get the counts + warnings you'd have seen for real.

Schema versioning

The top-level schema_version gates compatibility. Current: 1.

When the schema evolves, we'll add schema_version: 2 support and carry an upgrade path in services/importer.py. Old dumps will either continue to work or get a 422 with a clear migration message.

Use cases

  • Backupcurl /export > /backups/nakatomi-$(date +%F).json on a cron. Ship to S3.
  • Dev environment — export prod nightly, scrub PII, import into staging.
  • Migration — move from one Railway deploy to another.
  • Split workspaces — export one, re-import under a different workspace slug on the same instance.
  • Disaster recovery — last week's export lets you reconstruct the workspace (minus file bytes + timeline if you didn't opt in).

Clone this wiki locally