Skip to main content

Text campaigns

Text campaigns

Product name: Text campaigns (Fundraising hub). Channel: SMS/MMS via Telnyx (send-sms). Route / navigation: Path /fundraising, Zustand fundraisingTool = text-campaigns. See 00-FUNDRAISING-HUB.md.

Primary component: src/features/fundraising/components/SMSCampaigns.tsx

Bulk recipient semantics (Apr 27, 2026): Bulk text campaigns send only to normalized, unique phone numbers with explicit `sms_consent.opted_in = true`. send-sms normalizes/dedupes recipients before consent checks, monthly SMS limit checks, MintBucks balance checks, and the Telnyx loop. Duplicate CRM/list rows for the same physical phone get at most one text and one charge. Missing consent records are skipped for bulk/campaign sends; single-recipient individual/test sends may still proceed without a consent row unless the number is explicitly opted out.

MintBucks / monthly limit basis: Text campaign prechecks and deductions use the eligible recipient count after phone dedupe and consent filtering, not the raw phone-capable audience size. If 500 records have phone numbers but only 50 unique numbers are explicitly opted in, the SMS monthly limit and MintBucks balance check use 50.

Recipients: Shared audience controls with email — src/features/fundraising/components/shared/FundraisingCampaignRecipientsFields.tsx (mode: 'sms'). Mailing lists use the same marketing_lists data as email (09-MAILING-LISTS.md).

Mailing-list SMS preview (Apr 9, 2026): SMSCampaigns.tsx now computes a mailing-list sendable preview before send using the same donor / volunteer / contact soft links used at send time. The compose UI shows:

  • a loading state while recipient stats are resolving
  • sendable vs total members for SMS
  • warnings for members who are still unlinked / missing phone numbers
  • warnings for recipients opted out in sms_consent

Draft/save/send actions are blocked while mailing-list SMS stats are still loading so campaigns do not persist misleading recipient counts.

The composer's dropdown counts and send button use normalized, unique, explicitly opted-in phone numbers for bulk recipient modes. This keeps the UI aligned with the backend's bulk consent policy.

Mapping-ready mailing lists: The effect that loads fetchFundraisingLists (all lists; inactive rows appear disabled in compose) now `await waitForMappingInitialized()` (with cancel on dependency change) so lists are not skipped when the page mounts before isMappingInitialized() is true.

Parent org default sender: In parent/admin workspace view, SMS compose defaults to the parent organization. The “From organization” dropdown is an optional override to send from a specific child fund.

Features (high level)

  • Tabs: Campaigns and Compose only (no in-app Text Templates UI).
  • Compose links: Optional Add Links checkbox. When enabled, selecting an Event, VideoBlast, or giving page (donor page) auto-inserts one managed line per type into the message body (`RSVP:`, `Watch:`, `Donate:`). Changing the dropdown replaces that line; unchecking Add Links strips all three managed lines and clears the selectors. Each destination is shortened via `create-sms-short-link` before insertion; if shortening fails, the line is not added and the UI shows an error toast.
  • Character limits / STOP suffix: SMS_LIMITS and SMS_STOP_SUFFIX live in src/lib/smsTemplates.ts (limits/helpers only; not a template picker).
  • Scheduling + TCPA / FCC window: src/lib/smsTimeWindow.ts (aligned with _shared/sms-time-window.ts on the edge).
  • Persistence + send: src/lib/db/sms-campaigns.ts, src/lib/db/fundraising-sms.ts → infrastructure in `documentation/backend/SMS-EMAIL-INFRASTRUCTURE.md`.

SMS short links

  • Table: sms_short_links (migration supabase/migrations/20260511120000_sms_short_links.sql). RLS on; no client policies — browser uses the edge layer.
  • Create: Edge function `create-sms-short-link`; client wrapper `createSmsShortLink()` in src/lib/db/sms-short-links.ts.
  • Resolve: `redirect-qr` at `https://q.alignmint.app/{short_code}` tries `qr_codes` first, then `sms_short_links`. See EDGE-FUNCTIONS.md § *Short links*.

SMS templates (sms_templates table)

The `sms_templates` table remains in the database for legacy rows and RLS/tests, but Text Campaigns no longer reads or edits it (no template tab, CRUD, or hooks in the app compose path).

Outcome semantics and troubleshooting

send-sms returns a normalized outcome contract so frontend behavior is deterministic:

  • request_completed — whether the request made it through the send pipeline (auth/guards/provider attempt path).
  • outcome_status — one of:
  • full_success (sent > 0 and failed === 0)
  • partial_success (sent > 0 and failed > 0)
  • full_failure (sent === 0)
  • trace_id — correlates frontend result, edge-function logs, and provider triage.
  • diagnostics — compact provider-facing metadata (attempted send, first provider error, and sampled message IDs).

Campaign status expectations

  • Campaign send flow persists status: "sending" before invoking send-sms.
  • Edge function updates final campaign status/counts (sent or failed) server-side.
  • Frontend must invalidate smsCampaigns after send completion so list transitions immediately from sending to final status.

Non-delivery triage checklist

1. Capture trace_id from the UI failure path. 2. Check Supabase function logs for [send-sms] entries matching the same trace. 3. Verify whether diagnostics.telnyx_attempted is true. 4. If attempted, use diagnostics.first_provider_error_* and message IDs to inspect Telnyx. 5. If not attempted, fix upstream guards/eligibility (consent/suppression/tier/time-window).

Mailing-list recipient resolution notes

  • Mailing-list text sends only include list members whose source_type / source_id resolves to a donor, volunteer, or contact row with a phone number.
  • manual rows do not receive texts unless they were soft-linked back to CRM from the Mailing Lists editor.
  • Consent checks normalize recipient phone numbers to the same E.164-like format used by send-sms, so preview and send behavior stay aligned even when CRM phone formatting varies.
  • Mailing-list sendable counts dedupe by normalized phone. If the same opted-in phone appears more than once through list membership overlap, it is counted and sent once.

Related docs


Synced from IFMmvp-Frontend documentation: pages/fundraising/11-TEXT-CAMPAIGNS.md

Ready to Get Started?

See how Alignmint can simplify your nonprofit's operations. Schedule a free demo with our team and we'll walk you through everything.

Questions? Email us at steven@getalignmint.org

Ready to get started?Start Plus Trial