PRODUCT / INTERNAL BUILD

SMS CPaaS platform — Multi-Tenant SMS & WhatsApp CPaaS

Kannel can bind SMPP. It cannot be the company. This platform puts PostgreSQL as the ledger, FastAPI as the decision layer, and a Redis worker as the send path — with three keys: platform, tenant/reseller, RBAC user.

Vue 3FastAPIPostgreSQLRedisKannelDLTCPaaS

Watch the 4:22 demo Dedicated watch page Configure & request quote →

Industry
SMS operators · resellers · CPaaS teams
Type
Multi-tenant SMS / WhatsApp CPaaS
Role
Solo — architecture, FastAPI, Vue, worker
Keys
Platform · tenant/reseller · RBAC user
Stack
Vue 3 · FastAPI · PostgreSQL · Redis · Kannel
Proof
11 running screens · 65 OpenAPI paths

The problem

Indian enterprises need SMS and WhatsApp with DLT templates, reseller tenancy and prepaid wallets — not a single Twilio account with no isolation between agencies.

Constraints

Delivery stayed inside the client's stack, tenancy, compliance and media-ownership boundaries. Where a choice was forced (suite vs owned plane, BYOC vs CPaaS, local vs cloud models), the architecture section below records the trade-off rather than a marketing rewrite.

Business requirements

Platform operator, tenant/reseller hierarchy, RBAC users, campaign send, DLT registration, wallet top-ups and provider routing (Kannel, Twilio, Gupshup) on one FastAPI + Vue control plane.

Engineering challenges

  • Tenant tree — master, reseller and client rows with scoped queries.
  • DLT compliance — template and sender IDs before broadcast.
  • Provider abstraction — route SMS without forking the product per carrier.

The solution

SMS CPaaS platform CPaaS: landing, JWT auth, platform and tenant dashboards, campaigns, wallet, contacts and OpenAPI-documented REST — captured in eleven running screens on this site.

My role

Solo architecture and full-stack delivery — FastAPI, Vue 3, PostgreSQL, Redis and Kannel integration. Not a voice/PBX product; see CCaaS case studies for FreeSWITCH work.

Technology stack

Vue 3FastAPIPostgreSQLRedisKannel

Production considerations

  • JWT expiry, permission codes and tenant_id on every mutating route.
  • Seeded demo users must not ship to production builds.
  • Marketing stats on the landing page are copy — not telemetry claims.

Product depth

Context. Not a voice / SIP / PBX platform

The case-study chrome on this site often assumes FreeSWITCH, WebRTC and Grafana. This repo is SMS CPaaS. Kannel was chosen because it is an SMPP router, not a media switch. Grafana, Prometheus and SIP are not in the codebase. Voice work is on FreeSWITCH CCaaS and Asterisk 22 UnifiedPBX.

Counts from the running OpenAPI document and Alembic tree — not production traffic or uptime: 65 OpenAPI paths, 15 router tags, 3 SMS providers (Kannel, Twilio, Gupshup), 13 Alembic revisions.

Screenshots

Public door — acquisition, not telemetry

SMS CPaaS platform landing hero: India's Enterprise Messaging Control Plane, Request Demo, and marketing stats including 125.0M messages delivered

Hero: “India's Enterprise Messaging Control Plane.” CTAs, TRAI / routing / API-first badges. Dual-channel SMS + WhatsApp. GET /cms/landing plus hardcoded Vue sections. The 125.0M / 500+ enterprises / 99.95% uptime / 98.5% delivery tiles are marketing copy. They are not used as results on this page.

Screenshots

One door, three keys

SMS CPaaS platform login card with email, password, Login and Register

Email + password. POST /auth/login returns a JWT (HS256, default 60 minutes). Then GET /auth/permissions and GET /auth/me. SPA stores permission codes in sessionStorage; Axios sends Bearer. First-tenant register exists. Seeded local users in docs must not ship to production.

Who does what

RoleJobSurfaces in this capture
Platformtenant_id IS NULL. Tenant lifecycle, routes, flags, auditPlatform Overview, Tenants, Feature Flags, Audit Logs
Tenant / resellermaster / reseller / client via parent_tenant_idDashboard, Campaigns, Templates, DLT, Wallet, Contacts, Routes
RBAC userPermission codes plus user overridesNav gated by can(); missing permission → Settings

Parent is credited after a client campaign debit. Margin in this build is a hardcoded 10%, not tenants.reseller_margin_percent.

Platform — is anyone sending?

The question this screen answers: “How many tenants are live, and did anything go out today?”

Platform Overview with SMS today 0, delivery rate 0 percent, 3 active tenants and 0 active routes

GET /admin/platform/overview behind require_platform_admin. This capture: SMS today 0, delivery 0%, 3 active tenants, 0 active routes, empty top-tenants table. Left rail still shows Platform Overview, Tenants, Routes, Compliance, Feature Flags, Audit Logs. “System healthy” in the top bar is a static label, not a probe of GET /health.

Tenant — spend, delivery, campaign load

Tenant dashboard with wallet balance rupees 0, low-wallet alert, zero messages sent and DLT plus PCI badges

GET /dashboard/stats, /dashboard/realtime, /wallet/balance. Wallet ₹0, messages sent 0, delivery 0%, active campaigns 0. Low-wallet alert at ₹500 is a frontend threshold. “Industry avg: 96.5%” is hardcoded UI copy. Badges state the product domains: DLT, PCI payments, SMS + WhatsApp.

Campaigns — create is send

Campaigns page with Create and send form for name, approved template, contact list and DLT sender ID

Name, approved template, contact list, DLT sender ID, Create & send. No separate Start button. Channel filter on the table. Pipeline: approved template + list → skip opted-out (and unconsented promotional) → DLT gate → render placeholders → segment billing → SELECT wallet FOR UPDATE → insert messages → enqueue Redis sms:send (2,000 IDs/batch). Pause/cancel patches campaign status; already-queued IDs can still drain.

Templates screen with Sync from WhatsApp, Add template, and zero counts for SMS and WhatsApp

Tabs: All / SMS / WhatsApp / Compliance. Sync from WhatsApp needs WHATSAPP_BUSINESS_ACCOUNT_ID. Empty in this capture. Campaigns may only use approved templates. WhatsApp send is the exception to the queue: it runs synchronously inside POST /campaigns via Meta Graph v18; wallet cost is recorded as zero in this implementation.

Contacts page with one list, CSV column guidance and Add list

One list in this restore. In-product CSV columns: phone, name, is_opted_out. STOP/START MO webhooks flip that flag. Tenant for inbound currently uses a default tenant id in env, not DID-level mapping.

DLT — TRAI before the queue

Templates Compliance tab with PE profiles 0 and Sender IDs 0 plus Add profile and Add sender ID

PE profiles and sender IDs (header, route type, status). Template regex lives on templates, not a separate DLT-template table (migration 010 merged that model). Toggle DLT_VALIDATION_ENABLED / SettingsService. When the gate is on, mismatched traffic is never billed or queued.

SMS Routes inventory empty with Add route and Platform admin buttons

Empty inventory here; UI still exposes Kannel / Twilio / Gupshup. Worker uses provider + smpp_id from the selected route. Campaign create sorts by priority only. LCR and weighted-split helpers exist in routing_service.py and are unused by create. Failover rows can be stored; the worker does not auto-fail over on DLR failure.

Wallet — lock, then enqueue

Wallet screen with current balance rupees 0, Add funds Paytm or Razorpay, and empty transaction history

Copy on the screen: card data never touches these servers. Paytm checksum and Razorpay HMAC credit the ledger; callbacks are idempotent on order status. Concurrent campaigns cannot overdraw: wallet row lock + segmenter (160/153 GSM, 70/67 UCS-2). Insufficient balance → HTTP 400, no enqueue. Failed SMS is not auto-refunded on DLR in this build (a credit helper exists; webhooks do not call it).

HopSync or asyncFailure mode
Login / CRUD / DLT / wallet lockSynchronousHTTP 4xx, no enqueue
SMS submitAsync (Redis + worker)3 retries, then messages.failed
WhatsApp sendSynchronous in createRow marked failed; wallet cost 0
DLR / MO / payment callbackSynchronous webhookIdempotent wallet; DLR updates status

The contract the UI already uses

Swagger SMS SaaS API 0.1.0 OAS 3.1 with Authorize and auth plus tenants tags

Swagger at /docs — SMS SaaS API 0.1.0, OAS 3.1, 65 paths, 15 tags. Bearer Authorize. Public: login, register, health, CMS, contact form, payment callbacks, SMS webhooks. Locked: campaigns, wallet, DLT, platform admin. Generated from FastAPI signatures — not a separately maintained YAML file.

Working-system video

The recording is the product walking — not a slide deck. Same stack as these screens. UnifiedPBX.in is not a public SMS CPaaS platform tenant login.

Watch on YouTube → · Dedicated watch page →

How the system flows

Create and send is one action: the API validates DLT and wallet, enqueues Redis batches, and workers submit to Kannel or HTTP SMS providers.

flowchart LR
  T[Tenant Create and Send] --> V[Template DLT consent check]
  V --> W[Lock wallet debit]
  W --> Q[Redis sms:send batches]
  Q --> K[Worker asyncpg httpx]
  K --> S[Kannel SMPP or Twilio Gupshup]
  S --> D[DLR update status]
SMS campaign path — prepaid wallet before the queue.

Architecture

The rule: FastAPI decides; the worker submits; Kannel (or HTTP aggregators) execute.

flowchart TB
  Vue[Vue 3 landing tenant platform]
  API[FastAPI 65 paths OpenAPI]
  PG[(PostgreSQL 16)]
  Redis[(Redis 7 queue)]
  W[Worker retries]
  K[Kannel SMPP]
  TW[Twilio Gupshup HTTP]
  Vue --> API
  API --> PG
  API --> Redis
  Redis --> W
  W --> K
  W --> TW
SMS CPaaS platform CPaaS architecture.

WhatsApp is not on that async hop: API → Meta Cloud inside campaign create. arq is in requirements.txt; the queue is a Redis list, not ARQ. Compose runs Postgres and Redis by default; Kannel is an optional profile. API/UI run locally. Kubernetes, Grafana and a CI test suite are not provided.

Engineering challenges

ProblemApproachResidual
HTTP must not wait on SMPPRedis batchesNo DLQ UI; pause is weak
TRAI rejects unpaid spamDLT + consent before queueDepends on toggle + data quality
Concurrent prepaid sendWallet row lock + ledgerFailed SMS not auto-refunded on DLR
Fresh migrate vs ORM seed002 uses Tenant columns from 011Open migration debt
  • Multi-provider DLR → normalise to delivered/failed + raw dlr_status
  • PCI → hosted checkout; PAN never posted to this API
  • Tenant leakage → tenant_id from JWT on queries (application layer, not row-level DB policies)
  • Known hardening items: SMS DLR/MO webhooks unauthenticated; X-Tenant-ID accepted without a platform-admin gate

My role

Solo engineer: control plane vs data plane, FastAPI routers and Pydantic, Vue SPA, schema and 13 Alembic revisions, wallet pre-deduct, DLT gate, provider adapters, webhook normalisation, Compose for Postgres/Redis/optional Kannel, backup scripts, OpenAPI.

Architecture FastAPI Vue 3 Kannel SMPP DLT Wallets

Engineering outcomes

No invented delivery-rate SLA. What the running UI and spec actually show:

  • One login that fans out by platform vs tenant vs permission codes
  • Prepaid wallet with row lock before enqueue
  • DLT PE / sender IDs and approved-template campaign create
  • Async SMS worker (Kannel, Twilio, Gupshup) and sync WhatsApp via Meta
  • Paytm / Razorpay signed, idempotent credit — no PAN
  • OpenAPI 3.1 at /docs (65 paths)
  • Honest gaps: unused LCR helpers, no Grafana, scheduled_at not a running scheduler

Architectural insight

If the HTTP request waits on SMPP, you built a form, not a CPaaS. If DLT is a spreadsheet beside the composer, TRAI will still reject the traffic and the wallet will still have been charged. The product is identity, compliance and prepaid accounting — SMSC is the adapter. Related: watch the demo, vertical products, CCaaS when the same operator also needs queues, SMS CPaaS platform article for the reseller narrative.

FAQ — buyer & architecture questions

Ten common questions about this case study, fit, and engagement.

Documented builds include multi-tenant messaging, resellers and OTP flows where shipped.

Some platforms combine messaging with voice routes; see stack tags on the page.

Multi-tenant admin and billing hooks are standard requirements for CPaaS products.

Implemented per country rules in operator deployments.

Yes as integration scope; credentials stay with the operator.

ISPs, aggregators and SaaS founders selling messaging/voice under their brand.

Share the current stack (PBX, CRM, cloud), the failure or goal in one paragraph, peak call volume, carriers, and any deadline. Screenshots, a pcap, or a short Loom beat a 40-page RFP. Use the project brief or email hello@unifiedpbx.in.

Yes. Delivery is remote-first from Delhi with scheduled overlap for EU, UK, US and APAC stand-ups. Production changes use written runbooks, rollback steps and agreed maintenance windows.

Yes. Many engagements begin with a 1–2 week SIP trace review, tenant-isolation audit, or architecture assessment. If the fit is good, scope expands from evidence—not from a generic sales deck.

Founders, CTOs, telecom leads, MSPs and product teams building or fixing UCaaS, CCaaS, CRM+voice, AI voice, or vertical SaaS—not buyers who only need seats on a mass-market suite.

Building a messaging control plane you own?

Share DLT status, who is platform vs reseller vs sender, which SMSC you already bind, and what already bills or rejects. The first reply is architecture — not a per-SMS quote.

Discuss Your Project