vsms
An A2P SMS gateway for Cameroon — OTP and notification delivery over MTN and Orange, with a provider abstraction that covers both HTTP APIs and SMPP.
Status (snapshot, 2026-08-13 — docs/roadmap.md is the maintained version, and GitHub Issues is the real tracker): foundations and delivery are done. Schema, migrations, encoding, and MSISDN parsing; a signed transport (every request a COSE_Sign1 message under a registered key, no tokens) and console pairing in place of a login; a real worker — leader election, optimistic-CAS claim loops, the message and job state machines, DLR ingestion, a job queue; and signed, at-least-once webhooks with retries and circuit breaking. Retention, audit anchoring, backup with a real restore drill, and alerting are done. The admin console is partly built (messages, dashboard, jobs, workers, providers, routes); MTN, the routing rules engine, and failover have landed.
One thing gates real traffic, and it is not code: #36 — no message from this system has ever reached a real handset. That needs a human with a real Orange account and a real phone. Everything automated around it proves how this system behaves under faults; none of it proves Orange behaves the way this code assumes.
You can run all of it locally, with no carrier account, in one command — see local development.
Getting started
Which door you want depends on what you're doing:
The short version, for a developer who just wants it running:
git clone https://github.com/vaam-apps/vsms.git
cd vsms
just demo
That brings up a scratch Postgres, the gateway, the worker, a fake Orange, and the admin console, wired together with a provisioned client — and a message reaches delivered without a single real SMS being sent. docker compose -f compose.dev.yaml --profile console logs demo-app shows the whole round trip proved for you, end to end, including a real signed webhook that verifies (examples/node/demo-app).
What's here
| Path | What it is |
|---|
docs/integrating.md | For a developer sending SMS through vsms. Credentials and request signing, request and response shapes, message states, status codes, webhooks. |
docs/architecture.md | The full design. Data model, provider abstraction, worker topology, security, compliance, and every framework constraint that shaped a decision. |
docs/roadmap.md | Sequencing: what order the work happens in, what genuinely blocks what, and what has to be true before real traffic. |
OPEN_QUESTIONS.md | What this system does not know the answer to — human decisions, unverified claims, accepted limitations. |
docs/runbooks/ | Step-by-step operational procedures: local development, deployment, backup and restore, alerting, and the real-handset acceptance gate. |
AGENTS.md | Current project status in detail: what's built, what's still open, and every non-obvious thing found by actually running the toolchain. |
CONTRIBUTING.md | The three rules (R1/R2/R3) the codebase is written against, and the workflow for changing the schema. |
schemas/vsms.cstack | 28 models, 21 enums, 24 procedures. Parses, emits Postgres DDL, and expands through include_server_schema!. |
backends/migrations/postgres/0001_init/ | Generated by cratestack migrate diff. Do not hand-edit — regenerated wholesale on every schema change; see AGENTS.md. |
backends/migrations/postgres/0002_bootstrap/ | Everything the emitter doesn't produce: id and timestamp defaults, the updated_at trigger, the state machines, partial indexes, foreign keys. Generated from §2.10 of the design doc. |
backends/crates/sms-encoding/ | GSM 03.38 vs UCS-2 analysis, segment counting, normalisation, transliteration. |
backends/crates/sms-msisdn/ | E.164 +237 parsing, line-type classification, operator-prefix lookup. |
backends/crates/sms-api/ | Where include_server_schema! expands: generated models, policies, the REST router, the procedure registry, DLR ingestion, and the webhook subscribers. |
backends/crates/ is libraries, backends/apps/ is binaries, and nothing in backends/crates/ depends on anything in backends/apps/.
Stack
- Rust for the gateway and worker, on CrateStack
=0.17.2 — schema-first, .cstack generates the model layer, policies, audit, events and REST surface. Pinned exactly, and the installed CLI must match the pin; see AGENTS.md.
- A signed transport on CrateStack's envelope layer. Every request is a COSE_Sign1 message under a key the caller generated and every answer is signed back — no tokens, no shared secrets anywhere in the system. Humans use the console with a browser key a vaam admin phone paired; there is no login and no OpenID provider (
docs/architecture.md §4).
- PostgreSQL 16 as the only coordination mechanism — system of record, claim loops via optimistic CAS on
@version (SKIP LOCKED isn't expressible through the framework), leader election via advisory locks, and the state machines enforced by triggers. No broker, no Redis, no consensus library.
- TypeScript / Next.js 15 for the admin console — messages and state timelines, an operator dashboard, jobs, workers, providers, routes, and a route simulator. Partly built; see the roadmap.
Design notes worth knowing before you read the code
Three findings shaped the schema more than any product requirement, and each is a property of the toolchain rather than of the problem:
- Scalar list fields panic the server macro.
String[] parses fine and the migration emitter writes a cheerful TEXT[], but include_server_schema! dies with unsupported SQLx value type for this slice. Every multi-value column here is a sentinel-delimited string instead.
- Any
@default(...) excludes a field from the create input — literals included. A @default on something a caller must set makes it unwritable. It's kept only where being unsettable is the point, such as Message.state, which is why no client can create a message that is already delivered.
@server_only excludes a field from create and update, so it can never be populated at all. It is for columns the database owns, not for secrets you write.
None of these are documented upstream. Every framework constraint found this way — not just these three — is recorded in §2.0 with the exact error each one produces, and in AGENTS.md with the story of how it was found.
Licence
MIT — see LICENSE.