Architecture
NornWeave’s architecture uses thematic naming inspired by Norse mythology, with each component having a clear technical purpose.
Component Overview
| Component | Thematic Name | Technical Purpose |
|---|---|---|
| Storage Layer | Urdr (The Well) | Database adapters for persistence |
| Ingestion Engine | Verdandi (The Loom) | Webhook processing, HTML to Markdown parsing |
| API & Outbound | Skuld (The Prophecy) | REST API, email sending, rate limiting |
| API Gateway | Yggdrasil | Central router connecting all providers |
| MCP Tools | Huginn & Muninn | Read/write tools for AI agents |
Thematic Reasoning
Urdr (The Well) represents “The Past.” The database holds the immutable history of what has already happened (logs, stored messages).
Verdandi (The Loom) represents “The Present” or “Becoming.” This engine processes incoming webhooks in real-time, parsing raw HTML into clean Markdown threads.
Skuld (The Prophecy) represents “The Future” or “Debt.” This layer handles what shall be done: sending emails, scheduling replies, and managing API credits/rate limits.
Yggdrasil is the central axis that connects all disparate worlds (Email Providers like Mailgun, SES) into one unified structure.
Huginn & Muninn are Odin’s ravens (Thought and Memory). These are the specific MCP tools that fly out to retrieve knowledge for the Agent.
System Architecture Diagram
flowchart TB
subgraph providers [Email Providers]
MG[Mailgun]
SES[AWS SES]
SG[SendGrid]
RS[Resend]
end
subgraph yggdrasil [Yggdrasil - API Gateway]
WH[Webhook Routes]
API[REST API v1]
end
subgraph verdandi [Verdandi - Ingestion Engine]
Parser[HTML Parser]
Sanitizer[Cruft Remover]
Threader[Threading Logic]
end
subgraph urdr [Urdr - Storage Layer]
PG[(PostgreSQL)]
SQLite[(SQLite)]
end
subgraph skuld [Skuld - Outbound Sender]
Sender[Email Sender]
RateLimiter[Rate Limiter]
end
subgraph mcp [Huginn & Muninn - MCP Tools]
Resources[Resources]
Tools[Tools]
end
providers --> WH
WH --> verdandi
verdandi --> urdr
API --> urdr
API --> skuld
skuld --> providers
mcp --> API
Data Flow
Inbound Email Flow
- External user sends an email to your inbox address
- Email provider (Mailgun, etc.) calls the webhook endpoint
- Yggdrasil receives the webhook and routes to the appropriate handler
- Verdandi parses the raw email:
- Converts HTML to Markdown
- Removes reply cruft (“On Jan 1, John wrote:”)
- Extracts attachments
- Verdandi determines threading using
In-Reply-ToandReferencesheaders - Urdr stores the message and updates the thread
Agent Reading Flow
- AI Agent calls
GET /v1/threads/{id}(or uses MCP) - Yggdrasil routes the request
- Urdr fetches the thread and messages
- Response formatted as LLM-ready conversation
Agent Reply Flow
- AI Agent calls
POST /v1/messages(or uses MCPsend_email) - Yggdrasil routes the request
- Skuld processes the outbound message:
- Converts Markdown to HTML
- Adds threading headers
- Applies rate limiting
- Skuld sends via the configured provider
- Urdr stores the sent message
Sequence Diagram
sequenceDiagram
participant User as External User
participant Provider as Email Provider
participant Yggdrasil as Yggdrasil - API Gateway
participant Verdandi as Verdandi - Ingestion Engine
participant Urdr as Urdr - Storage
participant Skuld as Skuld - Outbound Sender
participant Agent as AI Agent
Note over User,Agent: Inbound Email Flow
User->>Provider: Sends email
Provider->>Yggdrasil: POST /webhooks/{provider}
Yggdrasil->>Verdandi: Parse raw email
Verdandi->>Verdandi: HTML to Markdown
Verdandi->>Urdr: Store message and thread
Note over User,Agent: Agent Reading Flow
Agent->>Yggdrasil: GET /v1/threads/{id}
Yggdrasil->>Urdr: Fetch thread
Urdr-->>Agent: Markdown conversation
Note over User,Agent: Agent Reply Flow
Agent->>Yggdrasil: POST /v1/messages
Yggdrasil->>Skuld: Send email
Skuld->>Provider: Deliver via API
Provider->>User: Email delivered
Database Schema
The storage layer enforces this schema through the StorageAdapter:
Inboxes Table
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
email_address | String | Unique email address |
name | String | Display name |
provider_config | JSON | Provider-specific metadata |
created_at | Timestamp | Creation time |
Threads Table
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
inbox_id | UUID | Foreign key to Inbox |
subject | Text | Thread subject |
last_message_at | Timestamp | Last activity |
participant_hash | String | Hash of participants for grouping |
Messages Table
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
thread_id | UUID | Foreign key to Thread |
inbox_id | UUID | Foreign key to Inbox |
provider_message_id | String | Message-ID header |
direction | Enum | INBOUND or OUTBOUND |
content_raw | Text | Original HTML/Text |
content_clean | Text | LLM-ready Markdown |
metadata | JSON | Headers, attachments, etc. |
created_at | Timestamp | Message time |
Directory Structure
The codebase follows the thematic naming:
src/nornweave/
├── core/ # Shared interfaces and config
├── urdr/ # Storage layer
│ └── adapters/ # PostgreSQL, SQLite implementations
├── verdandi/ # Ingestion engine
│ ├── parser.py # HTML to Markdown
│ ├── sanitizer.py # Cruft removal
│ └── threading.py # Thread grouping
├── skuld/ # Outbound layer
│ ├── sender.py # Email sending
│ └── rate_limiter.py
├── yggdrasil/ # API gateway
│ ├── app.py # FastAPI application
│ └── routes/ # API endpoints
├── huginn/ # MCP resources
└── muninn/ # MCP tools