Skip to content

Repository files navigation

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: milestones 0 and 1 done; milestone 2 functionally complete. The schema, migrations, encoding, and MSISDN-parsing crates are done. Real authentication is done — machine callers get OAuth client_credentials tokens via private_key_jwt, no shared secrets anywhere. The worker is real: leader election, optimistic-CAS claim loops, the message and job state machines, DLR ingestion, and a generic job queue all run against a live database, not stubs. The one thing still open in milestone 2 is #36 — a real SMS to a real handset — which is blocked on physical hardware (real Orange credentials, a real phone), not code; see the getting-started runbook to run everything short of that yourself. The admin console (milestone 4) is not started. See the roadmap for what's next.

Getting started

git clone https://github.com/vymalo/vsms.git
cd vsms
just check

Then follow docs/runbooks/getting-started.md — clone, build, apply migrations, run the checks, and run both binaries against a scratch database, ending with a real message moving through the real state machine. docs/runbooks/ has other step-by-step procedures, including the real-handset acceptance gate.

What's here

Path What it is
docs/architecture.md The full design. Data model, provider abstraction, worker topology, security, compliance, and every framework constraint that shaped a decision.
docs/runbooks/ Step-by-step operational procedures — getting started, and the milestone-2 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.
schema/schema.cstack 19 models, 13 enums, 7 procedures. Parses, emits Postgres DDL, and expands through include_server_schema!.
schema/migrations/postgres/0001_init/ Generated by cratestack migrate diff. Do not hand-edit — regenerated wholesale on every schema change; see AGENTS.md.
schema/migrations/postgres/0002_bootstrap/ Everything the emitter doesn't produce: id and timestamp defaults, the updated_at trigger, both state machines, partial indexes, foreign keys. Generated from §2.10 of the design doc.
crates/sms-encoding/ GSM 03.38 vs UCS-2 analysis, segment counting, normalisation, transliteration.
crates/sms-msisdn/ E.164 +237 parsing, line-type classification, operator-prefix lookup.
crates/sms-api/ Where include_server_schema! expands: generated models, policies, the REST router, the procedure registry, and DLR ingestion.
crates/sms-auth/ authkestra-op glue: ClientStore/ClientAssertionStore backed by CrateStack delegates, RS256 signing-key management with overlap-window rotation.
crates/sms-worker/ The worker as a library: role selection, advisory-lock leader election, the CAS claim loop, and each role's real body (dispatch, scheduler, jobs).
crates/sms-provider/ The SmsProvider trait every adapter implements — capabilities, submit, DLR parsing, health — framework-free.
crates/sms-provider-orange-cm/ The Orange Cameroon HTTP adapter: OAuth2 token caching, submission, DLR parsing.
app/sms-gateway/ The API server binary. serve binds HTTP and mounts the OP; rotate-signing-key is the one operator action it needs before serve can issue tokens; routes prints the generated route table and needs no database.
app/sms-worker/ The role-selectable worker binary (--roles dispatch,scheduler,jobs,...) — see its own module doc for why the package is sms-worker-bin but the binary is sms-worker.
ci/ The R1 lint, the R2 state-machine parity check, the migration runner, the bootstrap-SQL generator, and the state-machine SQL test.

crates/ is libraries, app/ is binaries, and nothing in crates/ depends on anything in app/.

Stack

  • Rust for the gateway and worker, on CrateStack =0.6.3 — schema-first, .cstack generates the model layer, policies, audit, events and REST surface.
  • Authkestra =0.3.3 as the OIDC provider. Today, every caller is a machine: OAuth client_credentials with private_key_jwt client authentication, no shared secrets anywhere in the system. Human login (authorization code + PKCE) is designed but not built — nothing needs it until the admin console does.
  • 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 both state machines enforced by triggers. No broker, no Redis, no consensus library.
  • TypeScript / Next.js for the admin console — designed, not started.

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.

About

No description, website, or topics provided.

Resources

Contributing

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages