┌──────────────────────────────────┬──────────────────────────────────┐
│ Embedded Go SDK │ HTTP Server (any language) │
│ (no server needed) │ (full infrastructure) │
├──────────────────────────────────┼──────────────────────────────────┤
│ go get .../wpd-message-gateway │ git clone → make start │
│ gateway.New(config) │ POST /v1/email (HTTP) │
│ Config lives in your code │ Config lives in PostgreSQL │
│ No DB required │ React Portal UI (partial) │
└──────────────────────────────────┴──────────────────────────────────┘
Use this when you have a Go application and want to send messages directly — no server setup required.
go get github.com/weprodev/wpd-message-gatewaypackage main
import (
"context"
"log"
"github.com/weprodev/wpd-message-gateway/pkg/contracts"
"github.com/weprodev/wpd-message-gateway/pkg/gateway"
)
func main() {
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",
FromName: "MyApp",
},
},
})
if err != nil {
log.Fatal(err)
}
result, err := gw.SendEmail(context.Background(), &contracts.Email{
To: []string{"user@example.com"},
Subject: "Welcome!",
HTML: "<h1>Hello!</h1>",
})
if err != nil {
log.Fatal(err)
}
log.Printf("Sent! ID: %s", result.ID)
}// No external service — messages are captured in RAM
gw, _ := gateway.New(gateway.Config{
DefaultEmailProvider: "memory",
})
gw.SendEmail(ctx, email) // captured locally, not sentPass credentials directly to gateway.New(). Use environment variables or your secrets manager — never hard-code credentials.
gw, _ := gateway.New(gateway.Config{
DefaultEmailProvider: os.Getenv("EMAIL_PROVIDER"), // "mailgun", "memory", etc.
EmailProviders: map[string]gateway.EmailConfig{
"mailgun": {
CommonConfig: gateway.CommonConfig{
APIKey: os.Getenv("MAILGUN_API_KEY"),
},
Domain: os.Getenv("MAILGUN_DOMAIN"),
FromEmail: os.Getenv("MAILGUN_FROM_EMAIL"),
FromName: os.Getenv("MAILGUN_FROM_NAME"),
},
},
})Use this when you want to:
- Send messages from any language (Python, PHP, Ruby, JS, etc.)
- Manage provider credentials via a UI (not in code)
- Have a team managing multiple workspaces
- Use the built-in message inbox (dev/testing)
This spins up the entire stack including a PostgreSQL database (pre-loaded with migrations and permission/demo seeds), a hot-reloading Go backend (via air), and the Portal UI dev server:
# Start all services
make dev
# Stop all services
make dev-down
# Reset Database: Since DB init scripts run only on first boot, run this to reset data/schema:
docker compose down -vOpen http://localhost:10104 — the Portal UI.
If you prefer to run components directly on your host machine:
git clone https://github.com/weprodev/wpd-message-gateway.git
cd wpd-message-gateway
cp configs/local.example.yml configs/local.yml
make startFor Option B, you must manually run PostgreSQL locally, apply schema migrations, and apply the SQL seeds (see below).
Open http://localhost:10104 — the Portal UI.
The Portal UI currently supports:
- Register and sign in (email + password)
- Workspaces — list workspaces you belong to and open one
- Integrations — connect, activate, deactivate, or remove messaging providers
- Message logs — audit trail of gateway send requests (Overview + channel tabs)
- Send test — send a test message through the gateway for the workspace
For local dev with PostgreSQL, run migrations then apply seeds in order (see database/init-db.sh): 001_seed_permissions.sql, 002_seed_providers.sql, 003_seed_mailgun_config.sql, and 004_demo_workspace.sql (optional demo data: demo@weprodev.com / secret).
curl -X POST http://localhost:10101/v1/email \
-u "wk_abc123:your-secret" \
-H "X-Workspace-Key: myapp" \
-H "Content-Type: application/json" \
-d '{
"to": ["user@example.com"],
"subject": "Hello",
"html": "<h1>World</h1>"
}'Or with Bearer token:
curl -X POST http://localhost:10101/v1/email \
-H "Authorization: Bearer wk_abc123:your-secret" \
-H "X-Workspace-Key: myapp" \
-H "Content-Type: application/json" \
-d '{"to": ["user@example.com"], "subject": "Hello", "html": "<h1>World</h1>"}'import requests
response = requests.post(
"http://localhost:10101/v1/email",
auth=("wk_abc123", "your-secret"),
headers={"X-Workspace-Key": "myapp"},
json={
"to": ["user@example.com"],
"subject": "Hello from Python",
"html": "<h1>Hello!</h1>"
}
)
print(response.json())const response = await fetch("http://localhost:10101/v1/email", {
method: "POST",
headers: {
"Authorization": "Basic " + btoa("wk_abc123:your-secret"),
"X-Workspace-Key": "myapp",
"Content-Type": "application/json",
},
body: JSON.stringify({
to: ["user@example.com"],
subject: "Hello from JS",
html: "<h1>Hello!</h1>",
}),
});
const result = await response.json();Authentication is email + password — the Portal returns a JWT used for all /api/v1/* requests.
# Login (or register first, then login)
curl -X POST http://localhost:10101/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "you@example.com", "password": "your-password"}'
# Response:
# { "token": "<jwt>", "user": { "id": "...", "email": "..." } }Use the JWT for all /api/v1/* requests:
curl -H "Authorization: Bearer <jwt>" http://localhost:10101/api/v1/workspacesUse your workspace API key credentials:
# Basic auth
curl -u "wk_abc123:secret" -H "X-Workspace-Key: myapp" ...
# Bearer (colon-separated)
curl -H "Authorization: Bearer wk_abc123:secret" -H "X-Workspace-Key: myapp" ...// Go SDK
result, err := gw.SendEmail(ctx, &contracts.Email{
To: []string{"user@example.com"},
CC: []string{"cc@example.com"}, // optional
BCC: []string{"bcc@example.com"}, // optional
Subject: "Hello",
HTML: "<h1>HTML body</h1>",
PlainText: "Plain text fallback", // optional
ReplyTo: "reply@example.com", // optional
})# HTTP API
curl -X POST /v1/email -d '{
"to": ["user@example.com"],
"cc": ["cc@example.com"],
"subject": "Hello",
"html": "<h1>HTML body</h1>",
"plain_text": "Plain text fallback"
}'result, err := gw.SendSMS(ctx, &contracts.SMS{
To: []string{"+1234567890"},
Message: "Your verification code is 123456",
})result, err := gw.SendPush(ctx, &contracts.PushNotification{
DeviceTokens: []string{"device-token-1"},
Title: "New Message",
Body: "You have a new message",
Data: map[string]string{"action": "open_chat"},
})result, err := gw.SendChat(ctx, &contracts.ChatMessage{
To: []string{"#channel"},
Message: "Hello from the gateway!",
})| Method | Endpoint | Auth |
|---|---|---|
| POST | /v1/email |
API key + X-Workspace-Key |
| POST | /v1/sms |
API key + X-Workspace-Key |
| POST | /v1/push |
API key + X-Workspace-Key |
| POST | /v1/chat |
API key + X-Workspace-Key |
Routes registered in internal/presentation/router.go. Portal UI pages exist only for auth, workspace list, message logs, and send test — other management routes are REST-only until UI is built.
| Method | Endpoint | Portal UI | Description |
|---|---|---|---|
| POST | /api/v1/auth/register |
✓ | Create portal user (first_name, last_name, email, password) |
| POST | /api/v1/auth/login |
✓ | Login (returns JWT) |
| GET | /api/v1/auth/verify-email |
Email verification link handler | |
| GET | /api/v1/auth/me |
Current user (JWT) |
| Method | Endpoint | Portal UI | Description |
|---|---|---|---|
| GET | /api/v1/workspaces |
✓ | List workspaces for the logged-in user |
| GET | /api/v1/workspaces/:wid/logs |
✓ | Message request logs (optionally filter by channel) |
| POST | /api/v1/workspaces/:wid/send-test/:channel |
✓ | Send test via gateway (email / sms / push / chat) |
Used by Bruno, curl, and CI bootstrap. Not documented endpoint-by-endpoint here; see internal/presentation/router.go and E2E testing for create workspace, API keys, settings, integrations, templates, and members.
Workspace-scoped routes require a valid portal JWT and are governed by a Role-Based Access Control (RBAC) middleware backed by wpd-gogate.
Roles and permissions are defined in permission.go:
- admin: Full read/write access. The creator of a workspace is automatically assigned this role (as
adminin both members repository andgogate). - member: Read-only access to workspaces, members, API keys, logs, integrations, templates, settings, and invitations, plus the ability to send test messages (
send.test).
To bootstrap roles and permissions, make sure you run the database seeds:
- Run
database/seeds/001_seed_permissions.sqlto populate roles (admin,member), default permissions, and role-to-permission mappings. - Run
database/seeds/002_seed_providers.sqlto seed the provider catalog (memory, mailgun, etc.). - Run
database/seeds/003_seed_mailgun_config.sqlto seed Mailgun Portal config field metadata. - Run
database/seeds/004_demo_workspace.sql(optional) — demo user (demo@weprodev.com/secret), workspace, admin role, memory integration, API key (demo-client-id/demo-secret).
The HTTP server uses two distinct authorization approaches by design:
- Portal Management API (
/api/v1/workspaces/:wid/...): Restricts access using JWT tokens combined with fine-grainedwpd-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 decoupled model allows client-side SDKs, SSE event streaming, and automation runners to interact with the simulated inbox without requiring full portal RBAC configuration.
The Portal UI shows request logs (/logs), not the memory inbox (/inbox/*). Inbox capture, SSE, and internal ingest are documented in Portal inbox.
environment: local
server:
port: 10101
portal:
jwt_secret: "your-long-secret-here-minimum-32-chars"
jwt_ttl_hours: 72
ui_port: 10104Note: Provider credentials (Mailgun API keys, etc.) are NOT in YAML files. They are configured via the Portal REST API and stored encrypted in PostgreSQL.
# PostgreSQL connection
DATABASE_URL=postgres://user:pass@localhost:5432/gateway?sslmode=disable
# or individual components:
DB_HOST=localhost
DB_PORT=5432
DB_USER=gateway
DB_PASSWORD=secret
DB_NAME=gateway
# Portal JWT (override yaml)
MESSAGE_JWT_SECRET=your-jwt-secret
# AES encryption key for provider credentials (32 bytes)
MESSAGE_CONFIG_ENCRYPTION_KEY=your-32-byte-key-hereControl how messages are handled per workspace. Default is memory_only.
Set via REST (no Portal UI page yet):
PATCH /api/v1/workspaces/:wid/settings
Authorization: Bearer <portal-jwt>
Content-Type: application/json
{ "message_dispatch_mode": "provider_only" }See Portal inbox for mode behavior.
| Mode | Behavior | Use Case |
|---|---|---|
memory_only |
In-process RAM only, not sent | Development, testing |
provider_only |
Sent to real provider, no local copy | Production |
memory_and_provider |
Both — local copy + real send | Staging, debugging |
Using a Mailgun Sandbox Domain:
- Log into Mailgun → Sending → Domains → [Sandbox]
- Add recipient email to Authorized Recipients
- Recipient must click the verification link
Check workspace message_dispatch_mode via GET /api/v1/workspaces/:wid/settings. If it is memory_only, messages are captured locally — not sent to the provider.
- Ensure
X-Workspace-Keymatches the workspace'sunique_key(slug, not UUID) - Verify the API key is active (
GET /api/v1/workspaces/:wid/api-keyswith portal JWT) - Check
client_idandclient_secret— secret is shown once at creation
- Architecture — System design
- Portal inbox — Memory capture and inbox REST API
- E2E Testing — Automated testing patterns
- Contributing — Adding providers