-
Notifications
You must be signed in to change notification settings - Fork 2
Architecture
Matt Dula edited this page Apr 18, 2026
·
2 revisions
A visual tour. See docs/ARCHITECTURE.md in the repo for the canonical copy; this page mirrors it with extra context.
%%{init: {"look": "handDrawn", "theme": "dark"}}%%
flowchart LR
Claude[Claude Desktop]
ChatGPT[ChatGPT]
Cursor[Cursor]
Perplexity[Perplexity]
CLI[curl / scripts]
Claude & ChatGPT & Cursor & Perplexity --> MCP
CLI --> REST
subgraph Nakatomi[Nakatomi CRM]
direction TB
REST[REST API]
MCP[MCP server]
Worker[Webhook worker<br/>background thread]
Schema[/schema + llms.txt + agent.json/]
end
REST & MCP & Worker --> PG[(Postgres)]
REST --> Storage[(Files<br/>local or S3)]
REST & MCP -.->|optional| Memory[Memory connectors]
Worker -.->|signed POST| Sub[Subscribers]
What runs in-process:
- REST API + MCP server (same FastAPI app, one DB pool)
- Webhook worker thread — started in
lifespan, claims pending rows viaSELECT ... FOR UPDATE SKIP LOCKED - Memory connector HTTP clients — created at startup per enabled adapter
What's external:
- Postgres (any 13+ will do)
- File storage (local volume or S3-compatible)
- Memory providers (DocDeploy, Supermemory, GBrain)
- The subscribers on the other end of your webhooks
%%{init: {"look": "handDrawn", "theme": "dark"}}%%
sequenceDiagram
participant Route
participant Emit as emit()
participant DB as Postgres
participant Worker
participant Target as subscriber
Route->>Emit: contact.created
Emit->>DB: INSERT WebhookDelivery<br/>status=pending
Note over Worker: poll every 5s
Worker->>DB: claim pending
Worker->>Target: POST signed
alt 2xx
Worker->>DB: status=succeeded
else 5xx / timeout
Worker->>DB: attempts++<br/>next_attempt_at += backoff
Note over Worker: after WEBHOOK_MAX_RETRIES<br/>→ status=dead
end
Retry backoff schedule: [2, 8, 30, 120, 300] seconds. Tunable via
WEBHOOK_MAX_RETRIES (default 3). See Webhooks for the wire format.
%%{init: {"look": "handDrawn", "theme": "dark"}}%%
flowchart LR
Agent[Agent /<br/>upload] -->|POST /ingest| Dispatch{format?}
Dispatch -->|csv| A1[csv adapter]
Dispatch -->|json| A2[json adapter]
Dispatch -->|vcard| A3[vcard adapter]
Dispatch -->|text| A4[text adapter]
A1 & A2 & A3 & A4 --> Norm[normalize]
Norm --> Dedupe[dedupe via external_id]
Dedupe --> CRM[(rows)]
Dedupe -.->|ingest.completed| Timeline[[timeline + audit]]
See Ingest for adapter details and custom mappings.
%%{init: {"look": "handDrawn", "theme": "dark"}}%%
flowchart LR
W[CRM mutation] --> Emit
Emit --> TL[(timeline)]
Emit --> WH[webhook queue]
Emit -.->|configured connectors| MC[DocDeploy /<br/>Supermemory /<br/>GBrain]
MC --> Link[(MemoryLink)]
R[memory_recall query] --> Fan[fan-out]
Fan --> MC
MC --> Merge[merge + score]
Merge --> Out[results + crm_links]
Every CRM write gets mirrored outbound to every enabled connector. Every recall fans out across them and decorates results with the crm_link references already on file.
%%{init: {"look": "handDrawn", "theme": "dark"}}%%
flowchart LR
A[(Workspace A)] -->|GET /export| Doc[["json doc"]]
Doc -->|POST /import| B[(Workspace B)]
Doc -.->|portable| Any[any Nakatomi]
See Export-Import for the doc schema and merge rules.
- One Postgres, one process. No Redis, no Kafka, no eventual consistency to explain. The webhook queue and rate-limit counters live in the same DB as the CRM rows.
-
Worker in-process for v1.
SKIP LOCKEDmakes it safe to scale horizontally. Pulling the worker into its own process is a zero-logic refactor when volume justifies it. -
Agents discover through
/schema. No SDK stubs; no generated clients. Ask, learn, act. - Memory connectors are plural. You can enable a cheap one (Supermemory) and a self-hosted one (GBrain) at once. Recall merges across both.
Repository · Issues · MIT licensed · maintained by Matt Dula