This document describes the structure and design principles of the WPD Message Gateway following the current refactored architecture.
The gateway is designed to serve two completely different use cases from a single codebase:
┌──────────────────────────────────────────────────────────────────────┐
│ MODE 1 — Go Package (Embedded SDK) │
│ │
│ go get github.com/weprodev/wpd-message-gateway │
│ │
│ ┌────────────────┐ ┌─────────────────────┐ │
│ │ Your Go App │────▶│ pkg/gateway.New() │ │
│ └────────────────┘ │ (no server, no DB) │ │
│ └──────┬──────────────┘ │
│ │ uses registry │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ Provider (Mailgun, etc)│ │
│ └─────────────────────────┘ │
│ │
│ Config lives in your code. No infrastructure required. │
└──────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│ MODE 2 — HTTP Server (Full Stack) │
│ │
│ git clone → make start │
│ │
│ ┌───────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ React UI │ │ REST API │ │ Any Language │ │
│ │ (Portal) │ │ /api/v1/* │ │ HTTP client │ │
│ └─────┬─────┘ └──────┬───────┘ └────────┬────────┘ │
│ │ │ │ /v1/email │
│ └───────────────┴───────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────┐ │
│ │ Go HTTP Server │ │
│ │ (Echo framework) │ │
│ └──────────┬─────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ PostgreSQL DB │ ← single source of truth │
│ │ workspaces │ for ALL configuration │
│ │ integrations │ (provider credentials, │
│ │ api_keys │ workspace settings, │
│ │ templates │ members, logs, etc.) │
│ │ workspace_settings │ │
│ └─────────────────────┘ │
│ │
│ Config lives in PostgreSQL — configured via the React UI. │
└──────────────────────────────────────────────────────────────────────┘
For Go applications that want to send messages without running any infrastructure.
import (
"github.com/weprodev/wpd-message-gateway/pkg/contracts"
"github.com/weprodev/wpd-message-gateway/pkg/gateway"
)
// Pass provider config directly — no DB, no server
gw, err := gateway.New(gateway.Config{
DefaultEmailProvider: "mailgun",
EmailProviders: map[string]gateway.EmailConfig{
"mailgun": {
CommonConfig: gateway.CommonConfig{APIKey: "key-xxx"},
Domain: "mg.example.com",
FromEmail: "noreply@example.com",
},
},
})
gw.SendEmail(ctx, &contracts.Email{
To: []string{"user@example.com"},
Subject: "Hello",
HTML: "<h1>Hi!</h1>",
})Key characteristics:
- No PostgreSQL required
- No HTTP server required
- Provider credentials live in your application config (env vars, secrets manager, etc.)
- Uses the same provider registry and contracts as the server mode
- Memory provider available for testing (no external dependencies)
┌─────────────────────────────────────────────────────────────────┐
│ External World │
│ ┌────────────────────┐ ┌──────────────────────────────┐ │
│ │ React Portal UI │ │ HTTP Clients (any language) │ │
│ │ (config, auth, │ │ POST /v1/email │ │
│ │ logs, templates) │ │ POST /v1/sms etc. │ │
│ └────────────────────┘ └──────────────────────────────┘ │
└──────────────┬──────────────────────────┬───────────────────────┘
│ /api/v1/* │ /v1/*
│ (Portal JWT) │ (Workspace JWT or API key)
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Presentation Layer │
│ (internal/presentation/) │
│ ┌─────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Router │ │ PortalHandler │ │ GatewayHandler │ │
│ │ │ │ (auth, WS mgmt)│ │ (send email, │ │
│ └─────────────┘ │ InboxHandler │ │ sms, push, │ │
│ │ (captured msgs)│ │ chat) │ │
│ └────────┬────────┘ └────────┬────────┘ │
└─────────────────────────────┼────────────────────┼──────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ Core Layer │
│ (internal/core/) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ PortalService │ │
│ │ Email+password auth · Workspaces · Members · API keys │ │
│ │ Integrations · Templates · Settings · Logs │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ GatewayService │ │
│ │ SendEmail() · SendSMS() · SendPush() · SendChat() │ │
│ │ Reads message_dispatch_mode from workspace_settings │ │
│ │ Reads provider credentials from integrations table │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Ports (Interfaces) │ │
│ │ EmailSender · SMSSender · PushSender · ChatSender │ │
│ │ Repository interfaces for all DB entities │ │
│ └──────────────────────────────────────────────────────────┘ │
└──────────────────────────────┬──────────────────────────────────┘
│ implements
▼
┌─────────────────────────────────────────────────────────────────┐
│ Infrastructure Layer │
│ (internal/infrastructure/) │
│ ┌─────────────────────┐ ┌───────────────────────────────┐ │
│ │ Inbox UI Capture │ │ Repository Implementations │ │
│ │ ───────────────── │ │ ──────────────────────────── │ │
│ │ inbox/ │ │ postgres/ │ │
│ │ (portal store) │ │ WorkspaceRepository │ │
│ │ │ │ APIKeyRepository │ │
│ └──────────┬──────────┘ │ IntegrationRepository │ │
│ │ │ ... (all entities) │ │
│ ┌────────▼──────┐ └───────────────────────────────┘ │
│ │ Memory Store │ │
│ │ (in-process │ │
│ │ inbox for │ │
│ │ dev/testing)│ │
│ └───────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────┐
│ PostgreSQL Database │
│ (all persistent data)│
└────────────────────────┘
Portal accounts use email + password (passwords are stored hashed). All management requests to /api/v1/workspaces/:wid/... require a valid JWT token (Authorization: Bearer <portal-jwt>) and are authorized via Role-Based Access Control (RBAC) powered by the wpd-gogate library.
The core service layer decouples from wpd-gogate by depending on the port.AuthorizationGate interface. The implementation adapter lives in internal/infrastructure/authgate/gate_adapter.go.
- admin: Full access to all portal permissions. Assigned automatically to the workspace creator.
- member: Manage workspace resources (integrations, templates, settings, API keys, inbox) but cannot modify workspace metadata, manage members, or send invitations. See
database/seeds/001_seed_permissions.sqlfor the exact permission matrix. - viewer: Read-only access to workspace resources (
*.readpermissions). Assigned explicitly via membership or invitation.
Roles are global in the roles table (wpd-gogate schema) and scoped per workspace through workspace_members.role_id and model_has_roles.team_id.
Public workspace guests (authenticated users who are not members of a public workspace) receive a narrower guest set — workspaces.read and templates.read only — via domain.PublicGuestPermissions. The RBAC middleware bypasses gogate checks only for those permissions; all other routes still require membership and role-based permissions. Write operations always require explicit membership with sufficient permissions.
Sending messages via /v1/* requires workspace API credentials and the workspace unique key:
POST /v1/email
Authorization: Bearer <workspace-jwt-token>
OR
X-Api-Client-Id: wk_abc123
X-Api-Client-Secret: <secret>
X-Workspace-Key: <workspace-id-or-slug>
The application enforces two distinct authorization streams to balance security and usability:
- Portal REST API: Restricts access using JWT tokens combined with fine-grained
wpd-gogatepermission middleware (RequirePermission). - Inbox SSE API (
/api/v1/workspaces/:wid/inbox/...): Restricts access using JWT tokens combined with a direct database workspace membership check (RequireWorkspaceMember) and the workspace API key (RequireWorkspaceAPIKey). This design allows client-side SDKs, SSE event streaming, and automation runners to interact with the simulated inbox without requiring full portal RBAC configuration.
When running as an HTTP server, PostgreSQL holds all configuration. There are no YAML files for provider credentials.
To avoid docs drifting from the real schema, treat these as the sources of truth:
- Migration SQL:
database/migrations/*.up.sql - ERD:
docs/assets/database-schema.drawio
This doc intentionally does not duplicate full column lists.
Provider credentials are stored AES-encrypted in the integrations table. Configure via the Integrations page in the Portal UI or POST /api/v1/workspaces/:wid/integrations.
The React Portal (frontend/) includes:
| Area | Pages |
|---|---|
| Auth | Register, sign in |
| Workspaces | List, create, join |
| Logs | Overview (all channels) + per-channel tabs (SMS, push, chat) |
| Captured inbox browser, templates, Get started curl guide | |
| Integrations | Connect and manage providers |
| Settings | General, API keys, message dispatch |
API-only today: SMS/push/chat captured-message browsers (email inbox only), internal inbox ingest.
Each workspace controls outbound routing and inbox content capture with two orthogonal settings:
| Setting | Values | Purpose |
|---|---|---|
message_dispatch_mode |
memory (default) | provider |
Where the message is routed |
store_message_content |
false (default) | true |
Whether message bodies are captured in the portal inbox (in-process) |
Portal Settings → Message Dispatch exposes two controls that map directly to these keys:
| UI control | Setting key | Values |
|---|---|---|
| Dispatch mode (radio) | message_dispatch_mode |
memory | provider |
| Store message content (checkbox) | store_message_content |
false (off) | true (on) |
Configured via PATCH /api/v1/workspaces/:wid/settings (Portal Settings → Message Dispatch):
PATCH /api/v1/workspaces/:wid/settings
{ "message_dispatch_mode": "provider", "store_message_content": "true" }
Request logs: Every API send appends a row to message_request_logs (operational tracing with correlation request_id). Payload bodies are stored in a separate table (message_request_payloads) to prevent table bloat and enforce independent data privacy/retention policies.
HTTP Client (your app)
│
│ POST /v1/email
│ Authorization: Bearer <workspace-jwt>
│ X-Workspace-Key: <workspace-uuid>
│ { "to": [...], "subject": "...", "html": "..." }
▼
APIKeyAuthMiddleware
│ Validates workspace-jwt OR client_id+secret
│ Resolves workspace from X-Workspace-Key (UUID or slug)
│ Sets workspace_id in request context
▼
GatewayHandler.HandleSendEmail()
│ Reads workspace_id from context
▼
GatewayService.SendEmail(ctx, workspaceID, email)
│ Reads message_dispatch_mode + store_message_content from DB
│
├── memory → Memory Provider (in-process RAM)
│ │
│ ▼
│ Portal Inbox (SSE + REST)
│
├── provider → reads integrations table for workspace+channel
│ Decrypts AES config
│ Instantiates provider via registry
│ → sends to Mailgun/etc
│
└── store_message_content=true → also persists content via inbox writer
Your Go App
│
│ gw.SendEmail(ctx, email)
▼
pkg/gateway.Gateway
│ No DB lookup — config was passed to New()
▼
Provider Registry
│ Finds factory for DefaultEmailProvider name
▼
Provider (Mailgun/Memory/etc)
│ Sends message
▼
contracts.SendResult
Every entity in the system is scoped to a workspace:
Workspace "myapp"
├── Members (users with roles: admin, member, viewer)
├── API Keys (for machine-to-machine sends)
├── Integrations (provider credentials per channel)
│ ├── email: mailgun { api_key: "...", domain: "..." }
│ └── sms: (not configured)
├── Settings { message_dispatch_mode: "provider", store_message_content: "false" }
├── Templates (email HTML templates)
└── Message Logs (request audit trail)
A user can be a member of multiple workspaces. Each workspace has its own provider config, API keys, members, messages, etc.
wpd-message-gateway/
├── cmd/
│ └── server/ # HTTP server entry point
│ └── main.go
│
├── configs/ # Minimal server config (NO provider credentials)
│ ├── local.yml # Port, JWT secret
│ └── local.example.yml
│
├── database/
│ ├── migrations/ # SQL migrations (schema evolution)
│ └── seeds/ # Optional demo data
│
├── internal/ # Private application code
│ ├── app/ # Bootstrap and wiring
│ │ ├── config.go # Server config (port, JWT)
│ │ ├── wire.go # Dependency injection — wires all layers
│ │ ├── validation.go # Config validation
│ │ └── imports.go # Blank imports to trigger provider init()
│ │
│ ├── core/ # Business logic (domain-pure)
│ │ ├── domain/ # Types: Workspace, User, APIKey, Integration...
│ │ ├── port/ # Interfaces: EmailSender, repositories...
│ │ ├── service/
│ │ │ ├── gateway_service.go # Message dispatch (reads DB integrations)
│ │ │ └── portal_service.go # Auth, workspaces, API keys, etc.
│ │ └── authjwt/ # JWT sign/parse
│ │
│ ├── infrastructure/ # DB + provider + auth adapters
│ │ ├── authgate/ # wpd-gogate RBAC adapter implementation
│ │ ├── inbox/ # In-process UI capture store
│ │ ├── logger/ # Application-wide context-aware logger
│ │ └── repository/
│ │ └── postgres/ # All PostgreSQL repository implementations
│ │
│ ├── presentation/ # HTTP layer
│ │ ├── router.go # All route definitions
│ │ ├── handler/
│ │ │ ├── gateway_handler.go # POST /v1/* (send messages)
│ │ │ ├── portal_handler.go # /api/v1/* (auth, workspace CRUD)
│ │ │ ├── portal_inbox_handler.go # /api/v1/workspaces/:wid/inbox/*
│ │ │ └── send_helper.go # Shared message dispatcher & logger
│ │ └── middleware/
│ │ ├── auth.go # API key auth for /v1/*
│ │ ├── portal_jwt.go # Portal JWT for /api/v1/*
│ │ ├── portal_inbox_auth.go # Inbox-specific auth (JWT + member + key)
│ │ └── rbac.go # wpd-gogate permission validator
│ │
│
├── pkg/ # Public packages (imported by external Go apps)
│ ├── contracts/ # Message types: Email, SMS, PushNotification...
│ ├── auth/ # Hash utilities (shared between SDK and server)
│ ├── encryption/ # AES encryption (for DB-stored provider config)
│ ├── provider/ # Provider Adapters (mailgun, memory, etc)
│ ├── registry/ # Provider factory registry
│ └── gateway/ # Embedded SDK — gateway.New() for pure Go usage
│ ├── gateway.go # Gateway struct, SendEmail/SMS/Push/Chat
│ ├── config.go # Config struct + New() constructor
│ └── errors.go
│
├── frontend/ # React Portal UI (Vite + TypeScript + Tailwind)
│ └── src/
│ ├── features/ # auth, workspaces, inbox (message logs)
│ └── components/ # design system components
│
└── docs/ # Documentation
A workspace is the primary isolation unit. All resources (API keys, integrations, templates, members, logs) are scoped to a workspace. Your applications connect to a specific workspace. You can have multiple workspaces for different products, environments, or teams.
In server mode, provider credentials (Mailgun API keys, etc.) are stored encrypted in the integrations table. Configure them via the Portal Integrations page or the REST API.
Ports define capabilities — the "What" (Send Email), not the "How" (using Mailgun API).
- Sender interfaces:
pkg/contracts/—EmailSender,SMSSender,PushSender,ChatSender - Server ports:
internal/core/port/— repository contracts,InboxReader,InboxWriter - Message payloads (
Email,SMS,SendResult, …): defined only inpkg/contracts/— ports and services use those types; they are not duplicated underinternal/core/domain/. - Benefit: Providers are interchangeable — any implementation that satisfies the interface works
Providers register via Go's init() mechanism (Open/Closed Principle):
Provider Package Provider Registry
(register.go) ──init()──▶ (pkg/registry)
RegisterEmailProvider("mailgun", factory)
Adding a new provider requires NO changes to existing code.
Only create new files in pkg/provider/<name>/
The memory provider captures messages in process RAM. It's always available — no external service needed. Combined with workspace settings, you can:
- Use
message_dispatch_mode=memoryfor local development (captured messages via inbox REST API / Bruno) - Use
message_dispatch_mode=providerin production (messages go to the real provider) - Set
store_message_content=trueto persist message bodies regardless of routing mode
The React Portal runs at portal.ui_port (default 10104) when the server starts.
Portal UI: sign in, manage workspaces, view request logs, browse the email captured inbox, manage email templates, configure integrations and settings (general, API keys, message dispatch).
API-only: team member management, SMS/push/chat captured-message browsers, internal inbox ingest.
To enable robust trace tracking across all layers, the gateway employs an end-to-end correlation ID pipeline:
- Correlation Generation: Echo's
RequestIDmiddleware generates a unique request UUID for every incoming HTTP request, placing it in theX-Request-IDheader. - Context Propagation: Router middleware copies the request ID into
context.Contextvialogger.WithRequestID. API key auth also callslogger.WithWorkspacesoworkspace_idandapi_key_idappear on all/v1/*logs. Send handlers addchannelandprovider. - Structured slog: go-pkg
ContextExtractorsappend correlation fields to everyslog.*Contextrecord (request_id,workspace_id,api_key_id,channel,provider). - Audit Trail:
SendHelperpersistsrequest_idon every row inmessage_request_logs, linking HTTP requests to the operational audit trail.
- Dependency Rule: Dependencies point inward. Infrastructure depends on Core, never the reverse.
- Testability: Core logic can be tested without HTTP, without PostgreSQL, without external APIs.
- Ports & Adapters: The Core defines interfaces (ports); Infrastructure provides implementations (adapters).
| Principle | Application |
|---|---|
| Single Responsibility | Each provider handles one vendor. GatewayService handles routing. |
| Open/Closed | Add new providers without modifying existing code. |
| Liskov Substitution | Any EmailSender implementation works interchangeably. |
| Interface Segregation | Separate interfaces for Email, SMS, Push, Chat. |
| Dependency Inversion | Core depends on abstractions (ports), not implementations. |
- KISS: Minimal API surface —
Send(ctx, message)is all you need - DRY: Types defined once in
pkg/contracts/, reused everywhere - DB-First Config: In server mode, PostgreSQL is the single source of truth — no credentials in files
- Usage Guide — SDK and HTTP API usage
- Contributing — Adding providers
- Portal inbox — Memory capture and inbox REST API
- E2E Testing — Automated testing patterns
- Workflow — CI/CD and release process