Skip to content

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)”

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.

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.


  1. Add Twilio Request Signature validation for all webhook endpoints.
  2. Offload Twilio status processing to a background job (ActiveJob/Solid Queue).

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.

Store a rate snapshot or cost_cents on the Call record to preserve historical pricing accuracy.

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)
end

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

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_CHECK in 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:

db/schema.rb
t.integer :balance_cents, default: 0, null: false
# app/models/account.rb
validates :balance_cents, numericality: { greater_than_or_equal_to: 0 }

Problem:

  • No Transaction model 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:

  1. Create immutable Transaction ledger model:
db/migrate/xxx_create_transactions.rb
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.timestamps
end
  1. Make balance a computed/cached value:
app/models/account.rb
def current_balance_cents
transactions.completed.sum(:amount_cents)
end
# Add cached balance for performance
# db/migrate/xxx_add_cached_balance_to_accounts.rb
add_column :accounts, :cached_balance_cents, :integer, default: 0, null: false
add_index :accounts, :cached_balance_cents
  1. Implement idempotent transaction creation service:
app/services/ledger/create_transaction.rb
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
end
end

Impact: 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 table
t.bigint :account_id, null: false
add_foreign_key :users, :accounts
# company_seats table
t.bigint :account_id, null: false
add_foreign_key :company_seats, :accounts

Problem:

  • 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:

  1. Add account ownership validation:
app/models/account.rb
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
  1. Add database-level check (optional but recommended):
-- Prevent deletion of last owner
-- This requires a trigger or application-level enforcement

Impact: Prevents orphaned wallets and ensures billing attribution is always possible.


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:

Terminal window
# Gemfile - NO phony_rails or similar
grep -i phony Gemfile # No results

Problem:

  • 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:

  1. Add phony_rails gem:
# Gemfile
gem 'phony_rails'
  1. Create phone normalization concern:
app/models/concerns/phone_normalizable.rb
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
end
end
  1. Apply to Call model (when created in Phase 3):
app/models/call.rb
class Call < ApplicationRecord
include PhoneNormalizable
normalize_phone_attribute :to_number
end

Impact: Installing this NOW prevents data migration headaches later. Dirty data in production is expensive to clean.


Issue: The PRD explicitly requires rate limiting for “login, registration, token issuance, and call initiation,” but no rate limiting is configured.

Current State:

Terminal window
grep -r "Rack::Attack" config/ # No results

Problem:

  • 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:

  1. Add rack-attack gem:
# Gemfile
gem 'rack-attack'
  1. Configure rate limits:
config/initializers/rack_attack.rb
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
end
end
# config/application.rb
config.middleware.use Rack::Attack

Impact: 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:

  1. Add Twilio gem:
# Gemfile
gem 'twilio-ruby'
  1. Create webhook validation concern:
app/controllers/concerns/twilio_webhook_validatable.rb
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
end
end
  1. Apply to webhook controller:
app/controllers/api/webhooks/twilio_controller.rb
class Api::Webhooks::TwilioController < ApplicationController
include TwilioWebhookValidatable
skip_before_action :verify_authenticity_token
def voice
# TwiML generation
end
def status
# Call status update
end
end

Impact: 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:

  1. Add Stripe gem (if not already present):
# Gemfile
gem 'stripe'
  1. Create webhook controller with verification:
app/controllers/api/webhooks/stripe_controller.rb
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
end
end

Impact: 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_user for password-authenticated users
  • Passwordless provides current_company_seat for magic link users
  • The ApplicationController has current_account helper, 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:

  1. Create unified caller identification:
app/controllers/application_controller.rb
def current_caller
return current_user if current_user.present?
return current_company_seat if current_company_seat.present?
nil
end
def current_caller_identity
if current_user.present?
"user_#{current_user.id}"
elsif current_company_seat.present?
"seat_#{current_company_seat.id}"
else
nil
end
end
  1. Token endpoint with caller identification:
app/controllers/api/calls_controller.rb
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 }
end
end

Impact: Ensures proper attribution of calls to individual users/seats for billing and prevents unauthorized token generation.


Section titled “⚡ SCALABILITY (Recommended Before Phase 3)”

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:

  1. 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)
  2. Stripe webhooks can be slow if they trigger:

    • Payment verification API calls
    • Ledger transaction creation
    • Email receipts
  3. 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:

  1. Create webhook processing jobs:
app/jobs/process_twilio_status_job.rb
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
end
end
# app/jobs/process_stripe_payment_job.rb
class 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 }
)
end
end
  1. Webhook controllers enqueue jobs immediately:
app/controllers/api/webhooks/twilio_controller.rb
def status
ProcessTwilioStatusJob.perform_later(
params[:CallSid],
params[:CallStatus],
params[:CallDuration],
params.to_unsafe_h
)
head :ok # Respond immediately
end

Impact: 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:

config/database.yml
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:

  1. Increase connection pool for production:
config/database.yml
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 %>
  1. Configure Solid Queue worker count:
config/queue.yml
production:
dispatchers:
- polling_interval: 1
batch_size: 500
workers:
- queues: "*"
threads: 5
processes: 3
polling_interval: 0.1

Impact: Prevents job processing bottlenecks under load.


🛠️ REFACTOR (Fix Before Phase 3 Stimulus Controllers)

Section titled “🛠️ REFACTOR (Fix Before Phase 3 Stimulus Controllers)”

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:

app/javascript/application.js
// Entry point for the build script in your package.json

Problem:

  • No Stimulus controllers directory
  • No Stimulus imports configured
  • No example controller to follow

Required Changes:

  1. Set up Stimulus properly:
app/javascript/application.js
import "@hotwired/turbo-rails"
import "./controllers"
  1. Create controllers directory:
Terminal window
mkdir -p app/javascript/controllers
  1. Create index file:
app/javascript/controllers/index.js
import { application } from "./application"
// Eager load all controllers
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
eagerLoadControllersFrom("controllers", application)
  1. Create application controller:
app/javascript/controllers/application.js
import { Application } from "@hotwired/stimulus"
const application = Application.start()
application.debug = false
window.Stimulus = application
export { application }
  1. Create placeholder dialer controller:
app/javascript/controllers/dialer_controller.js
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.


Issue: The architecture doc mentions JSON endpoints like /api/calls/token, but there’s no API namespace configured in routes.

Current State:

config/routes.rb
# No /api namespace defined

Problem:

  • 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:

  1. Create API namespace:
config/routes.rb
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
end
end
  1. Create base API controller:
app/controllers/api/base_controller.rb
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
end
end
  1. Inherit from base:
app/controllers/api/calls_controller.rb
module Api
class CallsController < BaseController
# ...
end
end

Impact: Cleaner API design, easier to add versioning later (e.g., /api/v1/calls/token).


Issue: Phase 3 requires a Call model to persist call records, but this hasn’t been created yet.

Current State:

  • No Call model exists
  • No migration planned

Required Changes:

  1. Create Call model migration:
db/migrate/xxx_create_calls.rb
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]
end
end
  1. Create Call model:
app/models/call.rb
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: true
end

Impact: Required for Phase 3; creating now allows for better planning.


Issue: The architecture doc mentions a CallRate model for per-minute pricing by destination, but this doesn’t exist.

Current State:

  • No CallRate model
  • No pricing data

Required Changes:

  1. Create CallRate model:
db/migrate/xxx_create_call_rates.rb
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]
end
end
  1. Create model with lookup logic:
app/models/call_rate.rb
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
end
end
  1. Seed with basic rates:
db/seeds.rb
CallRate.find_or_create_by!(prefix: '+1', country_code: 'US', country_name: 'United States') do |rate|
rate.price_per_minute_cents = 2 # $0.02/min
end
CallRate.find_or_create_by!(prefix: '+44', country_code: 'GB', country_name: 'United Kingdom') do |rate|
rate.price_per_minute_cents = 5 # $0.05/min
end

Impact: Required for Phase 3 rate calculator and Phase 5 billing.


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:

  1. Define minimum balance constant:
config/initializers/mobayilo.rb
module Mobayilo
MINIMUM_CALL_BALANCE_CENTS = 100 # $1.00
end
  1. Add balance check to Account model:
app/models/account.rb
def sufficient_balance_for_call?
cached_balance_cents >= Mobayilo::MINIMUM_CALL_BALANCE_CENTS
end
def balance_dollars
cached_balance_cents / 100.0
end
  1. Use in token endpoint:
app/controllers/api/calls_controller.rb
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...
end

Impact: Prevents users from initiating calls they can’t afford, reducing support burden.


  • #1: Create immutable Transaction ledger model with idempotency keys
  • #2: Add account ownership validation to prevent orphaned wallets
  • #3: Install phony_rails and create phone normalization infrastructure
  • #4: Install and configure rack-attack for rate limiting
  • #5: Add Twilio webhook signature validation
  • #6: Add Stripe webhook signature verification
  • #7: Design and implement secure token endpoint with caller identification
  • #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 /api namespace with base controller and error handling
  • #12: Create Call model and migration
  • #13: Create CallRate model and seed basic pricing data
  • #14: Implement balance validation for call initiation

  1. Create Transaction model (#1) - CRITICAL
  2. Install phony_rails (#3) - CRITICAL
  3. Add account ownership validation (#2) - CRITICAL
  4. Create Call and CallRate models (#12, #13) - FOUNDATION
  1. Install rack-attack and configure rate limits (#4) - SECURITY
  2. Set up Stimulus controllers (#10) - REFACTOR
  3. Create /api namespace (#11) - REFACTOR
  4. Implement balance validation (#14) - REFACTOR
  1. Add Twilio webhook validation (#5) - SECURITY
  2. Add Stripe webhook validation (#6) - SECURITY
  3. Design token endpoint with caller identification (#7) - SECURITY
  4. Implement background webhook processing (#8) - SCALABILITY
  5. Tune database connection pool (#9) - SCALABILITY

  • 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
  • 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
  • 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

The Phase 0 & 1 implementation is solid but has critical gaps that must be addressed before Phase 2 & 3:

  1. Ledger architecture is the highest priority - the current mutable balance field will cause data integrity issues
  2. Security infrastructure (rate limiting, webhook validation) must be in place before exposing API endpoints
  3. Phone normalization should be installed NOW to prevent dirty data
  4. 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.