Pre-Flight Checklist: Mobayilo Phase 2 & 3 Readiness
Pre-Flight Checklist: Mobayilo Phase 2 & 3 Readiness
Section titled “Pre-Flight Checklist: Mobayilo Phase 2 & 3 Readiness”Review Date: 2026-01-19 Reviewer Role: Senior Full-Stack Engineer & Lead Architect Current Phase: Phase 0 & 1 Complete (Bootstrap + Auth) Upcoming: Phase 2 (Ledger), Phase 3 (VoIP/Twilio)
Phase 2 & 3 Code Review Updates (2026-01-20)
Section titled “Phase 2 & 3 Code Review Updates (2026-01-20)”1) Audit of Gaps (Verification)
Section titled “1) Audit of Gaps (Verification)”Race Conditions: ✅ Fixed
Account.balance_cents is treated as a cache. Updates use atomic SQL increments on cached_balance_cents to avoid read-modify-write races. Ledger idempotency is enforced via Transaction.ensure_idempotency_key.
Ownership Constraints: ⚠️ Partially fixed
DB enforces users.account_id not null, but Account can exist without users at creation time. validate :has_owner, on: :update prevents long-lived orphans while allowing initial signup flows.
Phone Normalization: ✅ Fixed
Call model uses PhoneNormalizable (phony_rails) to normalize E.164.
Security & Rate Limiting: ❌ FAILED
Rack::Attack throttles /api/calls/token and call initiation, but Twilio request signature validation is missing.
CRITICAL: Api::Webhooks::TwilioController skips CSRF but does not validate Twilio signatures, so webhook spoofing is possible.
2) Phase 2 & 3 Code Review Notes
Section titled “2) Phase 2 & 3 Code Review Notes”Ledger integrity: ✅ Strong
Ledger is source of truth; account balance is a cache. Transaction has clear enums and immutable post-commit updates.
VoIP robustness: ✅ Solid Token endpoint identity strings (user/seat) enable attribution. Dialer Stimulus controller handles device state well.
Background processing: ❌ CRITICAL
Api::Webhooks::TwilioController#status processes webhook payloads inline. This risks Twilio timeouts and retries under load. Must offload to ActiveJob/Solid Queue.
Immediate Actions (P0)
Section titled “Immediate Actions (P0)”- Add Twilio Request Signature validation for all webhook endpoints.
- Offload Twilio status processing to a background job (ActiveJob/Solid Queue).
Forward-looking Guidance (Phases 4–6)
Section titled “Forward-looking Guidance (Phases 4–6)”Phase 4 (Stripe): ensure financial auditability
Section titled “Phase 4 (Stripe): ensure financial auditability”Create a Transaction in pending state when the payment intent is created.
When the webhook arrives, locate that transaction by stripe_payment_intent_id and transition to completed.
Phase 5 (Billing): rate snapshot on calls
Section titled “Phase 5 (Billing): rate snapshot on calls”Store a rate snapshot or cost_cents on the Call record to preserve historical pricing accuracy.
Phase 6 (Auto Top-up): callback placement
Section titled “Phase 6 (Auto Top-up): callback placement”Do not place auto top-up logic in controllers.
Prefer Transaction callbacks or a dedicated service/job:
after_commit :check_auto_top_up, on: :create
def check_auto_top_up return unless account.auto_top_up_enabled? return unless account.cached_balance_cents < account.auto_top_up_threshold_cents
AutoTopUpJob.perform_later(account_id)endExecutive Summary
Section titled “Executive Summary”Phase 0 & 1 implementation provides a solid foundation with Rails 8.1, Devise authentication, and Company/Account models. However, critical architectural gaps exist that will cause friction or data integrity issues when implementing the ledger system and VoIP integration.
Key Findings:
- ✅ Strengths: Clean separation of individual/company accounts, passwordless magic links working, Solid Queue configured
- ⚠️ Critical Gaps: No ledger/transaction model, no rate limiting, no phone normalization, mutable balance field vulnerable to race conditions
- 🔒 Security Concerns: Missing Twilio webhook validation, no token endpoint protection, no fraud prevention guardrails
🔴 CRITICAL (Must Fix Before Phase 2)
Section titled “🔴 CRITICAL (Must Fix Before Phase 2)”0. SMTP TLS Verification Must Be Restored (Production Blocker)
Section titled “0. SMTP TLS Verification Must Be Restored (Production Blocker)”Issue: Development currently allows SMTP_DISABLE_CRL_CHECK=true, which sets
openssl_verify_mode = VERIFY_NONE to bypass CRL failures.
Why this matters:
- Disables TLS verification
- Accepts potentially untrusted certificates
- Not acceptable for production
Required Changes:
- Remove
SMTP_DISABLE_CRL_CHECKin production - Ensure proper CA/CRL validation for ZeptoMail
1. Ledger Architecture: Race Condition Vulnerability
Section titled “1. Ledger Architecture: Race Condition Vulnerability”Issue: The current Account.balance_cents field is mutable and directly updatable, creating race conditions when multiple processes (Stripe webhook + Twilio status callback) attempt concurrent balance updates.
Current State:
t.integer :balance_cents, default: 0, null: false
# app/models/account.rbvalidates :balance_cents, numericality: { greater_than_or_equal_to: 0 }Problem:
- No
Transactionmodel exists yet - Balance can be updated directly:
account.update!(balance_cents: new_value) - Two webhooks processing simultaneously can cause lost updates
- No audit trail for balance changes
- No idempotency protection
Required Changes:
- Create immutable
Transactionledger model:
create_table :transactions do |t| t.references :account, null: false, foreign_key: true t.string :idempotency_key, null: false, index: { unique: true } t.integer :amount_cents, null: false t.string :transaction_type, null: false # purchase, call_charge, refund, bonus, adjustment t.string :status, null: false, default: 'pending' # pending, completed, failed t.jsonb :metadata, default: {} t.references :related_call, foreign_key: { to_table: :calls }, null: true t.string :stripe_payment_intent_id, index: true t.timestampsend- Make balance a computed/cached value:
def current_balance_cents transactions.completed.sum(:amount_cents)end
# Add cached balance for performance# db/migrate/xxx_add_cached_balance_to_accounts.rbadd_column :accounts, :cached_balance_cents, :integer, default: 0, null: falseadd_index :accounts, :cached_balance_cents- Implement idempotent transaction creation service:
module Ledger class CreateTransaction def call(account:, amount_cents:, type:, idempotency_key:, metadata: {}) Transaction.create_with( amount_cents: amount_cents, transaction_type: type, metadata: metadata, status: 'completed' ).find_or_create_by!( account: account, idempotency_key: idempotency_key ) rescue ActiveRecord::RecordNotUnique Transaction.find_by!(idempotency_key: idempotency_key) end endendImpact: Without this, Phase 2 ledger will have data integrity issues from day one.
2. Missing Database Constraints for Wallet Owner Relationship
Section titled “2. Missing Database Constraints for Wallet Owner Relationship”Issue: The Account model serves as the wallet owner for both User and CompanySeat, but there’s no enforcement preventing orphaned accounts or ensuring proper ownership.
Current State:
# users tablet.bigint :account_id, null: falseadd_foreign_key :users, :accounts
# company_seats tablet.bigint :account_id, null: falseadd_foreign_key :company_seats, :accountsProblem:
- Individual accounts should have exactly 1 owner user
- Company accounts can have multiple users + seats
- No constraint prevents creating an individual account with 0 users
- No validation ensures company accounts have at least 1 owner user
Required Changes:
- Add account ownership validation:
validate :has_owner, on: :update
private
def has_owner return if individual? && users.where(role: :owner).exists? return if company? && users.where(role: :owner).exists?
errors.add(:base, "Account must have at least one owner")end- Add database-level check (optional but recommended):
-- Prevent deletion of last owner-- This requires a trigger or application-level enforcementImpact: Prevents orphaned wallets and ensures billing attribution is always possible.
3. No Phone Number Normalization Library
Section titled “3. No Phone Number Normalization Library”Issue: Phase 3 requires E.164 phone number normalization, but no normalization library is installed. This will lead to “dirty data” in call logs and failed Twilio API calls.
Current State:
# Gemfile - NO phony_rails or similargrep -i phony Gemfile # No resultsProblem:
- Users will enter numbers in various formats:
(555) 123-4567,+1-555-123-4567,5551234567 - Twilio requires E.164 format:
+15551234567 - Without normalization, call logs will have inconsistent data
- Rate lookups by prefix will fail
- Call history filtering/searching will be broken
Required Changes:
- Add
phony_railsgem:
# Gemfilegem 'phony_rails'- Create phone normalization concern:
module PhoneNormalizable extend ActiveSupport::Concern
included do def self.normalize_phone_attribute(attr_name) phony_normalize attr_name, default_country_code: 'US' validates attr_name, phony_plausible: true end endend- Apply to Call model (when created in Phase 3):
class Call < ApplicationRecord include PhoneNormalizable normalize_phone_attribute :to_numberendImpact: Installing this NOW prevents data migration headaches later. Dirty data in production is expensive to clean.
🔒 SECURITY (Must Fix Before Phase 3)
Section titled “🔒 SECURITY (Must Fix Before Phase 3)”4. No Rate Limiting Infrastructure
Section titled “4. No Rate Limiting Infrastructure”Issue: The PRD explicitly requires rate limiting for “login, registration, token issuance, and call initiation,” but no rate limiting is configured.
Current State:
grep -r "Rack::Attack" config/ # No resultsProblem:
- Token endpoint (
GET /api/calls/token) will be vulnerable to DoS - Toll fraud: attackers can generate unlimited tokens and place calls
- No protection against credential stuffing on login
- No throttling on magic link requests
Required Changes:
- Add
rack-attackgem:
# Gemfilegem 'rack-attack'- Configure rate limits:
class Rack::Attack # Throttle login attempts by email throttle('logins/email', limit: 5, period: 20.minutes) do |req| if req.path == '/users/sign_in' && req.post? req.params['user']['email'].to_s.downcase.presence end end
# Throttle magic link requests throttle('magic_links/email', limit: 3, period: 5.minutes) do |req| if req.path.start_with?('/seats/sign_in') && req.post? req.params['passwordless']['email'].to_s.downcase.presence end end
# CRITICAL: Throttle token generation (Phase 3) throttle('api/token', limit: 10, period: 1.minute) do |req| if req.path == '/api/calls/token' && req.get? req.env['warden']&.user&.id || req.ip end end
# CRITICAL: Throttle call initiation (Phase 3) throttle('api/calls/create', limit: 5, period: 1.minute) do |req| if req.path == '/api/calls' && req.post? req.env['warden']&.user&.id || req.ip end endend
# config/application.rbconfig.middleware.use Rack::AttackImpact: Without this, the app is vulnerable to toll fraud and DoS attacks from day one of Phase 3.
5. Missing Twilio Webhook Signature Validation
Section titled “5. Missing Twilio Webhook Signature Validation”Issue: Phase 3 will expose Twilio webhook endpoints (/api/webhooks/twilio/*), but there’s no infrastructure to validate Twilio’s request signatures.
Current State:
- No webhook validation code exists
- No Twilio gem installed for signature verification
Problem:
- Attackers can forge webhook requests to manipulate call status
- Fake “call completed” webhooks could skip billing
- Fake “payment succeeded” webhooks could credit accounts for free
Required Changes:
- Add Twilio gem:
# Gemfilegem 'twilio-ruby'- Create webhook validation concern:
module TwilioWebhookValidatable extend ActiveSupport::Concern
included do before_action :validate_twilio_signature, only: [:voice, :status] end
private
def validate_twilio_signature validator = Twilio::Security::RequestValidator.new(ENV['TWILIO_AUTH_TOKEN']) signature = request.headers['X-Twilio-Signature'] url = request.original_url params_hash = request.POST
unless validator.validate(url, params_hash, signature) head :forbidden end endend- Apply to webhook controller:
class Api::Webhooks::TwilioController < ApplicationController include TwilioWebhookValidatable skip_before_action :verify_authenticity_token
def voice # TwiML generation end
def status # Call status update endendImpact: Critical security vulnerability if not fixed before Phase 3 launch.
6. No Stripe Webhook Signature Verification
Section titled “6. No Stripe Webhook Signature Verification”Issue: Similar to Twilio, Stripe webhooks (Phase 4) need signature verification to prevent fraud.
Current State:
- No Stripe webhook controller exists yet
- No signature verification infrastructure
Required Changes:
- Add Stripe gem (if not already present):
# Gemfilegem 'stripe'- Create webhook controller with verification:
class Api::Webhooks::StripeController < ApplicationController skip_before_action :verify_authenticity_token
def create payload = request.body.read sig_header = request.env['HTTP_STRIPE_SIGNATURE'] endpoint_secret = ENV['STRIPE_WEBHOOK_SECRET']
begin event = Stripe::Webhook.construct_event( payload, sig_header, endpoint_secret ) rescue JSON::ParserError, Stripe::SignatureVerificationError => e head :bad_request return end
case event.type when 'payment_intent.succeeded' handle_payment_success(event.data.object) end
head :ok end
private
def handle_payment_success(payment_intent) idempotency_key = "stripe_pi_#{payment_intent.id}" # Use Ledger::CreateTransaction service endendImpact: Prevents attackers from crediting accounts without payment.
7. Token Endpoint User Identification Risk
Section titled “7. Token Endpoint User Identification Risk”Issue: The PRD mentions a GET /api/calls/token endpoint for Twilio token generation, but there’s no design for how to securely identify the user and prevent unauthorized token generation.
Current State:
- No token endpoint exists yet
- No design documented for user identification
Problem:
- Devise provides
current_userfor password-authenticated users - Passwordless provides
current_company_seatfor magic link users - The
ApplicationControllerhascurrent_accounthelper, but this doesn’t identify the individual caller - For company accounts, we need to know which seat is placing the call for usage attribution
Required Changes:
- Create unified caller identification:
def current_caller return current_user if current_user.present? return current_company_seat if current_company_seat.present? nilend
def current_caller_identity if current_user.present? "user_#{current_user.id}" elsif current_company_seat.present? "seat_#{current_company_seat.id}" else nil endend- Token endpoint with caller identification:
class Api::CallsController < ApplicationController before_action :require_authenticated_actor!
def token # Check balance if current_account.cached_balance_cents < 100 # $1 minimum render json: { error: 'Insufficient balance' }, status: :payment_required return end
# Generate Twilio token with caller identity token = Twilio::JWT::ClientScope.new( account_sid: ENV['TWILIO_ACCOUNT_SID'], signing_key_sid: ENV['TWILIO_API_KEY_SID'], signing_key_secret: ENV['TWILIO_API_KEY_SECRET'], identity: current_caller_identity, ttl: 3600 )
render json: { token: token.to_jwt } endendImpact: Ensures proper attribution of calls to individual users/seats for billing and prevents unauthorized token generation.
⚡ SCALABILITY (Recommended Before Phase 3)
Section titled “⚡ SCALABILITY (Recommended Before Phase 3)”8. Webhook Processing Strategy
Section titled “8. Webhook Processing Strategy”Issue: The PRD asks whether background processing (Solid Queue) is needed for Twilio status updates to avoid blocking web workers.
Analysis:
Current State:
- Solid Queue is installed and configured
- No webhook controllers exist yet
Recommendation: Use background jobs for ALL webhook processing
Reasoning:
-
Twilio status callbacks can be slow if they trigger:
- Ledger transaction creation (database write)
- Balance recalculation (potentially complex query)
- Turbo Stream broadcasts (for real-time UI updates)
- Email notifications (SMTP latency)
-
Stripe webhooks can be slow if they trigger:
- Payment verification API calls
- Ledger transaction creation
- Email receipts
-
Webhook timeout risk:
- Twilio expects a 200 response within 15 seconds
- If processing takes >15s, Twilio retries, causing duplicate processing
- Even with idempotency keys, retries waste resources
Required Changes:
- Create webhook processing jobs:
class ProcessTwilioStatusJob < ApplicationJob queue_as :webhooks
def perform(call_sid, status, duration_seconds, params) call = Call.find_by!(twilio_sid: call_sid) call.update!( status: status, duration_seconds: duration_seconds, ended_at: Time.current )
if status == 'completed' && duration_seconds.to_i > 0 BillingService.charge_for_call(call) end endend
# app/jobs/process_stripe_payment_job.rbclass ProcessStripePaymentJob < ApplicationJob queue_as :webhooks
def perform(payment_intent_id) # Idempotent ledger transaction creation Ledger::CreateTransaction.call( account: account, amount_cents: amount, type: 'purchase', idempotency_key: "stripe_pi_#{payment_intent_id}", metadata: { stripe_payment_intent_id: payment_intent_id } ) endend- Webhook controllers enqueue jobs immediately:
def status ProcessTwilioStatusJob.perform_later( params[:CallSid], params[:CallStatus], params[:CallDuration], params.to_unsafe_h )
head :ok # Respond immediatelyendImpact: Prevents webhook timeouts and retries, improves reliability, allows horizontal scaling of webhook processing.
9. Database Connection Pooling for Webhooks
Section titled “9. Database Connection Pooling for Webhooks”Issue: If webhooks are processed in background jobs, the default connection pool size may be insufficient.
Current State:
max_connections: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>Problem:
- Default pool size is 5 connections
- If Solid Queue runs 10 workers, they’ll compete for connections
- Web workers also need connections
- Under load, jobs will block waiting for connections
Required Changes:
- Increase connection pool for production:
production: primary: &primary_production <<: *default database: mobayilo_production username: mobayilo password: <%= ENV["MOBAYILO_DATABASE_PASSWORD"] %> pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i + ENV.fetch("SOLID_QUEUE_WORKERS") { 5 }.to_i %>- Configure Solid Queue worker count:
production: dispatchers: - polling_interval: 1 batch_size: 500 workers: - queues: "*" threads: 5 processes: 3 polling_interval: 0.1Impact: Prevents job processing bottlenecks under load.
🛠️ REFACTOR (Fix Before Phase 3 Stimulus Controllers)
Section titled “🛠️ REFACTOR (Fix Before Phase 3 Stimulus Controllers)”10. Missing Stimulus Controller Structure
Section titled “10. Missing Stimulus Controller Structure”Issue: Phase 3 requires a Stimulus controller for the Dialer integrating Twilio Voice JS, but there’s no Stimulus infrastructure set up yet.
Current State:
// Entry point for the build script in your package.jsonProblem:
- No Stimulus controllers directory
- No Stimulus imports configured
- No example controller to follow
Required Changes:
- Set up Stimulus properly:
import "@hotwired/turbo-rails"import "./controllers"- Create controllers directory:
mkdir -p app/javascript/controllers- Create index file:
import { application } from "./application"
// Eager load all controllersimport { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"eagerLoadControllersFrom("controllers", application)- Create application controller:
import { Application } from "@hotwired/stimulus"
const application = Application.start()application.debug = falsewindow.Stimulus = application
export { application }- Create placeholder dialer controller:
import { Controller } from "@hotwired/stimulus"
export default class extends Controller { static targets = ["numberInput", "callButton", "status"]
connect() { console.log("Dialer controller connected") // Phase 3: Initialize Twilio Device here }
dial() { const number = this.numberInputTarget.value console.log("Dialing:", number) // Phase 3: Call Twilio Device.connect() }
hangup() { console.log("Hanging up") // Phase 3: Call Twilio Device.disconnectAll() }}Impact: Having this structure in place now makes Phase 3 implementation smoother.
11. Missing API Namespace and Versioning
Section titled “11. Missing API Namespace and Versioning”Issue: The architecture doc mentions JSON endpoints like /api/calls/token, but there’s no API namespace configured in routes.
Current State:
# No /api namespace definedProblem:
- API endpoints mixed with HTML routes
- No versioning strategy
- No API-specific error handling
- No API-specific authentication (e.g., token-based for future mobile apps)
Required Changes:
- Create API namespace:
namespace :api do namespace :calls do get :token end
resources :calls, only: [:create, :show]
namespace :webhooks do namespace :twilio do post :voice post :status end
namespace :stripe do post :create end endend- Create base API controller:
module Api class BaseController < ActionController::API include ActionController::HttpAuthentication::Token::ControllerMethods
rescue_from ActiveRecord::RecordNotFound, with: :not_found rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
private
def not_found render json: { error: 'Not found' }, status: :not_found end
def unprocessable_entity(exception) render json: { error: exception.message }, status: :unprocessable_entity end endend- Inherit from base:
module Api class CallsController < BaseController # ... endendImpact: Cleaner API design, easier to add versioning later (e.g., /api/v1/calls/token).
12. No Call Model or Migration Planned
Section titled “12. No Call Model or Migration Planned”Issue: Phase 3 requires a Call model to persist call records, but this hasn’t been created yet.
Current State:
- No
Callmodel exists - No migration planned
Required Changes:
- Create Call model migration:
class CreateCalls < ActiveRecord::Migration[8.1] def change create_table :calls do |t| t.references :account, null: false, foreign_key: true t.references :caller, polymorphic: true, null: false # User or CompanySeat
# Twilio identifiers t.string :twilio_sid, index: { unique: true } t.string :twilio_parent_call_sid
# Call details t.string :to_number, null: false t.string :from_number t.string :status, null: false, default: 'initiated'
# Timing t.datetime :initiated_at t.datetime :answered_at t.datetime :ended_at t.integer :duration_seconds
# Billing t.integer :cost_cents t.string :rate_id # Reference to CallRate t.references :charge_transaction, foreign_key: { to_table: :transactions }
# Metadata t.jsonb :metadata, default: {}
t.timestamps end
add_index :calls, :status add_index :calls, :initiated_at add_index :calls, [:account_id, :initiated_at] endend- Create Call model:
class Call < ApplicationRecord include PhoneNormalizable
belongs_to :account belongs_to :caller, polymorphic: true belongs_to :charge_transaction, class_name: 'Transaction', optional: true
normalize_phone_attribute :to_number
enum :status, { initiated: 'initiated', ringing: 'ringing', in_progress: 'in-progress', completed: 'completed', busy: 'busy', no_answer: 'no-answer', failed: 'failed', canceled: 'canceled' }, suffix: true
validates :twilio_sid, uniqueness: true, allow_nil: true validates :to_number, presence: trueendImpact: Required for Phase 3; creating now allows for better planning.
13. No CallRate Model for Pricing
Section titled “13. No CallRate Model for Pricing”Issue: The architecture doc mentions a CallRate model for per-minute pricing by destination, but this doesn’t exist.
Current State:
- No
CallRatemodel - No pricing data
Required Changes:
- Create CallRate model:
class CreateCallRates < ActiveRecord::Migration[8.1] def change create_table :call_rates do |t| t.string :country_code, null: false t.string :country_name, null: false t.string :prefix, null: false t.integer :price_per_minute_cents, null: false t.boolean :active, default: true, null: false
t.timestamps end
add_index :call_rates, :prefix add_index :call_rates, [:country_code, :active] endend- Create model with lookup logic:
class CallRate < ApplicationRecord validates :prefix, presence: true, uniqueness: true validates :price_per_minute_cents, numericality: { greater_than: 0 }
scope :active, -> { where(active: true) }
def self.for_number(e164_number) # Match longest prefix first active.where("? LIKE prefix || '%'", e164_number) .order(Arel.sql('LENGTH(prefix) DESC')) .first end
def price_per_minute_dollars price_per_minute_cents / 100.0 endend- Seed with basic rates:
CallRate.find_or_create_by!(prefix: '+1', country_code: 'US', country_name: 'United States') do |rate| rate.price_per_minute_cents = 2 # $0.02/minend
CallRate.find_or_create_by!(prefix: '+44', country_code: 'GB', country_name: 'United Kingdom') do |rate| rate.price_per_minute_cents = 5 # $0.05/minendImpact: Required for Phase 3 rate calculator and Phase 5 billing.
14. Insufficient Balance Validation
Section titled “14. Insufficient Balance Validation”Issue: The token endpoint needs to check if the user has sufficient balance before issuing a token, but there’s no service or validation for this.
Current State:
- No balance checking logic
- No minimum balance constant defined
Required Changes:
- Define minimum balance constant:
module Mobayilo MINIMUM_CALL_BALANCE_CENTS = 100 # $1.00end- Add balance check to Account model:
def sufficient_balance_for_call? cached_balance_cents >= Mobayilo::MINIMUM_CALL_BALANCE_CENTSend
def balance_dollars cached_balance_cents / 100.0end- Use in token endpoint:
def token unless current_account.sufficient_balance_for_call? render json: { error: 'Insufficient balance', current_balance: current_account.balance_dollars, minimum_required: Mobayilo::MINIMUM_CALL_BALANCE_CENTS / 100.0 }, status: :payment_required return end
# Generate token...endImpact: Prevents users from initiating calls they can’t afford, reducing support burden.
📋 Summary Checklist
Section titled “📋 Summary Checklist”Critical (Block Phase 2)
Section titled “Critical (Block Phase 2)”- #1: Create immutable
Transactionledger model with idempotency keys - #2: Add account ownership validation to prevent orphaned wallets
- #3: Install
phony_railsand create phone normalization infrastructure
Security (Block Phase 3)
Section titled “Security (Block Phase 3)”- #4: Install and configure
rack-attackfor rate limiting - #5: Add Twilio webhook signature validation
- #6: Add Stripe webhook signature verification
- #7: Design and implement secure token endpoint with caller identification
Scalability (Recommended Before Phase 3)
Section titled “Scalability (Recommended Before Phase 3)”- #8: Implement background job processing for all webhooks
- #9: Increase database connection pool size for Solid Queue workers
- #9a: Ensure Solid Queue scheduler is running and recurring tasks are loaded (e.g.,
config/recurring.yml)
Refactor (Fix Before Phase 3 Implementation)
Section titled “Refactor (Fix Before Phase 3 Implementation)”- #10: Set up Stimulus controller structure and create dialer controller skeleton
- #11: Create
/apinamespace with base controller and error handling - #12: Create
Callmodel and migration - #13: Create
CallRatemodel and seed basic pricing data - #14: Implement balance validation for call initiation
Recommended Implementation Order
Section titled “Recommended Implementation Order”Week 1 (Before Phase 2 Starts)
Section titled “Week 1 (Before Phase 2 Starts)”- Create
Transactionmodel (#1) - CRITICAL - Install
phony_rails(#3) - CRITICAL - Add account ownership validation (#2) - CRITICAL
- Create
CallandCallRatemodels (#12, #13) - FOUNDATION
Week 2 (During Phase 2)
Section titled “Week 2 (During Phase 2)”- Install
rack-attackand configure rate limits (#4) - SECURITY - Set up Stimulus controllers (#10) - REFACTOR
- Create
/apinamespace (#11) - REFACTOR - Implement balance validation (#14) - REFACTOR
Week 3 (Before Phase 3 Starts)
Section titled “Week 3 (Before Phase 3 Starts)”- Add Twilio webhook validation (#5) - SECURITY
- Add Stripe webhook validation (#6) - SECURITY
- Design token endpoint with caller identification (#7) - SECURITY
- Implement background webhook processing (#8) - SCALABILITY
- Tune database connection pool (#9) - SCALABILITY
Additional Recommendations
Section titled “Additional Recommendations”Testing Strategy
Section titled “Testing Strategy”- Unit tests: All ledger transaction creation logic (idempotency is critical)
- Integration tests: Webhook signature validation (security is critical)
- System tests: Dialer Stimulus controller with mocked Twilio Device
Monitoring & Observability
Section titled “Monitoring & Observability”- Add logging for all ledger transactions
- Add metrics for webhook processing time
- Add alerts for failed webhook signature validations
- Add alerts for rate limit violations
Documentation Needs
Section titled “Documentation Needs”- Document idempotency key format for each transaction type
- Document webhook retry behavior and idempotency guarantees
- Document rate limit thresholds for each endpoint
- Document E.164 normalization rules and supported countries
Conclusion
Section titled “Conclusion”The Phase 0 & 1 implementation is solid but has critical gaps that must be addressed before Phase 2 & 3:
- Ledger architecture is the highest priority - the current mutable balance field will cause data integrity issues
- Security infrastructure (rate limiting, webhook validation) must be in place before exposing API endpoints
- Phone normalization should be installed NOW to prevent dirty data
- Background job processing for webhooks is essential for scalability
Recommendation: Address all CRITICAL items before starting Phase 2, and all SECURITY items before starting Phase 3. The REFACTOR items can be done in parallel with Phase 2 implementation.