Kulcare Docs
DeveloperAuthentication DocumentationNotifications

Dev Notification Outbox

Overview

A development-only tool for previewing outbound notifications locally, at http://localhost:4000/dev/notifications.

Testing notification flows used to be awkward and channel-specific: SMS routed a Slack copy but still hit the real provider, email went out via real SES, and WhatsApp was simply disabled. The outbox is one place to see what a flow would have sent — across SMS, email, and WhatsApp — without sending anything real.

Use it whenever you're building or debugging a flow that sends a notification (signup/OTP, appointment reminders, referrals, invites, password reset, etc.).

Captured: SMS, email, WhatsApp. Not captured: push and in-app notifications — they are zero-cost and run exactly as in production.

How to use

  1. Restart the Rails server after pulling (this feature is config-level — routes, an initializer, and the mailer delivery_method — none of which hot-reload).
  2. Trigger a flow that sends a notification.
  3. Open http://localhost:4000/dev/notifications — hit it directly on the backend port (:4000), not through the frontend /api proxy.
    • Filter by channel (SMS / Email / WhatsApp).
    • Clear all empties the outbox.
    • http://localhost:4000/dev/notifications.json returns the raw captures.

Notes:

  • Email also opens in letter_opener (a browser tab) in addition to appearing in the outbox.
  • WhatsApp only appears when a flow actually calls WhatsappService — Wati has no dev sandbox, so those sends are captured-and-skipped.

Behavior by environment

ChannelDevelopmentStaging / Production
SMScaptured; real send suppresseddelivered normally (not captured)
Emailcaptured; delivered via letter_opener (no SES)delivered via SES (not captured)
WhatsAppcaptured; send skipped (Wati has no dev sandbox)delivered (not captured)
Push / in-apprun as productionrun as production

The outbox is development only — see Security below.

Technical Architecture

Capture happens at the low-level channel services, not the newer Dispatcher, because ~200 legacy sms/* / whatsapp/* templates call the services directly and most notifications fire from Sidekiq jobs. The services are the only seam that catches 100% of a channel's traffic.

  • Shared::Notifications::DevOutbox (domains/shared/services/notifications/dev_outbox.rb) — the store. record / all / clear, all gated on enabled? (Rails.env.development?). Backed by a capped Redis list ($redis, key dev:notification_outbox, cap 500) so the web process and Sidekiq workers share one outbox. Best-effort: a Redis error is logged and swallowed, never breaking a send.
  • SmsService.call — records to the outbox, then in development returns success before the real provider (MSG91/Twilio). DevOutbox.record no-ops outside development, so staging/production fall through and deliver.
  • WhatsappService.call — records into the outbox inside the existing development short-circuit.
  • Email — an anonymous Mail interceptor registered in config/initializers/setup_email.rb (development only) records every outgoing message. Delivery still happens via letter_opener (see development.rb).
  • Shared::Notifications::DevOutboxHtml — renders the viewer page; every message-derived value is HTML-escaped via ERB::Util.html_escape.
  • Shared::DevNotificationsController (shared/controllers) + the /dev/notifications routes — the viewer, mounted only in development.

Security

The viewer is unauthenticated and renders message bodies (which contain OTPs and PHI), so it is locked to local development by four independent gates:

  1. DevOutbox.enabled? is Rails.env.development? — record/all/clear all no-op otherwise.
  2. The /dev/notifications routes are mounted only inside if Rails.env.development?.
  3. The Mail interceptor is registered only in development.
  4. The controller's before_action returns 404 unless Rails.env.development? && request.local? — i.e. loopback only.

Caveat: the loopback gate (request.local?) means that if you run the backend behind a Docker port-forward or reverse proxy where the client IP is not 127.0.0.1/::1 (e.g. a Docker bridge gateway), the viewer returns 404. Native localhost access (the default) works. If your setup needs it, relax require_local_development! to an explicit IP allowlist.

Configuration

No configuration required — it's on automatically in development. Constants live in DevOutbox: REDIS_KEY (dev:notification_outbox) and MAX_ENTRIES (500, newest-first).

Email preview requires config.action_mailer.delivery_method = :letter_opener in config/environments/development.rb (the letter_opener gem is in the Gemfile's :development group).

Side effects (development only)

Because the SMS path short-circuits before delivery, in development SMS NotificationLog rows are not written and dedup is skipped. This is dev-only and has no effect on staging/production.

Testing

domains/shared/test/services/notifications/dev_outbox_test.rb covers the DevOutbox store (record/all/filter/no-op/clear), the SMS and WhatsApp capture seams (stubbing Rails.env.development?), and the viewer's HTML-escaping guarantee (a <script>-laden body renders escaped). enabled? is stubbed true in tests since it is false in the test environment by design.

On this page