Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<a href="https://github.com/weprodev/wpd-message-gateway/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
</p>
<p align="center">
<strong>A unified Go library and HTTP API for sending Email, SMS, Push, and Chat messages.</strong>
<strong>A unified Go library and HTTP API for sending Email, SMS, Push, Chat and OTP messages.</strong>

One interface, multiple providers. Write your messaging code once — switch between Mailgun, Twilio, Firebase, WhatsApp, and more without changing a single line of application code.

Expand Down Expand Up @@ -114,7 +114,7 @@ Building applications that send messages across multiple channels is complex. Yo

**Message Gateway provides:**

- **Unified API** — Email, SMS, Push, and Chat through a single, consistent interface
- **Unified API** — Email, SMS, Push, Chat, and OTP through a single, consistent interface
- **Provider abstraction** — Switch from Mailgun to SendGrid with a config change—no code changes
- **DB-first config** — In server mode, all provider credentials live in PostgreSQL, managed via the Portal UI
- **Workspace isolation** — Multiple workspaces, each with its own providers, API keys, templates, and members
Expand All @@ -131,6 +131,8 @@ Building applications that send messages across multiple channels is complex. Yo
| **SMS** | Text to mobile numbers | Memory (Twilio planned) |
| **Push** | Mobile and web notifications | Memory (Firebase planned) |
| **Chat** | Slack, WhatsApp, Telegram | Memory (planned) |
| **OTP** | Slack, WhatsApp, Telegram | Memory (planned) |


---

Expand Down Expand Up @@ -186,7 +188,7 @@ wpd-message-gateway/
│ ├── presentation/ # HTTP router, handlers, middleware
│ └── registry/ # Provider factory registry (self-registration via init)
├── pkg/ # Public packages (contracts, gateway SDK, auth, encryption)
│ ├── contracts/ # Email, SMS, Push, Chat types
│ ├── contracts/ # Email, SMS, Push, Chat, OTP types
│ ├── gateway/ # Embedded Go SDK — gateway.New()
│ ├── auth/ # Password hashing (portal)
│ └── encryption/ # AES helpers
Expand Down
1 change: 1 addition & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ func logConfiguration(cfg *app.Config) {
"sms_provider", cfg.DefaultSMSProvider(),
"push_provider", cfg.DefaultPushProvider(),
"chat_provider", cfg.DefaultChatProvider(),
"otp_provider", cfg.DefaultOTPProvider(),
)
}

Expand Down
3 changes: 3 additions & 0 deletions configs/local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ providers:
sms: memory
push: memory
chat: memory
otp: memory

# Provider configurations
email:
Expand Down Expand Up @@ -48,3 +49,5 @@ providers:
# slack:
# webhook_url: "https://hooks.slack.com/services/..."

otp:
memory: {}
38 changes: 19 additions & 19 deletions database/migrations/20260318000000_init_gateway.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@ $$ LANGUAGE plpgsql;
-- =====================================================

CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL,
password TEXT,
first_name TEXT,
last_name TEXT,
status TEXT NOT NULL DEFAULT 'active'
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL,
password_hash TEXT,
display_name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'suspended', 'blocked')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

CONSTRAINT users_email_unique UNIQUE (email),
CONSTRAINT users_email_lower_check CHECK (email = lower(email))
Expand All @@ -42,17 +41,18 @@ CREATE TRIGGER trg_users_set_updated_at
CREATE TABLE workspaces (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
slug TEXT NOT NULL,
unique_key TEXT NOT NULL,
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
status TEXT NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'suspended')),
is_private BOOLEAN NOT NULL DEFAULT TRUE,
hashed_pin_code TEXT,
visibility TEXT NOT NULL DEFAULT 'private',
admin_email TEXT,
hashed_pin TEXT,
icon_key VARCHAR(64),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

CONSTRAINT workspaces_slug_unique UNIQUE (slug)
CONSTRAINT workspaces_unique_key_unique UNIQUE (unique_key)
);

COMMENT ON COLUMN workspaces.icon_key IS 'Optional icon identifier for workspace branding (e.g. marketing, product, support)';
Expand Down Expand Up @@ -95,7 +95,7 @@ CREATE TABLE workspace_members (
);

CREATE INDEX idx_workspace_members_user_id ON workspace_members(user_id);
CREATE INDEX idx_workspace_members_role_id ON workspace_members(role_id);
CREATE INDEX idx_workspace_members_role ON workspace_members(role);

-- =====================================================
-- 5. invitations
Expand Down Expand Up @@ -129,7 +129,7 @@ CREATE INDEX idx_invitations_token_hash ON invitations(token_hash);

CREATE TABLE workspace_channels (
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
channel_type TEXT NOT NULL CHECK (channel_type IN ('email', 'sms', 'push', 'chat')),
channel_type TEXT NOT NULL CHECK (channel_type IN ('email', 'sms', 'push', 'chat', 'otp')),
enabled BOOLEAN NOT NULL DEFAULT TRUE,

PRIMARY KEY (workspace_id, channel_type)
Expand All @@ -142,7 +142,7 @@ CREATE TABLE workspace_channels (
CREATE TABLE providers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
channel_type TEXT NOT NULL CHECK (channel_type IN ('email', 'sms', 'push', 'chat')),
channel_type TEXT NOT NULL CHECK (channel_type IN ('email', 'sms', 'push', 'chat', 'otp')),
status TEXT NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'not_supported', 'blocked')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
Expand All @@ -167,7 +167,7 @@ CREATE TABLE provider_config_fields (
label TEXT NOT NULL,
description TEXT,
field_type TEXT NOT NULL
CHECK (field_type IN ('text', 'password', 'email', 'url', 'boolean', 'textarea', 'select')),
CHECK (field_type IN ('text', 'password_hash', 'email', 'url', 'boolean', 'textarea', 'select')),
required BOOLEAN NOT NULL DEFAULT FALSE,
default_value TEXT,
options JSONB, -- for field_type='select': ["tls","ssl","none"]
Expand All @@ -190,7 +190,7 @@ CREATE TABLE integrations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
provider_id UUID NOT NULL,
channel_type TEXT NOT NULL CHECK (channel_type IN ('email', 'sms', 'push', 'chat')),
channel_type TEXT NOT NULL CHECK (channel_type IN ('email', 'sms', 'push', 'chat', 'otp')),
encrypted_config BYTEA NOT NULL,
status TEXT NOT NULL DEFAULT 'connected'
CHECK (status IN ('connected', 'error', 'disconnected')),
Expand Down Expand Up @@ -245,7 +245,7 @@ CREATE TABLE message_request_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
api_key_id UUID REFERENCES api_keys(id) ON DELETE SET NULL,
channel_type TEXT NOT NULL CHECK (channel_type IN ('email', 'sms', 'push', 'chat')),
channel_type TEXT NOT NULL CHECK (channel_type IN ('email', 'sms', 'push', 'chat', 'otp')),
provider_name VARCHAR(64),
http_method VARCHAR(16) NOT NULL,
status_code SMALLINT NOT NULL,
Expand Down Expand Up @@ -277,7 +277,7 @@ CREATE TABLE templates (
name VARCHAR(255) NOT NULL,
unique_key VARCHAR(255) NOT NULL,
channel_type TEXT NOT NULL DEFAULT 'email'
CHECK (channel_type IN ('email', 'sms', 'push', 'chat')),
CHECK (channel_type IN ('email', 'sms', 'push', 'chat', 'otp')),
subject VARCHAR(512),
content TEXT NOT NULL,
category VARCHAR(64),
Expand Down
40 changes: 32 additions & 8 deletions database/seeds/001_demo_workspace.sql
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
-- Demo workspace, API key, email integration, template (aligned with latest schema)

INSERT INTO users (id, email, password, first_name, last_name, status)
INSERT INTO users (id, email, password_hash, display_name, status)
VALUES (
'00000000-0000-0000-0000-000000000010',
'demo@weprodev.com',
'$2a$14$dummy.hash.for.demo.only',
'Demo',
'User',
'Demo User',
'active'
)
ON CONFLICT (id) DO NOTHING;
Expand All @@ -17,29 +16,31 @@ VALUES
('00000000-0000-0000-0000-000000000021', 'member', 'Member')
ON CONFLICT (id) DO NOTHING;

INSERT INTO workspaces (id, name, slug, owner_id, status, is_private, hashed_pin_code, icon_key)
INSERT INTO workspaces (id, name, unique_key, owner_id, admin_email, status, visibility, hashed_pin, icon_key)
VALUES (
'00000000-0000-0000-0000-000000000001',
'Demo Workspace',
'demo',
'00000000-0000-0000-0000-000000000010',
'demo@weprodev.com',
'active',
true,
'private',
NULL,
NULL
)
ON CONFLICT (id) DO NOTHING;

INSERT INTO workspace_members (workspace_id, user_id, role_id)
INSERT INTO workspace_members (workspace_id, user_id, role)
VALUES (
'00000000-0000-0000-0000-000000000001',
'00000000-0000-0000-0000-000000000010',
'00000000-0000-0000-0000-000000000020'
'admin'
)
ON CONFLICT (workspace_id, user_id) DO NOTHING;

INSERT INTO workspace_channels (workspace_id, channel_type, enabled)
VALUES ('00000000-0000-0000-0000-000000000001', 'email', true)
VALUES ('00000000-0000-0000-0000-000000000001', 'email', true),
('00000000-0000-0000-0000-000000000001', 'otp', true)
ON CONFLICT (workspace_id, channel_type) DO NOTHING;

INSERT INTO api_keys (id, workspace_id, client_id, client_secret_hash, name, is_active)
Expand Down Expand Up @@ -75,6 +76,29 @@ VALUES (
)
ON CONFLICT (workspace_id, provider_id) DO NOTHING;

-- OTP memory provider (global catalog)
INSERT INTO providers (id, name, channel_type, status)
VALUES (
'00000000-0000-0000-0000-000000000031',
'memory',
'otp',
'active'
)
ON CONFLICT (id) DO NOTHING;

-- OTP default integration for demo workspace
INSERT INTO integrations (id, workspace_id, channel_type, provider_id, encrypted_config, status, is_default)
VALUES (
'00000000-0000-0000-0000-000000000004',
'00000000-0000-0000-0000-000000000001',
'otp',
'00000000-0000-0000-0000-000000000031',
E'\\x720231d45885b8d808a3e2930264f2b60ca166004abf449b6be50328217e',
'connected',
true
)
ON CONFLICT (workspace_id, provider_id) DO NOTHING;

INSERT INTO templates (id, workspace_id, name, unique_key, channel_type, subject, content, category, is_active, is_default)
VALUES (
'00000000-0000-0000-0000-000000000004',
Expand Down
18 changes: 9 additions & 9 deletions docs/backend/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ gw.SendEmail(ctx, &contracts.Email{
│ │ Router │ │ PortalHandler │ │ GatewayHandler │ │
│ │ │ │ (auth, WS mgmt)│ │ (send email, │ │
│ └─────────────┘ │ InboxHandler │ │ sms, push, │ │
│ │ (captured msgs)│ │ chat) │ │
│ │ (captured msgs)│ │ chat, otp) │ │
│ └────────┬────────┘ └────────┬────────┘ │
└─────────────────────────────┼────────────────────┼──────────────┘
│ │
Expand All @@ -136,14 +136,14 @@ gw.SendEmail(ctx, &contracts.Email{
│ └──────────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ GatewayService │ │
│ │ SendEmail() · SendSMS() · SendPush() · SendChat() │ │
│ │ Reads dispatch_mode from workspace_settings │ │
│ │ SendEmail() · SendSMS() · SendPush() · SendChat() · │ │
│ │ SendOTP() Reads dispatch_mode from workspace_setting │ │
│ │ Reads provider credentials from integrations table │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Ports (Interfaces) │ │
│ │ EmailSender · SMSSender · PushSender · ChatSender │ │
│ │ Repository interfaces for all DB entities │ │
│ │ EmailSender · SMSSender · PushSender · ChatSender · │ │
│ │ OTPSender Repository interfaces for all DB entities │ │
│ └──────────────────────────────────────────────────────────┘ │
└──────────────────────────────┬──────────────────────────────────┘
│ implements
Expand Down Expand Up @@ -228,7 +228,7 @@ This doc intentionally does **not** duplicate full column lists.

1. **Open Portal** → select workspace
2. **Go to Integrations** → Add Integration
3. **Select channel** (Email, SMS, Push, Chat) and **provider** (Mailgun, etc.)
3. **Select channel** (Email, SMS, Push, Chat, OTP) and **provider** (Mailgun, etc.)
4. **Enter credentials** — stored AES-encrypted in `integrations` table
5. **Set dispatch mode** in Settings → `memory_only` | `provider_only` | `memory_and_provider`

Expand Down Expand Up @@ -384,7 +384,7 @@ wpd-message-gateway/
│ ├── auth/ # Hash utilities (shared between SDK and server)
│ ├── encryption/ # AES encryption (for DB-stored provider config)
│ └── gateway/ # Embedded SDK — gateway.New() for pure Go usage
│ ├── gateway.go # Gateway struct, SendEmail/SMS/Push/Chat
│ ├── gateway.go # Gateway struct, SendEmail/SMS/Push/Chat/OTP
│ ├── config.go # Config struct + New() constructor
│ └── errors.go
Expand Down Expand Up @@ -413,7 +413,7 @@ In server mode, provider credentials (Mailgun API keys, etc.) are stored **encry
Ports define **capabilities** — the "What" (Send Email), not the "How" (using Mailgun API).

- **Location**: `internal/core/port/`
- **Interfaces**: `EmailSender`, `SMSSender`, `PushSender`, `ChatSender`
- **Interfaces**: `EmailSender`, `SMSSender`, `PushSender`, `ChatSender`, `OTPSneder`
- **Benefit**: Providers are interchangeable — any implementation that satisfies the interface works

### 4. Provider Self-Registration
Expand Down Expand Up @@ -463,7 +463,7 @@ The React Portal UI is **always enabled** when running the server. It serves as:
| **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. |
| **Interface Segregation** | Separate interfaces for Email, SMS, Push, Chat, OTP. |
| **Dependency Inversion** | Core depends on abstractions (ports), not implementations. |

### Other Principles
Expand Down
2 changes: 2 additions & 0 deletions docs/backend/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ internal/infrastructure/provider/
│ ├── email.go # Memory email provider
│ ├── sms.go # Memory SMS provider
│ ├── push.go # Memory push provider
│ ├── otp.go # Memory OTP provider
│ ├── chat.go # Memory chat provider
│ └── register.go # init() self-registration
Expand Down Expand Up @@ -258,6 +259,7 @@ Same pattern — use the corresponding registry function and port interface:
| SMS | `registry.RegisterSMSProvider(name, factory)` | `port.SMSSender` |
| Push | `registry.RegisterPushProvider(name, factory)` | `port.PushSender` |
| Chat | `registry.RegisterChatProvider(name, factory)` | `port.ChatSender` |
| OTP | `registry.RegisterOTPProvider(name, factory)` | `port.OTPSender` |

---

Expand Down
Loading