15 KiB
Messaging Center — Specification
Version: 0.1.0
Status: Draft
Author: James ⚡
Overview
Unified messaging hub that aggregates multiple communication channels into a single, normalized interface. OpenClaw (or any client) receives metadata via webhook, makes decisions, and sends commands back via REST API.
Key principle: Files never transit through OpenClaw. MC stores attachments locally, sends metadata only, executes routing commands.
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ MESSAGING CENTER │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Email │ │WhatsApp │ │ Signal │ │ SMS │ │Voicemail│ │
│ │ (IMAP) │ │(meow) │ │ (cli) │ │ (future)│ │(future) │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │ │ │
│ └───────────┴───────────┼───────────┴───────────┘ │
│ ▼ │
│ ┌───────────────────┐ │
│ │ Adapter Layer │ │
│ └─────────┬─────────┘ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Core Engine │ │
│ │ - Normalize messages │ │
│ │ - Store attachments (local) │ │
│ │ - Transcribe voice (Fireworks Whisper) │ │
│ │ - Queue outbound messages │ │
│ │ - Execute commands │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ REST API │ │ Webhooks │ │ Web GUI │ │
│ │ (commands) │ │ (events) │ │ (oversight) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ OPENCLAW │
│ - Receives message metadata (no files) │
│ - Makes decisions │
│ - Sends commands back to MC │
└─────────────────────────────────────────────────────────────────┘
Tech Stack
| Component | Technology |
|---|---|
| Language | Go 1.25 |
| Database | SQLite (embedded, simple) |
| Web GUI | Go + templ + htmx |
| Auth | OAuth 2.0 (local provider, or external like Authentik) |
| Transcription | Fireworks Whisper API |
| go-imap v2 | |
| whatsmeow | |
| Signal | signal-cli JSON-RPC (external daemon) |
Data Model
Message
type Message struct {
ID string `json:"id"`
Source string `json:"source"` // email, whatsapp, signal, sms, voicemail
Direction string `json:"direction"` // inbound, outbound
From Contact `json:"from"`
To Contact `json:"to"`
Timestamp time.Time `json:"timestamp"`
Type string `json:"type"` // text, voice, image, document, email
Subject string `json:"subject,omitempty"`
Body string `json:"body"` // text content or transcription
BodyHTML string `json:"body_html,omitempty"`
Attachments []Attachment `json:"attachments,omitempty"`
Status string `json:"status"` // received, processing, delivered, failed
Raw json.RawMessage `json:"raw,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type Contact struct {
ID string `json:"id"` // phone number, email, etc.
Name string `json:"name,omitempty"`
Type string `json:"type"` // phone, email, username
}
type Attachment struct {
ID string `json:"id"`
MessageID string `json:"message_id"`
Type string `json:"type"` // mime type
Filename string `json:"filename,omitempty"`
Size int64 `json:"size"`
LocalPath string `json:"-"` // not exposed via API
Transcription string `json:"transcription,omitempty"`
Status string `json:"status"` // stored, processing, routed, deleted
}
Command
type Command struct {
ID string `json:"id"`
Type string `json:"type"` // send, route, delete, archive, forward
Payload json.RawMessage `json:"payload"`
Status string `json:"status"` // pending, executing, completed, failed
Error string `json:"error,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
REST API
Base URL: http://localhost:8040/api/v1
Authentication
OAuth 2.0 Bearer tokens. All API endpoints require Authorization: Bearer <token>.
Token endpoint: POST /oauth/token
- Grant types:
client_credentials,authorization_code - Scopes:
messages:read,messages:write,commands:write,admin
Endpoints
Messages
| Method | Endpoint | Description | Scope |
|---|---|---|---|
| GET | /messages | List messages (paginated, filterable) | messages:read |
| GET | /messages/:id | Get single message | messages:read |
| GET | /messages/:id/attachments/:att_id | Download attachment | messages:read |
| POST | /messages | Send new message | messages:write |
Query parameters for GET /messages:
source- filter by source (email, whatsapp, signal)direction- inbound, outboundtype- text, voice, image, document, emailsince- ISO8601 timestampuntil- ISO8601 timestamplimit- max results (default 50, max 200)offset- pagination offset
POST /messages (send):
{
"channel": "whatsapp",
"to": "+17272252475",
"body": "Hello!",
"attachments": [
{"id": "att_xyz"} // reference existing attachment
]
}
Commands
| Method | Endpoint | Description | Scope |
|---|---|---|---|
| POST | /commands | Execute command | commands:write |
| GET | /commands/:id | Get command status | commands:write |
Command types:
route - Send attachment to external system
{
"type": "route",
"payload": {
"attachment_id": "att_xyz",
"destination": "docsys",
"options": {}
}
}
send - Send message (alias for POST /messages)
{
"type": "send",
"payload": {
"channel": "signal",
"to": "+17272252475",
"body": "Got it!"
}
}
delete - Delete message (email)
{
"type": "delete",
"payload": {
"message_id": "msg_abc"
}
}
archive - Archive message (email)
{
"type": "archive",
"payload": {
"message_id": "msg_abc"
}
}
forward - Forward attachment to chat
{
"type": "forward",
"payload": {
"attachment_id": "att_xyz",
"channel": "whatsapp",
"to": "+17272252475",
"caption": "Here's that file"
}
}
Channels
| Method | Endpoint | Description | Scope |
|---|---|---|---|
| GET | /channels | List configured channels | admin |
| GET | /channels/:name/status | Get channel status | admin |
| POST | /channels/:name/reconnect | Force reconnect | admin |
Admin
| Method | Endpoint | Description | Scope |
|---|---|---|---|
| GET | /health | Health check (no auth) | - |
| GET | /ready | Readiness check (no auth) | - |
| GET | /stats | System statistics | admin |
Webhooks
MC sends webhooks to configured endpoints on events.
Webhook payload:
{
"event": "message.received",
"timestamp": "2026-02-02T21:30:00Z",
"data": {
"id": "msg_abc123",
"source": "whatsapp",
"from": {"id": "+17272253810", "name": "Tanya"},
"type": "voice",
"body": "Hey, can you check on Sophia's appointment?",
"attachments": [
{"id": "att_xyz", "type": "audio/ogg", "size": 8420, "transcription": "Hey, can you check on Sophia's appointment?"}
]
}
}
Events:
message.received- New inbound messagemessage.sent- Outbound message deliveredmessage.failed- Outbound message failedchannel.connected- Channel connectedchannel.disconnected- Channel disconnectedcommand.completed- Command finishedcommand.failed- Command failed
Webhook auth: HMAC-SHA256 signature in X-MC-Signature header.
Web GUI
Dashboard for human oversight at http://localhost:8040/
Pages
- Dashboard - Overview of all channels, recent messages, system health
- Messages - Searchable/filterable message list with details
- Channels - Channel status, configuration, reconnect buttons
- Commands - Command history and status
- Settings - OAuth clients, webhooks, routing rules
Features
- Real-time updates via SSE (Server-Sent Events)
- Mobile responsive (Tailwind CSS)
- Dark mode
- Message search and filters
- Attachment preview (images, PDFs)
- Audio playback for voice messages
Configuration
# config.yaml
server:
host: 0.0.0.0
port: 8040
database:
path: /var/lib/messaging-center/mc.db
storage:
path: /var/lib/messaging-center/attachments
oauth:
issuer: http://localhost:8040
signing_key: ${MC_SIGNING_KEY}
access_token_ttl: 1h
clients:
- id: openclaw
secret: ${MC_OPENCLAW_SECRET}
scopes: [messages:read, messages:write, commands:write]
- id: admin
secret: ${MC_ADMIN_SECRET}
scopes: [messages:read, messages:write, commands:write, admin]
transcription:
provider: fireworks
api_key: ${FIREWORKS_API_KEY}
model: whisper-v3-turbo
webhooks:
- url: http://localhost:18789/hooks/message
secret: ${MC_WEBHOOK_SECRET}
events: [message.received, message.sent]
routing:
docsys:
url: http://localhost:8050/ingest
auth: bearer ${DOCSYS_TOKEN}
channels:
email:
enabled: true
imap:
host: 127.0.0.1
port: 1143
username: tj@jongsma.me
password: ${PROTON_BRIDGE_PASSWORD}
tls: starttls
smtp:
host: 127.0.0.1
port: 1025
username: tj@jongsma.me
password: ${PROTON_BRIDGE_PASSWORD}
tls: starttls
whatsapp:
enabled: true
data_dir: /var/lib/messaging-center/whatsapp
signal:
enabled: true
api_url: http://localhost:8080
account: +31634481877
Directory Structure
messaging-center/
├── cmd/
│ └── mc/
│ └── main.go
├── internal/
│ ├── adapter/
│ │ ├── adapter.go # Interface
│ │ ├── email/
│ │ ├── whatsapp/
│ │ └── signal/
│ ├── api/
│ │ ├── router.go
│ │ ├── messages.go
│ │ ├── commands.go
│ │ ├── channels.go
│ │ └── oauth.go
│ ├── core/
│ │ ├── engine.go
│ │ ├── message.go
│ │ ├── command.go
│ │ └── transcribe.go
│ ├── store/
│ │ ├── sqlite.go
│ │ └── migrations/
│ ├── web/
│ │ ├── handlers.go
│ │ ├── templates/
│ │ └── static/
│ └── webhook/
│ └── sender.go
├── config.yaml
├── go.mod
├── go.sum
├── Dockerfile
├── docker-compose.yml
└── README.md
Milestones
v0.1 — Foundation
- Project structure
- SQLite store with migrations
- Core engine (message normalization)
- REST API scaffold
- OAuth 2.0 (client_credentials)
- Health/ready endpoints
v0.2 — Email Adapter
- IMAP inbound (from mail-bridge)
- SMTP outbound
- Email-specific commands (delete, archive, move)
v0.3 — WhatsApp Adapter
- whatsmeow integration (from message-bridge)
- Voice transcription (Fireworks)
- Media handling
v0.4 — Signal Adapter
- signal-cli JSON-RPC client
- Bidirectional messaging
v0.5 — Web GUI
- Dashboard
- Message browser
- Channel status
- Settings
v0.6 — Webhooks & Routing
- Webhook sender with HMAC
- docsys routing
- Command execution
v1.0 — Production Ready
- Systemd service
- Documentation
- Docker image
- Backup/restore
Open Questions
-
OAuth provider: Build simple local provider, or integrate with external (Authentik)?
- Recommendation: Start with simple local provider, extract later if needed
-
Signal-cli: Keep as external daemon, or embed?
- Recommendation: Keep external (Java, complex), adapter calls JSON-RPC
-
Database: SQLite vs Postgres?
- Recommendation: SQLite for now (simpler), abstract for later swap
-
Message retention: How long to keep messages/attachments?
- Decision: Forever. Storage is cheap.
Ready to build. Starting with v0.1 foundation.