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_LIMITSandSMS_STOP_SUFFIXlive insrc/lib/smsTemplates.ts(limits/helpers only; not a template picker). - Scheduling + TCPA / FCC window:
src/lib/smsTimeWindow.ts(aligned with_shared/sms-time-window.tson 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(migrationsupabase/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 > 0andfailed === 0)partial_success(sent > 0andfailed > 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 invokingsend-sms. - Edge function updates final campaign status/counts (
sentorfailed) server-side. - Frontend must invalidate
smsCampaignsafter send completion so list transitions immediately fromsendingto 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_idresolves to a donor, volunteer, or contact row with a phone number. manualrows 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
- 01-EMAIL-CAMPAIGNS.md — Historical changelog and patterns shared with email (Fundraising-scoped)
- 09-MAILING-LISTS.md — List source for “Mailing list” recipient mode
- EDGE-FUNCTIONS.md —
send-sms,create-sms-short-link,redirect-qr,telnyx-sms-webhook
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