A proposed Rails consent ledger
Preserve evidence, enforce suppression and make uncertain delivery visible.
Proposed architecture, not deployed software. These are design and acceptance requirements. No production ledger, provider integration or concurrency test is claimed here. Applicable law, provider contracts and reviewed jurisdiction rules determine the policy.
Keep evidence, not a subscription checkbox
Use an append-only consent-event ledger with rebuildable permission and suppression projections. Record grants, withdrawals, complaints, administrative restrictions, expiry, ownership changes and corrections. Ordinary application roles must not update or delete historical events. Append corrections with links to the original; an integrity digest helps detect alteration but cannot prove that the original consent was valid.
- Identity and scope: tenant, rooftop, represented brand/legal sender, subject where established, protected destination, channel, purpose, campaign/use case and ownership epoch.
- What was accepted: exact disclosure version and content digest, interface version, language, affirmative action, collection source and authenticated actor/system.
- Timing and provenance: occurrence, receipt and effective times; source event ID; expiry where applicable; grants superseded or withdrawn; jurisdiction facts and uncertainty.
- Evidence management: protected evidence location, integrity digest, retention class and access history. A phone prefix alone does not establish residence or applicable law.
Supporting records include contact points, ownership epochs, disclosure/policy versions, suppressions, delivery requests/decisions, attempts, webhook receipts and an outbox. Tenant-scoped foreign keys and uniqueness constraints must prevent cross-tenant joins. Encrypt addresses and use keyed lookup digests; unsalted telephone hashes are easily enumerated. Do not invent email normalization rules that merge distinct destinations.
Make every permission decision explainable
Return allow, deny or review, recording policy version, reason codes, evidence references, evaluation time and projection version. Review holds the request. Missing evidence, uncertain ownership, unavailable policy state or stale critical projections fails closed.
Check platform safety blocks, represented-brand restrictions, channel/purpose withdrawals, provider restrictions, applicable law, grant evidence, sending windows and frequency limits. A narrow grant never overrides a broader applicable block. Tenant isolation must not fragment a single legal sender's suppression obligations: define an authorized sender-wide scope without exposing another tenant's customer data.
Purpose comes from approved templates, business events and content review. A job parameter cannot turn advertising into a transactional notice. Route mixed promotional/service content through the more restrictive policy. Any legally required notice needs its own reviewed workflow and authorized channel; never add a general bypass_suppression switch.
One suppression authority across providers
All send paths—Twilio, SignalWire, Telnyx, manual tools, imports and retries—must consult the same authoritative restrictions. Provider blocks are additional restrictions. Moving to another number, campaign or vendor must never evade a withdrawal. Reconcile provider state, alert on divergence and suspend affected routing when consistency is uncertain.
A START event restores only permission supported by its documented re-enrollment flow. It does not clear a complaint, ownership uncertainty, another channel's withdrawal or a broader safety block. Clear natural-language withdrawals need handling, not an instruction to repeat an exact keyword. Interpret ambiguous scope conservatively and keep evidence for review.
Authorize at enqueue and again at dispatch
- Enqueue: resolve subject, sender and purpose; evaluate policy; commit the request, decision and durable outbox intention in one database transaction. A poller must discover committed outbox work after a crash; an after-commit queue notification alone is insufficient.
- Reserve: reread current restrictions, ownership epoch, content/template version, provider eligibility and time-dependent limits. Reserve one attempt with database uniqueness and a short lease. Count reservations against frequency caps.
- Order competing operations: dispatch and suppression mutations need a defined common ordering boundary. Specify lock rows/order, leases, version fences and every participating writer, including broader sender-wide restrictions. A narrow row lock or a final version read alone cannot prevent all races.
- Submit and record: use the provider's documented endpoint-specific deduplication where available. Record accepted, rejected or outcome unknown. Provider acceptance is not delivery.
Rails transactions apply to a database connection, not a remote HTTP request. Pessimistic locking supplies primitives; the application must establish the complete protocol. See our Rails outbox and synchronization guidance.
A withdrawal transaction appends evidence, advances suppression versions and cancels pending requests. The proposed protocol must be tested under actual database concurrency before any guarantee is made. No fencing token constrains a provider unless that provider participates in the protocol.
Retries and out-of-order callbacks
Give each logical request a durable unique key and separate attempt records. After a timeout, reconcile provider identifiers/status before resending. If acceptance is unknown and deduplication is unavailable, quarantine for review. Retry eligible transient failures with capped backoff and a fresh policy decision; never fail over a policy denial. Exactly-once external delivery is not guaranteed.
Validate webhook signatures using each provider's documented URL/body canonicalization and maintained SDK where available. Check account, tenant, destination and event type. Persist authenticated receipts before success acknowledgment, deduplicate by provider/account/event ID and process idempotently. Use timestamp freshness only where the protocol supplies it; otherwise combine its signature contract with replay controls. Twilio's webhook guidance warns that parameters can evolve.
Preserve both occurrence and receipt time. A stale START must not supersede a newer STOP. Without trustworthy ordering, retain the restriction pending reconciliation. Twilio Event Streams documents duplicate and out-of-order events; that contract must not be assumed for every other product. Delivery receipts cannot create consent.
Ownership and media boundaries
A telephone number is not a permanent person identifier. Reassignment, conflicting ownership or a changed verified owner starts a new ownership epoch and quarantines old grants. Possession verification is not marketing permission. Shared household destinations need explicit scope or conservative suppression.
Push needs usable OS/device permission and the correct app account/purpose binding. Token rotation, logout and shared-device changes invalidate stale bindings. Voice outreach, recording, transcription and biometric processing have separate permission questions. Permission to receive a video or photo does not grant copyright, likeness or reuse rights; review those records independently.
Retain enough evidence, for a justified period
Minimize and encrypt sensitive evidence, restrict exports, redact logs and audit access. Counsel-approved retention and legal holds must reconcile defense, suppression and privacy duties. Append-only does not mean retaining personal data forever: separate identifying payloads from event structure, support approved deletion/key destruction and assess whether residual digests remain personal data. Keep only justified suppression identifiers; do not erase them in a way that silently reenrolls people.
Failure tests before activation
- Grant → enqueue → STOP → dispatch: block across every provider and failover path.
- Race STOP against reservation/submission: show deterministic ordering before authorization and explicit uncertainty for in-flight attempts.
- Duplicate jobs/receipts, reordered START/STOP, replay, invalid signature and tenant mismatch: no duplicate grant, accidental unblock or cross-tenant effect.
- Crash around commit/queue notification; lose the provider response after acceptance: recover the outbox without blind duplicate sends.
- Expired evidence, wrong brand/purpose, ownership change, stale cache and unavailable database: deny or review.
- Mixed-content relabeling, provider suppression, denied push permission and revoked recording/media rights: no bypass.
- Retention expiry, legal hold, export/deletion, backup restore and key rotation: preserve approved privacy outcomes and current withdrawals.
A cautious migration with measurable gates
Inventory every sender first. Import historical lists as unverified unless evidence establishes exact scope. Backfill suppressions before traffic; shadow-evaluate without sending; resolve discrepancies; canary only reviewed cohorts. Require independent policy-fixture review, a tested kill switch and rollback that preserves withdrawals. Monitor suppression lag, uncertain attempts, rejected callbacks and duplicate attempts without logging message bodies or raw destinations.
Success means every attempted send has a reproducible decision, every supported withdrawal route reaches the same authority, and failure-injection tests show no bypass before the defined dispatch boundary. These are proposed acceptance criteria, not results from a running consent system.