Architecture

How KiwiStack is built — from LXC containers to API conventions. Every component isolated, every boundary clear.

LXC Container Model

One component, one container. Upstream hidden, wrapper exposed.

LXC: kiwi-mail
UPSTREAM
Stalwart Mail Server
127.0.0.1:8080 — JMAP/HTTP
Unmodified binary. Localhost only. Hidden from network.
localhost proxy
KIWI WRAPPER
kiwi-mail
10.10.20.11:8443 — Kiwi API
Starts upstream. Health-checks. Proxies. Authenticates.
🔒
Isolated
Upstream binds to localhost only. Never reachable from outside the container.
🛡️
Controlled
Wrapper owns the lifecycle: start, health-check, proxy, shutdown.
🔌
Unified API
Only the Kiwi API is exposed. Upstream admin UIs and raw APIs are hidden.

LXC Strategy

How containers are structured, versioned, and managed throughout their lifecycle.

ISOLATION

One Service = One LXC

Each kiwi component runs in its own unprivileged LXC container. Inside: the upstream binary + the kiwi wrapper process. Nothing else.

BASE IMAGES

Minimal Templates

Alpine (musl) where upstream supports it, Ubuntu (glibc) where it doesn't. No desktop environment, no GUI, no unnecessary packages.

VERSIONING

Semantic + Snapshots

Each container is semantically versioned (e.g. kiwi-mail:0.3.1). ZFS snapshots before every update for instant rollback.

RESOURCES

cgroup Limits

Memory and CPU cgroup limits per container. Storage quotas. Each container gets its own network namespace on the private bridge.

LIFECYCLE
Create
Configure
Start
Health-check
Update
Snapshot
Migrate

Single-host: CLI/script manages lifecycle. Multi-host: the Vine orchestrates.

Base Image: Alpine vs Ubuntu

Alpine Linux (musl libc) gives smaller images and faster boots. But not every upstream runs on musl. We pick the lightest base that works.

Component Upstream Lang Base Why
kiwi-id Kanidm Rust Ubuntu OpenSSL + SQLite dynamically linked to glibc. No musl builds published, not tested on Alpine.
kiwi-store RustFS Rust Alpine Pure Rust, Apache 2.0. Static binary compiles cleanly against musl.
kiwi-mail Stalwart Rust Alpine pkg x86_64 release binary is glibc-linked, but Alpine testing repo has a musl-compiled package.
kiwi-chat Conduit / Tuwunel Rust Alpine Publishes fully static musl binaries with jemalloc. Alpine package available. Best musl support on this list.
kiwi-meet LiveKit Go Alpine Go server compiles to static CGO-less binary. Official image uses Alpine 3.x. (Agents SDK needs glibc, but we don't run agents.)
kiwi-work None (custom) Rust Alpine Axum + libsql embedded. Pure Rust, no C dependencies. Static musl binary.
kiwi-docs None (custom) Rust Alpine Axum + Loro CRDT. Pure Rust. Static musl binary.
kiwi-search Meilisearch Rust Ubuntu Official Docker image migrated from Alpine to Debian. musl may introduce search latency. Release binaries are glibc-only.
kiwi-sync Loro (embedded) Rust Alpine Loro is a linked Rust library, not a separate process. Pure Rust, no C deps.
kiwi-gate Pingora Rust Ubuntu BoringSSL C build system doesn't target musl. Alpine Docker PR was rejected by Cloudflare. No musl builds.
kiwi-mcp None (rmcp SDK) Rust Alpine Pure Rust, custom service with rmcp. Static musl binary.
kiwi-ui CopilotKit / AG-UI TypeScript Alpine Node.js runs on Alpine (node:alpine). Only non-Rust component in the stack.
Alpine 9 containers
Ubuntu 3 containers (kiwi-id, kiwi-search, kiwi-gate)

Filesystem Layout

Every LXC container follows the same directory structure. Upstream and Kiwi code are clearly separated.

WITH UPSTREAM (e.g. kiwi-mail)
/
├── opt/
│   ├── upstream/                # upstream binary & deps
│   │   ├── bin/stalwart-mail     # unmodified binary
│   │   ├── etc/config.toml       # upstream config
│   │   └── lib/                  # upstream shared libs
│   └── kiwi/                     # kiwi wrapper
│       ├── bin/kiwi-mail         # wrapper binary
│       └── version               # e.g. "0.3.1"
├── etc/
│   └── kiwi/
│       ├── config.toml           # wrapper config (bridge IP, ports)
│       └── jwt-public.pem        # Kiwi ID public key
├── var/
│   ├── lib/
│   │   ├── upstream/             # upstream data (DB, indices)
│   │   └── kiwi/                 # wrapper state (if any)
│   └── log/
│       └── kiwi/                 # structured logs (JSON)
└── run/
    └── kiwi.pid                  # PID file
CUSTOM SERVICE (e.g. kiwi-work)
/
├── opt/
│   └── kiwi/                     # no upstream directory
│       ├── bin/kiwi-work         # the service IS the binary
│       └── version               # e.g. "0.1.0"
├── etc/
│   └── kiwi/
│       ├── config.toml           # service config
│       └── jwt-public.pem        # Kiwi ID public key
├── var/
│   ├── lib/
│   │   └── kiwi/
│   │       └── data.db           # libsql embedded DB
│   └── log/
│       └── kiwi/                 # structured logs (JSON)
└── run/
    └── kiwi.pid                  # PID file
/opt/upstream/

Upstream binary and its dependencies. Never modified. Replaced atomically on upgrade. Absent in custom services.

/opt/kiwi/

Kiwi wrapper binary. Apache 2.0 code. Starts upstream, proxies requests, authenticates. Single static binary.

/var/lib/

Persistent data. Backed up by ZFS snapshots. Upstream and kiwi data are separated for clean upgrades.

Network Topology

Private bridge connects all containers. Three ports face the internet — everything else stays internal.

PROXMOX HOST Internet :443 HTTPS/WSS :25/:587 SMTP :3478 TURN/STUN Private Bridge — 10.10.20.0/24 Skin public-facing kiwi-gate kiwi-mcp kiwi-ui Seeds kiwi-mail kiwi-chat kiwi-meet Seeds continued kiwi-work kiwi-docs Core infrastructure kiwi-id kiwi-store libsql Flesh cross-cutting kiwi-search kiwi-sync RULES :443 HTTPS/WSS → Kiwi Gate (control plane: MCP, API, UI) :25/:587 SMTP → kiwi-mail direct (email delivery) :3478 TURN/STUN → kiwi-meet direct (WebRTC media bypass for performance) Internal traffic: HTTP/JSON on private bridge (no TLS needed)

The Proxy Pattern

Every Kiwi wrapper follows the same four-step lifecycle.

1

Start Upstream

Spawn the upstream process. Bind it to 127.0.0.1 only.

2

Health Check

Poll upstream with exponential backoff. Exit if it fails to start.

3

Serve Kiwi API

Bind Axum to the private bridge. Proxy, translate, authenticate.

4

Lifecycle

Forward SIGTERM. Drain connections. Graceful shutdown.

The wrapper does

Translate between Kiwi API and upstream API
Authenticate via Kiwi ID JWT tokens
Hide upstream admin UIs and raw APIs
Add structured logging and tracing spans
Expose GET /healthz for the orchestrator

The wrapper does NOT

Modify upstream source code
Link against upstream libraries (especially AGPL)
Persist its own state (state is in upstream or libsql)

Component Interactions

How requests flow from AI agents through the stack.

AI Agent Claude / GPT MCP client HTTPS Skin Layer Kiwi Gate TLS termination route to service Kiwi MCP tool registry JWT validation HTTP Kiwi Mail mail.* calendar.* contacts.* Stalwart (JMAP) 127.0.0.1:8080 Kiwi Work projects.* tasks.* board.* No upstream (custom Axum service) Kiwi Search search.global search.scope Meilisearch 127.0.0.1:7700 Private bridge — 10.10.20.0/24

The Workspace UI

Every service in one browser tab. The AI proposes, the human decides.

app.kiwistack.io 🍌 Inbox Drafts Sent Deleted Junk Archive Configuration Alice Martin Q1 report feedback Hey, I reviewed the numbers... Bob Chen Deployment checklist Can you double-check the... Carol Davis Design review notes Attached the mockups... Dave Wilson Lunch tomorrow? Eve Nakamura API docs update AM Alice Martin Feb 28, 2026 · 3:14 PM Q1 report feedback KIWI MAIL Auto-reply enabled "Thanks for your email! I'm out of office..." Calendar event created "Out of office" — Mar 1 – Mar 11 Apply Edit AI Assistant Set auto-reply, I'll be back in 10 days I'll set up an auto-reply for 10 days. Here's the configuration: ✉ Auto-reply Activation 📅 Calendar Event Creation Schedule a sprint review for tomorrow 2pm Found a 45-min slot at 2 PM that works for everyone. Here's the invite: 🎥 Schedule Meeting 📄 Edit Invite Tell it what you need... SERVICE BAR LIST PANE CONTENT PANE ASSISTANT PANEL MENUBAR

User Interface

Five named elements. Tell it what you need. It drafts the action. You approve.

Service Bar

Vertical icon bar on the left edge. Each service (Mail, Chat, Meet, Docs, Work, Store, Search) has its own color. One click swaps the entire workspace: menubar, list, content, and proposal context.

Menubar

Full-width contextual navigation for the active service (Inbox/Drafts/Sent for Mail, Channels/Direct for Chat, Board/List/Timeline for Work...). The AI can open the right menu item when navigating.

List Pane

Contextual list for the active service — inbox items, channels, meetings, documents, tasks, files, or search results. Selecting an item populates the Content Pane.

Content Pane

Full item view (email body, chat thread, document, kanban board). AI actions appear as a proposal card overlay — each AI-touched field gets a colored left border. Apply and Edit buttons live here; only the human can confirm.

🔌

Assistant Panel

Chat interface with conversation bubbles and CTA action buttons colored per target service. Can suggest actions, navigate between services, open a specific view — but never applies changes itself.

License Boundaries

Kiwi code is Apache-2.0. Upstream licenses are respected through clear boundaries.

Permissive

Apache-2.0 · MIT · BSD

Use freely. Link, embed, modify. No restrictions. Most of KiwiStack's dependencies fall here.

Use freely

MPL-2.0

Kanidm

File-level copyleft. Use unmodified binaries freely. If you modify Kanidm source files, those files must be shared under MPL-2.0.

File-level copyleft

AGPL-3.0

Stalwart · Proxmox Backup

Never link. Never embed. Communicate exclusively over HTTP. Network boundary keeps Kiwi code Apache-2.0.

Network boundary only

Hard Rules

1. Never use or extern crate an AGPL crate
2. Never copy AGPL source files into a Kiwi repository
3. MPL-2.0 files stay in their own crate — new files are Apache-2.0
4. Run cargo deny check licenses in CI

API Conventions

Internal HTTP/JSON between containers. MCP for AI agents. JWT for auth.

Internal APIs

HTTP/1.1 JSON on private bridge
No TLS internally (bridge is trusted)
/api/v1/{resource} base path
Bearer JWT in Authorization header

MCP Tools

Transport: HTTP + SSE (Streamable HTTP)
Naming: {service}.{action}
One endpoint, all tools (kiwi-mcp)
SDK: rmcp (Rust MCP implementation)
SUCCESS RESPONSE
{
  "data": { ... },
  "meta": {
    "request_id": "req_abc123",
    "timestamp": "2026-02-28T12:00:00Z"
  }
}
ERROR RESPONSE
{
  "error": {
    "code": "NOT_FOUND",
    "message": "Resource not found"
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Build Phases

Seven phases from foundation to commercial platform. Each unlocks the next.

1

Foundation

Kiwi ID Database Kiwi Mail Kiwi MCP Kiwi UI
2

Communication

Kiwi Chat Kiwi Meet Kiwi Search
3

Productivity

Kiwi Work Kiwi Docs Kiwi Sync
4

Storage

Kiwi Store
5

Interface

Kiwi Gate Kiwi Web
6

The Vine

Multi-Host WireGuard Mesh Scheduling
The Fruit (Phases 1-5): Complete single-host workplace — Apache 2.0
The Vine (Phase 6): Multi-host orchestration only — BSL 1.1