Porukator is a self-hosted SMS gateway that replaces commercial providers
(such as Twilio) by using your own Android phones as the SMS transport. Your
services hand Porukator a list of (phone number, message) jobs together with
the set of devices to send from; the service balances those messages across the
devices, and each device sends them as real SMS from its SIM and reports
delivery back.
It is a small, self-contained monorepo: a protobuf contract, a Go service, a React web UI, and an Android app.
- Phones as SMS senders — no third-party SMS provider; messages go out from the SIMs of devices you control.
- Multiple devices, balanced — submit a batch plus the devices to use, and Porukator distributes the messages round-robin across them.
- Send pacing — a configurable delay plus random jitter between each SMS a device sends, to spread traffic out.
- Live device status — the web UI shows which devices are online (connected and waiting for messages) and when each was last seen.
- Simple device onboarding — add a device in the UI and pair it by typing the host + token into the app, or by scanning a generated QR code.
- User accounts with roles — admins manage everything; managers manage only their own devices and API keys and see those devices' messages. Argon2id-hashed passwords, DB-backed sessions, admin-controlled disable/revoke/role changes.
- Owner-scoped API keys — any user can mint keys for their external services; a key reaches only its owner's devices (admin keys reach all). Disabling or deleting the owner stops the keys.
- Three clear access surfaces — user sessions for the admin UI, per-service API tokens for producers, and per-device access tokens for the phones.
- Durable, auditable log — every message is stored in Postgres with its phone number, content, status, and timestamps for when it was received and when it was sent.
- Status queries for producers — external services poll delivery status (pending / dispatched / sent / failed) for the messages or batches they submitted, through their API key (scoped to the key's owner).
- Resilient delivery — messages for an offline device are held and delivered when it reconnects; dispatch is exactly-once.
Porukator is one protobuf contract exposing three services, consumed by three clients:
flowchart TB
webui["Web UI<br/>(React SPA)"]
producers["Your upstream<br/>services"]
app["Android app<br/>(foreground sender)"]
sim(["SMS via SIM"])
subgraph svc["Porukator service (Go)"]
admin["AdminService<br/>(user session auth)"]
producer["ProducerService<br/>(API-token auth)"]
client["ClientService<br/>(client-token auth)"]
end
db[("Postgres<br/>devices · API tokens<br/>pacing · message log")]
webui <--> admin
producers --> producer
client -- "Job (phone, text, delay, jitter)" --> app
app -- "ReportDelivery" --> client
app --> sim
admin <--> db
producer --> db
client <--> db
-
Contract (
proto/) — a singleporukator.v1protobuf file defines the message types and the three services. All clients are generated from it (Go, TypeScript, Kotlin/Java). -
Service (
cmd/,internal/) — a Go server speaking Connect (gRPC-compatible) over HTTP. It hosts:AdminService— used by the web UI to manage devices, API tokens, pacing, and to read the message log.ProducerService— used by your other services to list devices and submit messages.ClientService— used by the Android app: a server stream the service pushes jobs onto, plus a call the app uses to report each send.
It keeps an in-memory registry of connected devices (the source of the "online" flag) and persists everything else in Postgres.
-
Web UI (
webui/) — a React single-page app. Users sign in with an account (the first admin is created withauthctl create --admin). Admins manage devices, API tokens, pacing settings, the message log, and user/session administration; managers see only their own devices and those devices' messages. -
Android app (
android/) — a Kotlin app. It is paired with connection parameters (host + access token, typed or scanned from a QR code) and runs a foreground service that holds the job stream open, sends each pushed SMS with the configured delay + jitter, and reports the outcome back.
sequenceDiagram
participant P as Producer service
participant S as Porukator service
participant DB as Postgres
participant A as Android device
A->>S: StreamJobs (opens stream, marked online)
P->>S: SendMessages(batch, device_ids)
S->>DB: store messages (PENDING), assign round-robin
S-->>P: batch_id + message_ids
S->>A: Job (phone, text, delay, jitter)
Note over A: send SMS via SIM,<br/>then wait delay + random(jitter)
A->>S: ReportDelivery(message_id, success, sent_at)
S->>DB: mark SENT / FAILED + timestamp
Note over S,A: device offline → messages stay PENDING,<br/>drained on reconnect
- Service — Go, Connect RPC, Postgres (pgx + sqlc + golang-migrate), Viper config, zap logging.
- Web UI — Vite, React, TypeScript, Connect-Query + TanStack Query, shadcn/ui + Tailwind, Zustand. Packages managed with pnpm.
- Android — Kotlin, Jetpack Compose, Connect-Kotlin, CameraX + ML Kit (QR scanning), DataStore.
- Contract — Protocol Buffers, generated with Buf.
- Go 1.25+
- Buf (protobuf generation) and just (task runner)
- Docker (for Postgres via
docker compose) - Node.js + pnpm for the web UI
- mprocs to run the whole stack in one terminal (
just dev) - Android SDK + JDK 17 (Android Studio recommended) for the app; SMS sending requires a real device with a SIM
just dev brings up Postgres, the backend, and the web UI together under mprocs;
quitting it stops the docker compose stack. See AGENTS.md for this
and the per-component build, run, and code-generation commands.
This project is developed with the assistance of AI tooling.
Authored by Dušan Simić.
Licensed under the BSD 2-Clause License — see LICENSE.