Skip to content

Phase 3: consent, inbound keywords, quiet hours, rate limits - #3

Merged
timimsms merged 1 commit into
mainfrom
phase-3-consent
Oct 16, 2026
Merged

timimsms merged 1 commit into
mainfrom
phase-3-consent

Conversation

@timimsms

@timimsms timimsms commented Oct 8, 2026

Copy link
Copy Markdown
Owner

Implements Phase 3 of docs/SPEC.md. Stacked on #2, which is stacked on #1. Merge in order.

What's included

  • Consent. Smswire::Consent stores status per phone number and scope, with source, timestamps, last keyword, and evidence metadata. A sender's consent_scope decides which scope applies, so two programs on two numbers keep separate consent.
  • Inbound webhook. POST <mount>/inbound/:provider verifies the signature, stores Smswire::InboundMessage once per provider message id, and answers Twilio with empty TwiML. Observers can implement sms_received for two-way messaging.
  • Keywords. STOP, START, HELP, and their synonyms match only as the whole message, ignoring case, whitespace, and surrounding punctuation. STOPALL opts out of every scope. The configured auto-reply goes out through a built-in compliance category that skips consent checks, so the opt-out confirmation reaches the number that just opted out. When Twilio Advanced Opt-Out already replied, signalled by OptOutType, consent is recorded without a second reply.
  • Category rules. Rules merge over shipped defaults. transactional and otp send unless opted out. marketing requires opt-in and observes 9pm to 8am quiet hours in the recipient's time zone.
  • Rate limits. Configured per category, counted from the deliveries table, and set to either defer or reject.
  • Deferral. Quiet-hour and rate-limit deferrals re-enqueue the action for the resume time, from both deliver_now and the background job.
  • Carrier opt-outs. A Twilio 21610 error records an opt-out with source carrier.

Decisions and deviations, recorded in the spec

  • Rate limits use the deliveries table instead of Rails.cache, so they survive restarts and are shared across processes.
  • The marketing quiet-hours default is 9pm to 8am, the US federal TCPA window, not 9pm to 9am. The README and spec note that some states are stricter.
  • OPTOUT and REVOKE were added to the opt-out keywords.
  • Category overrides merge per key, so configuring one category no longer drops the shipped rules for the others.

Verification

  • 126 tests, 382 assertions, passing locally on Rails 7.1, 7.2, 8.0, and 8.1.
  • Acceptance tests:
    • A signed STOP fixture flips consent, enqueues the confirmation, and delivers it to the opted-out number. A later transactional send is then refused.
    • A marketing send to a number with no opt-in returns :rejected_no_consent.
    • A marketing send at 10pm New York time is re-enqueued for 8am and delivered when the job runs.

🤖 Generated with Claude Code

https://claude.ai/code/session_01L5SuBiSMN4r9qr5CXbKkHV

Add smswire_consents and smswire_inbound_messages. Consent is kept per
phone number and scope, where a sender's consent_scope picks the scope
for both outbound checks and inbound keywords. Inbound webhooks are
verified, stored once per provider message id, and STOP / START / HELP
keywords update consent and send a configurable reply through the
built-in compliance category, unless the provider already replied.

Category rules merge over shipped defaults: transactional and otp send
unless opted out, marketing requires opt-in and observes 9pm-8am
quiet hours in the recipient's time zone. Rate limits are counted from
the deliveries table. Quiet-hour and rate-limit deferrals re-enqueue
the action for when it may send. Carrier opt-out errors record an
opt-out with source carrier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L5SuBiSMN4r9qr5CXbKkHV
@timimsms
timimsms changed the base branch from phase-2-persistence to main October 16, 2026 05:21
@timimsms
timimsms merged commit d4070b3 into main Oct 16, 2026
13 checks passed
@timimsms
timimsms deleted the phase-3-consent 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