Architecture

NornWeave’s architecture uses thematic naming inspired by Norse mythology, with each component having a clear technical purpose.

Component Overview

ComponentThematic NameTechnical Purpose
Storage LayerUrdr (The Well)Database adapters for persistence
Ingestion EngineVerdandi (The Loom)Webhook processing, HTML to Markdown parsing
API & OutboundSkuld (The Prophecy)REST API, email sending, rate limiting
API GatewayYggdrasilCentral router connecting all providers
MCP ToolsHuginn & MuninnRead/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

  1. External user sends an email to your inbox address
  2. Email provider (Mailgun, etc.) calls the webhook endpoint
  3. Yggdrasil receives the webhook and routes to the appropriate handler
  4. Verdandi parses the raw email:
    • Converts HTML to Markdown
    • Removes reply cruft (“On Jan 1, John wrote:”)
    • Extracts attachments
  5. Verdandi determines threading using In-Reply-To and References headers
  6. Urdr stores the message and updates the thread

Agent Reading Flow

  1. AI Agent calls GET /v1/threads/{id} (or uses MCP)
  2. Yggdrasil routes the request
  3. Urdr fetches the thread and messages
  4. Response formatted as LLM-ready conversation

Agent Reply Flow

  1. AI Agent calls POST /v1/messages (or uses MCP send_email)
  2. Yggdrasil routes the request
  3. Skuld processes the outbound message:
    • Converts Markdown to HTML
    • Adds threading headers
    • Applies rate limiting
  4. Skuld sends via the configured provider
  5. 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

ColumnTypeDescription
idUUIDPrimary key
email_addressStringUnique email address
nameStringDisplay name
provider_configJSONProvider-specific metadata
created_atTimestampCreation time

Threads Table

ColumnTypeDescription
idUUIDPrimary key
inbox_idUUIDForeign key to Inbox
subjectTextThread subject
last_message_atTimestampLast activity
participant_hashStringHash of participants for grouping

Messages Table

ColumnTypeDescription
idUUIDPrimary key
thread_idUUIDForeign key to Thread
inbox_idUUIDForeign key to Inbox
provider_message_idStringMessage-ID header
directionEnumINBOUND or OUTBOUND
content_rawTextOriginal HTML/Text
content_cleanTextLLM-ready Markdown
metadataJSONHeaders, attachments, etc.
created_atTimestampMessage 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