Every team that ships a database hits the same wall: schema migrations run in production, something breaks, and nobody finds out until a user files a ticket an hour later. The gap between the migration failing and someone knowing is the whole problem. This sample closes it.
The Telnyx code example is here:
https://github.com/team-telnyx/telnyx-code-examples/tree/main/sql-migration-agent
It is a Python + Flask service using the Telnyx Python SDK v4 that runs schema migrations, checks the result, and text-messages the on-call the instant a migration fails — over Telnyx SMS, with Ed25519-signed webhook delivery receipts so you know the alert actually reached the device. One Flask file, ~280 lines, no mock callbacks. The demo runs the whole loop with no real Telnyx account — it stubs the SMS client and generates its own Ed25519 keypair on first run.
What This Example Builds
The agent models a migration as a single in-code-path operation. Schema version is read, the migration script is fetched, the steps execute in order, and the version bumps only on success. On failure the version does not advance, rollback is initiated, and an SMS goes out to the on-call number the moment the migration breaks. The webhook roundtrip — a signed delivery receipt from Telnyx — closes the last loop so the alert is not fire-and-forget.
The key SDK calls in the v4 Python SDK:
import telnyx
client = telnyx.Telnyx(
api_key=os.getenv("TELNYX_API_KEY"),
public_key=os.getenv("TELNYX_PUBLIC_KEY"),
)
# Fire the failure alert the moment a step breaks
client.messages.send(
from_=os.getenv("TELNYX_FROM_NUMBER"),
to=notify_phone,
text=f"MIGRATION FAILED: {migration_id} — {error}",
)
# Verify the signed delivery receipt that comes back
raw_body = request.get_data(as_text=True)
event = client.webhooks.unwrap(payload=raw_body, headers=request.headers)
The webhooks.unwrap call does Ed25519 signature verification against the public key set on the client. If the signature does not verify, the request is rejected with a 400 — no delivery is recorded, no status is updated. The sample ships this roundtrip end-to-end; it is not a stub.
Run It Locally — No Telnyx Account Needed
The demo launcher (demo/demo_server.py) is a single file that stands up a SQLite store, stubs the SMS client, generates an Ed25519 keypair on first run, and serves a dashboard at http://localhost:5555. The whole loop runs in-process — no network calls, no credentials.
git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/sql-migration-agent
cp .env.example .env
pip install -r requirements.txt
python demo/demo_server.py
Open http://localhost:5555. The dashboard boots with zero migrations and a baseline schema version. Click "Run 001: Add users table" for the happy path — the migration succeeds, the schema version bumps to 001, no SMS fires because nothing broke. Then click "Run 003: Add orders (FAIL)" — the migration has a deliberate syntax error, the schema version does not advance, and a new entry appears in the SMS log: the on-call just got a text reading MIGRATION FAILED: 003 — syntax error near column.
To verify the webhook roundtrip, click "Send webhook (delivery receipt)". The demo server signs a payload with the Ed25519 key it generated on startup, POSTs it to its own /webhooks endpoint, and the app verifies the signature before recording the delivery. The SMS log row updates to delivered with a green check. That is the full loop — migration runs, fails, SMS fires, signed webhook comes back, app verifies, status lands in the log. Every step that would happen in production, happening on your laptop.
To wire real SMS, drop your Telnyx API key, public key (for webhook verification), messaging profile number, and a destination phone number into .env. The stubbed client is replaced automatically.
Why Ed25519 Webhook Verification Matters
Most samples that demonstrate SMS alerts skip the part that proves the message actually arrived. They send the SMS and move on. In production, "I sent the alert" is not the same as "the on-call saw the alert" — the device could be offline, the carrier could drop the message, the SIM could be swapped. Telnyx sends a signed webhook back when an SMS is delivered, and verifying that signature is the difference between an alerting system and an alert-hoping system.
The sample uses Ed25519 (via PyNaCl) because it is fast, has small keys, and the verification is deterministic. The public key is set on the Telnyx client at construction time, and webhooks.unwrap does the signature check on every incoming webhook. If verification fails, the request is rejected — the app does not record a delivery it cannot prove happened.
Where This Fits
Three places this pattern earns its keep:
- Scheduled production migrations — Run them on a cron, get a text the second any step fails. No polling, no dashboards to babysit. The on-call either hears "all good" or hears "migration 003 broke" — never silence.
- CI/CD gates — Wire this into your deploy pipeline so a failing migration blocks the release and pages the on-call before bad code reaches prod. The migration either succeeds and the deploy proceeds, or it fails and the on-call is notified before any user is.
- Multi-tenant SaaS — Run a migration per customer database and get per-tenant failure alerts, so you know exactly which tenant's schema broke and can roll them back individually without affecting the others.
What the Sample Does Not Do
The sample ships a real Ed25519 roundtrip and a real SMS send path (when wired with credentials), but it deliberately stubs a few things to keep the demo runnable on a laptop:
- SMS send is stubbed in the demo. The demo launcher replaces
telnyx_client.messageswith an in-memory recorder so no real SMS leaves the machine. The signed webhook roundtrip is real — the demo generates its own keypair and signs the delivery receipt — but the SMS itself is captured to a local log, not sent over the air. - Migration scripts come from an in-memory map. In production, you would fetch them from a shared store (the Telnyx CloudFS API is the natural fit). The sample's
fetch_migration_scriptfunction is the seam where you would plug in CloudFS. - Schema versioning is in-memory. The demo uses a Python dict for schema versions. In production, use a real SQL DB — the schema_versions table is the source of truth.
These stubs are intentional. They keep the demo under 280 lines, runnable with no credentials, and focused on the part that matters: the failure-to-notification loop.