Skip to content

Phase 2: deliveries, status callbacks, dedupe, log subscriber - #2

Merged
timimsms merged 1 commit into
mainfrom
phase-2-persistence
Oct 16, 2026
Merged

timimsms merged 1 commit into
mainfrom
phase-2-persistence

Conversation

@timimsms

@timimsms timimsms commented Oct 8, 2026

Copy link
Copy Markdown
Owner

Implements Phase 2 of docs/SPEC.md. Stacked on #1; merge that first and this PR will retarget to main.

What's included

  • Engine. Smswire::Engine replaces the Railtie, with bin/rails smswire:install:migrations and a status callback route at POST <mount>/status/:provider.
  • Deliveries table. One smswire_deliveries row per send, with provider id, status, segments, encoding, price, error, attempts, and timestamps. The primary key follows the app's generator setting, as Active Storage does.
  • Safe retries. Rows are claimed with an atomic conditional update before the provider is called. A transient error releases the claim, so the job retry reuses the same row. A claim held by a crashed worker expires after two minutes.
  • Deduplication. Identical messages inside dedupe_window, 10 minutes by default, return :duplicate. An explicit idempotency_key: on text overrides what counts as identical. A unique index makes concurrent duplicates safe.
  • Status callbacks. Twilio's HMAC-SHA1 signature is verified against callbacks_url, so TLS-terminating proxies don't break it. Callback URLs carry the delivery id, so a callback that beats the API response still finds its row. Status only moves forward, so late callbacks are ignored. Observers can implement sms_status_updated(delivery, update).
  • Body storage. On by default, and off for the otp category.
  • Log subscriber. Logs deliveries, rejections, and status updates with masked phone numbers and no bodies.

Spec decisions settled

  • Body storage default, and the primary key type. Both are recorded in the spec's open decisions section.
  • Columns are named to_number and from_number, since to and from are reserved words in PostgreSQL and MySQL.
  • callbacks_url replaces callbacks_host, because the engine mount path is part of the URL Twilio signs.
  • Rejected messages are not persisted; they surface through reject.smswire and observers.

Verification

  • 94 tests, 289 assertions, passing locally on Rails 7.1, 7.2, 8.0, and 8.1.
  • The acceptance test sends through Twilio against WebMock with a StatusCallback carrying the delivery id. It replays signed sent and delivered callbacks and checks the row moves from pending to queued, sent, and delivered, then ignores a late sent.

🤖 Generated with Claude Code

https://claude.ai/code/session_01L5SuBiSMN4r9qr5CXbKkHV

Convert the Railtie to an isolated Smswire::Engine with an install
migrations task and a smswire_deliveries table that follows the app's
primary key type. Every send is claimed atomically as a Delivery row
before the provider is called; transient errors release the claim so
the retry reuses the row, and stale claims expire after two minutes.

Identical messages inside dedupe_window return :duplicate. Status
callbacks are verified with Twilio's HMAC-SHA1 signature, matched by
delivery id or provider message id, and only move status forward.
Bodies are stored per category, with otp excluded by default. A log
subscriber reports deliveries, rejections, and status updates with
masked numbers and no bodies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L5SuBiSMN4r9qr5CXbKkHV
@timimsms
timimsms deleted the branch phase-1-core October 16, 2026 05:18
@timimsms timimsms closed this Oct 16, 2026
@timimsms timimsms reopened this Oct 16, 2026
@timimsms
timimsms changed the base branch from phase-1-core to main October 16, 2026 05:19
@timimsms
timimsms merged commit ce9d74e into main Oct 16, 2026
26 checks passed
@timimsms
timimsms deleted the phase-2-persistence branch October 16, 2026 05:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant