more SMS plan refinement
This commit is contained in:
348
progress/tech/PROJECT-sms-notifications.md
Normal file
348
progress/tech/PROJECT-sms-notifications.md
Normal file
@@ -0,0 +1,348 @@
|
||||
# SMS Notification Service
|
||||
|
||||
Problem: Users have no way to receive daily SMS notifications for reminders or new activity from the endorser-ch system. The daily-notification-plugin handles on-device notifications via Capacitor, but there is no server-side SMS channel for users who want text messages.
|
||||
|
||||
## Goal & Value Proposition
|
||||
|
||||
- Allow users to register and verify a cell phone number.
|
||||
- Allow users to choose notification type(s): a custom daily reminder message, and/or a daily digest of new activity from the `alertSearch` endpoint.
|
||||
- Allow users to choose their preferred time of day for each notification.
|
||||
- Run a periodic background service that wakes at the appropriate times and sends SMS messages.
|
||||
|
||||
This gives users a reliable, device-independent notification channel that works even when the app is not installed or active, complementing the on-device daily-notification-plugin.
|
||||
|
||||
## Architecture Decision: Separate Service
|
||||
|
||||
This should be a **separate service** rather than code inside endorser-ch, for several reasons:
|
||||
|
||||
1. **Separation of concerns** -- endorser-ch is a claim/attestation server; SMS delivery is operationally distinct (external API keys, retry queues, delivery tracking).
|
||||
2. **Independent scaling and deployment** -- SMS delivery has different failure modes (carrier throttling, vendor outages) and should not block or destabilize the claim server.
|
||||
3. **Security boundary** -- the SMS service stores phone numbers (PII); keeping them in a separate database limits blast radius.
|
||||
|
||||
### Access to Endorser-ch Partner Endpoints
|
||||
|
||||
The SMS service needs to call `alertSearch` and potentially other partner endpoints on behalf of users. Options considered:
|
||||
|
||||
| Approach | Pros | Cons |
|
||||
|----------|------|------|
|
||||
| **Shared secret header** | Simple to implement; endorser-ch adds one middleware check | Another secret to rotate; coarse-grained access |
|
||||
| **Service DID with delegation** | Fits existing DID auth model; fine-grained | More complex; requires DID key management in the SMS service |
|
||||
| **User-issued long-lived JWT** | No new auth mechanism; user controls scope | JWTs expire; user must re-authorize periodically |
|
||||
| **Internal network + API key** | Simple if co-located | Assumes network topology; not portable |
|
||||
|
||||
**Recommended: User-issued long-lived JWT** for the initial version. When a user registers for SMS notifications, the Time Safari app generates a long-lived JWT and sends it to the SMS service, which stores it alongside the user's registration. The SMS service then uses this stored JWT directly when calling endorser-ch partner endpoints (e.g., `alertSearch`) on the user's behalf. This requires no new auth mechanism on endorser-ch and gives each user control over their own scope. The tradeoff is that JWTs eventually expire, so the app must refresh them periodically (e.g., on app open, the app checks the SMS service for token expiry and re-issues if needed).
|
||||
|
||||
## Components
|
||||
|
||||
### 1. Phone Number Storage & Verification
|
||||
|
||||
- New table in the SMS service database (not in partner.db). The phone number is the natural key -- a given phone number has at most one registration, and the `issuerDid` tracks who currently owns it:
|
||||
```sql
|
||||
CREATE TABLE sms_user (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
phoneNumber TEXT UNIQUE NOT NULL, -- E.164 format; one registration per phone
|
||||
issuerDid TEXT NOT NULL, -- current owner's DID
|
||||
verified BOOLEAN DEFAULT FALSE,
|
||||
verificationCode TEXT, -- 6-digit code, hashed
|
||||
verificationExpiry DATETIME,
|
||||
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
- Verification flow:
|
||||
1. User submits phone number via the Time Safari app (which calls `POST /api/sms/register` with the user's session JWT for authentication).
|
||||
2. SMS service validates the number is US-only (`+1` prefix) for free-tier users. International numbers are rejected unless the user has a paid tier (see Phase 5).
|
||||
3. SMS service sends a 6-digit code via SMS. The code is valid for **15 minutes**.
|
||||
4. User enters the code in the app; the app sends it to `POST /api/sms/verify`.
|
||||
5. On match, `verified` is set to TRUE. The user's phone is now registered but no notifications are active yet.
|
||||
6. Requesting a new code while one is still valid **overrides** the previous code (the old code becomes invalid).
|
||||
7. Rate limits: max 3 verification codes per hour, max 5 attempts per code. After exceeding the code request limit, a **1-hour cooldown** before new codes can be requested.
|
||||
|
||||
- **JWT lifecycle** (separate from registration):
|
||||
The long-lived JWT is **not** sent at registration time. It is only needed when notifications are enabled:
|
||||
1. When the user enables any notification type, the app generates a long-lived JWT and sends it to `PUT /api/sms/jwt`. The SMS service stores it and records its expiry.
|
||||
2. The SMS service will not activate notifications without a valid stored JWT.
|
||||
3. The app is responsible for refreshing the stored JWT before it expires (e.g., every 90 days). On app open, the app checks `GET /api/sms/preferences` which includes JWT expiry status, and calls `PUT /api/sms/jwt` with a fresh token if needed.
|
||||
4. If the stored JWT expires without being refreshed, the scheduler skips that user's `alert_search` notifications (which require the JWT for endorser-ch calls). `reminder` notifications can still be sent since they don't call endorser-ch.
|
||||
|
||||
- **DID reassignment (lost key scenario)**:
|
||||
A user who loses their DID and creates a new one can re-register the same phone number:
|
||||
1. User calls `POST /api/sms/register` with their new DID (from the new JWT) and their existing phone number.
|
||||
2. The SMS service sees the phone number already exists with a different DID. It does **not** change the DID yet -- it sets `verified = FALSE`, stores the new DID in a `pendingDid` column, and sends a new verification code (subject to the same 15-minute expiry and rate limits).
|
||||
3. The user completes verification via `POST /api/sms/verify`.
|
||||
4. On successful verification, the service updates `issuerDid` to the new DID, clears `pendingDid`, and sets `verified = TRUE`. All existing notification preferences are preserved and now operate under the new DID.
|
||||
5. The old DID can no longer access or modify this registration.
|
||||
6. The old stored JWT (if any) is cleared. The user must send a new long-lived JWT via `PUT /api/sms/jwt` before `alert_search` notifications will resume.
|
||||
|
||||
This ensures that phone ownership is always proven before a DID change takes effect, preventing someone from hijacking another user's notifications by claiming their number.
|
||||
|
||||
Updated schema to support this:
|
||||
```sql
|
||||
CREATE TABLE sms_user (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
phoneNumber TEXT UNIQUE NOT NULL, -- E.164 format; one registration per phone
|
||||
issuerDid TEXT NOT NULL, -- current verified owner's DID
|
||||
pendingDid TEXT, -- new DID awaiting verification (NULL if none)
|
||||
storedJwt TEXT, -- long-lived JWT for calling endorser-ch on user's behalf
|
||||
jwtExpiresAt DATETIME, -- when the stored JWT expires; app refreshes before this
|
||||
verified BOOLEAN DEFAULT FALSE,
|
||||
verificationCode TEXT, -- 6-digit code, hashed
|
||||
verificationExpiry DATETIME, -- 15 minutes from code generation
|
||||
verificationAttempts INTEGER DEFAULT 0, -- attempts against current code (max 5)
|
||||
codesRequestedThisHour INTEGER DEFAULT 0, -- codes sent in current hour window (max 3)
|
||||
cooldownUntil DATETIME, -- if set, no new codes until this time (1-hour cooldown)
|
||||
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### 2. Notification Preferences
|
||||
|
||||
- **Phone verification is required before any notification can be enabled.** The `PUT /api/sms/preferences` endpoint rejects requests to set `enabled = TRUE` if the user's phone is not verified. Additionally, enabling any notification that requires endorser-ch data (i.e., `alert_search`) requires a valid stored JWT; the endpoint rejects the request if no JWT is stored or if it has expired.
|
||||
- Table for user notification choices:
|
||||
```sql
|
||||
CREATE TABLE sms_notification_pref (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
userId INTEGER NOT NULL REFERENCES sms_user(id),
|
||||
notificationType TEXT NOT NULL, -- 'reminder' or 'alert_search'
|
||||
enabled BOOLEAN DEFAULT TRUE,
|
||||
sendTimeUtc TEXT NOT NULL, -- HH:MM in UTC
|
||||
timezoneOffset INTEGER, -- minutes from UTC, for display
|
||||
reminderMessage TEXT, -- user's custom message (for 'reminder' type)
|
||||
alertSearchParams TEXT, -- JSON: location bounds, plan IDs, etc.
|
||||
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(userId, notificationType)
|
||||
);
|
||||
```
|
||||
|
||||
### 3. SMS Sending
|
||||
|
||||
- Use **Twilio** as the SMS provider (well-documented, good international support, reasonable pricing).
|
||||
- Alternative: Amazon SNS (cheaper for high volume) or Vonage.
|
||||
- Wrap the provider behind an interface so it can be swapped later.
|
||||
- Store send history for debugging and rate-limiting:
|
||||
```sql
|
||||
CREATE TABLE sms_send_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
userId INTEGER NOT NULL REFERENCES sms_user(id),
|
||||
notificationType TEXT NOT NULL,
|
||||
sentAt DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
status TEXT NOT NULL, -- 'sent', 'failed', 'delivered'
|
||||
providerMessageId TEXT,
|
||||
errorMessage TEXT
|
||||
);
|
||||
```
|
||||
|
||||
### 4. Periodic Scheduler
|
||||
|
||||
- A cron-like scheduler (e.g., `node-cron` or system cron) that runs **every 15 minutes**.
|
||||
- Each run:
|
||||
1. Query all enabled preferences where `sendTimeUtc` falls within the current 15-minute window.
|
||||
2. For `reminder` type: send the user's custom `reminderMessage`.
|
||||
3. For `alert_search` type:
|
||||
a. Call `GET /api/partner/alertSearch` on the endorser-ch server using the user's stored JWT, passing the user's stored `alertSearchParams` and an `afterDate` of the last successful check.
|
||||
b. If new activity is found, compose a summary SMS and send it.
|
||||
c. Update the last-checked timestamp.
|
||||
4. Log each send attempt to `sms_send_log`.
|
||||
- Idempotency: track the last send time per preference to avoid duplicate sends on scheduler restarts.
|
||||
|
||||
### 5. Authentication: JWT Validation via Endorser-ch
|
||||
|
||||
The SMS service does **not** implement its own JWT verification. Instead, every incoming request is authenticated by forwarding the caller's JWT to the endorser-ch server for validation:
|
||||
|
||||
1. Client sends request with `Authorization: Bearer <JWT>` header.
|
||||
2. SMS service forwards the JWT to the endorser-ch `GET /api/v2/report/rateLimits` endpoint, which validates the JWT and returns the caller's DID along with rate-limit data.
|
||||
3. SMS service uses the returned DID as the authenticated identity for the request.
|
||||
4. If endorser-ch returns an error or 401, the SMS service rejects the request.
|
||||
|
||||
This keeps DID/JWT verification logic in one place and avoids duplicating the DID resolver stack (ethr-did-resolver, did-peer, etc.) in the SMS service. The endorser-ch call result can be cached briefly (e.g., 60 seconds keyed by JWT hash) to avoid hammering the endpoint on rapid sequential requests.
|
||||
|
||||
### 6. API Endpoints (SMS Service)
|
||||
|
||||
All endpoints require a signed JWT validated against endorser-ch (see section 5 above).
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| POST | `/api/sms/register` | Submit phone number, triggers verification SMS |
|
||||
| POST | `/api/sms/verify` | Submit verification code; reassigns DID if phone was previously registered |
|
||||
| DELETE | `/api/sms/register` | Remove phone number and all preferences |
|
||||
| PUT | `/api/sms/jwt` | Refresh the stored JWT used for endorser-ch calls |
|
||||
| GET | `/api/sms/preferences` | Get current notification preferences |
|
||||
| PUT | `/api/sms/preferences` | Update notification preferences |
|
||||
| GET | `/api/sms/history` | Get recent send history |
|
||||
|
||||
### 7. Time Safari App Integration
|
||||
|
||||
The SMS notification settings are structurally parallel to the existing on-device daily notification settings (from the daily-notification-plugin / Capacitor local notifications). Users can enable one, the other, or both. The UI should make this relationship obvious.
|
||||
|
||||
- **UI approach: side-by-side notification channels.** Rather than burying SMS settings in a separate screen, present both channels together in the notification settings area. For each notification type (daily reminder, activity digest), show the two delivery channels as peer options:
|
||||
|
||||
```
|
||||
Daily Reminder
|
||||
┌─────────────────────────────────────┐
|
||||
│ 📱 On-Device Notification [ON] │
|
||||
│ Time: 8:00 AM │
|
||||
│ Message: "What can I give today?"│
|
||||
├─────────────────────────────────────┤
|
||||
│ 💬 SMS Notification [OFF] │
|
||||
│ Time: 8:00 AM │
|
||||
│ Message: "What can I give today?"│
|
||||
│ Phone: +1 (555) 123-4567 ✓ │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
New Activity Digest
|
||||
┌─────────────────────────────────────┐
|
||||
│ 📱 On-Device Notification [ON] │
|
||||
│ Time: 6:00 PM │
|
||||
├─────────────────────────────────────┤
|
||||
│ 💬 SMS Notification [OFF] │
|
||||
│ Time: 6:00 PM │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
This layout makes it immediately clear that:
|
||||
- These are two **delivery channels** for the same logical notification types.
|
||||
- Each channel has its own toggle and can be configured independently.
|
||||
- SMS requires a verified phone number (shown inline with verification status).
|
||||
|
||||
- **Phone number registration** is surfaced at the top of the notification settings screen (or inline the first time the user tries to enable an SMS toggle). Once verified, the number is shown with a checkmark across all SMS rows.
|
||||
|
||||
- **Shared defaults**: When a user enables SMS for a notification type they already have configured on-device, pre-fill the SMS time and message from the on-device settings as a convenience (user can change them independently).
|
||||
|
||||
- **JWT refresh**: Handled transparently. When the app opens the notification settings screen, it checks the SMS service for JWT expiry status and silently refreshes if needed. No user-facing UI for this.
|
||||
|
||||
## Endorser-ch Changes Required
|
||||
|
||||
Minimal changes to the endorser-ch server:
|
||||
|
||||
1. **No new endpoints required.** The SMS service uses the existing `GET /api/v2/report/rateLimits` endpoint to validate incoming JWTs and extract the caller's DID. For scheduled `alertSearch` calls, the SMS service uses the user's stored JWT directly, so no shared-secret middleware or service-to-service auth is needed on endorser-ch.
|
||||
|
||||
## Deployment
|
||||
|
||||
- **Runtime**: Node.js (consistent with endorser-ch ecosystem).
|
||||
- **Database**: SQLite (consistent with endorser-ch; simple deployment).
|
||||
- **Deployment**: Same server as endorser-ch or a small adjacent VM/container. If co-located, can use `localhost` for the endorser-ch API calls, simplifying network security.
|
||||
- **Environment variables**:
|
||||
- `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`, `TWILIO_PHONE_NUMBER`
|
||||
- `ENDORSER_API_URL` (e.g., `https://partner-api.endorser.ch`)
|
||||
- `SMS_DB_PATH`
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Phone numbers are PII: encrypt at rest if the deployment environment supports it.
|
||||
- Verification codes are hashed (bcrypt) before storage.
|
||||
- Rate-limit SMS sends per user (max 2 per day per notification type; max 5 verification codes per day).
|
||||
- Stored user JWTs must be encrypted at rest; they grant access to endorser-ch on the user's behalf.
|
||||
- The app must refresh stored JWTs before they expire; the SMS service should flag users whose JWTs are nearing expiry so the app can prompt a refresh.
|
||||
- Consider allowing users to pause/resume without deleting their number.
|
||||
|
||||
## Cost Estimate
|
||||
|
||||
- Twilio SMS: ~$0.0079/message (US), higher internationally.
|
||||
- At 100 users x 2 messages/day = 200 messages/day = ~$1.58/day = ~$47/month.
|
||||
- Verification messages add marginal cost (one-time per user).
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Core Infrastructure
|
||||
- [ ] Set up new Node.js service with SQLite database
|
||||
- [ ] Implement JWT-forwarding auth middleware (validate JWTs via endorser-ch `rateLimits` endpoint)
|
||||
- [ ] Implement phone registration and verification endpoints (including DID reassignment flow)
|
||||
- [ ] Implement stored JWT management (store user-issued JWTs, track expiry, refresh endpoint)
|
||||
- [ ] Integrate Twilio SDK for sending SMS
|
||||
|
||||
### Phase 2: Notification Preferences & Scheduler
|
||||
- [ ] Implement preference storage and API endpoints
|
||||
- [ ] Build the periodic scheduler (15-minute interval)
|
||||
- [ ] Implement `reminder` notification type (custom message send)
|
||||
- [ ] Implement `alert_search` notification type (fetch + summarize + send)
|
||||
|
||||
### Phase 3: App Integration
|
||||
- [ ] Add phone number registration UI to Time Safari settings
|
||||
- [ ] Add notification preference controls
|
||||
- [ ] Add send history view
|
||||
|
||||
### Phase 4: Hardening
|
||||
- [ ] Delivery status webhooks from Twilio (track delivered vs. failed)
|
||||
- [ ] Retry logic for failed sends
|
||||
- [ ] Admin dashboard for monitoring send rates and failures
|
||||
- [ ] Opt-out via SMS reply (STOP keyword handling)
|
||||
- [ ] Upgrade to service DID delegation auth (replacing stored user JWTs)
|
||||
|
||||
### Phase 5: Paid Tier -- Higher Limits & International SMS
|
||||
- [ ] Integrate a payment mechanism (e.g., Stripe, or crypto/gift-economy credit)
|
||||
- [ ] Add `sms_user` columns for payment status and tier (`free` / `paid`)
|
||||
- [ ] Enforce US-only phone numbers for free tier; allow international numbers for paid tier
|
||||
- [ ] Raise daily rate limits for paid users (e.g., free: 2 messages/day, paid: 10 messages/day)
|
||||
- [ ] Add payment status checks to the send path and preference validation
|
||||
- [ ] Time Safari UI for payment/upgrade flow
|
||||
|
||||
### Phase 6: Event-Triggered SMS from Partner Functions
|
||||
- [ ] Add `POST /api/sms/send` endpoint to SMS service (service-to-service auth)
|
||||
- [ ] Add per-event-type opt-in preferences (profile messages, meeting invites, etc.)
|
||||
- [ ] Integrate first trigger in endorser-ch (e.g., profile messaging)
|
||||
- [ ] Add event-type rate limiting per recipient
|
||||
- [ ] Extend Time Safari settings UI for event-type opt-in/opt-out
|
||||
|
||||
## Future Requirement: Event-Triggered SMS from Partner Functions
|
||||
|
||||
Beyond the scheduled daily notifications, there are future needs for **on-demand SMS messages triggered by partner functions inside endorser-ch**. These are not periodic -- they are fired in response to specific events.
|
||||
|
||||
### Example Scenarios
|
||||
|
||||
- **Profile messaging**: A user views a profile and wants to send a message to the profile owner. If the profile owner has opted in to SMS messages, the system sends them an SMS with the message content (or a link to view it).
|
||||
- **Meeting invitations**: An organizer creates a group onboarding meeting and wants to notify invited participants via SMS.
|
||||
- **Matching results**: After `groupOnboardMatch` runs, notify matched pairs via SMS.
|
||||
- **Claim confirmations**: Notify a user when someone confirms one of their claims.
|
||||
|
||||
### Implementation Options
|
||||
|
||||
| Approach | How it works | Pros | Cons |
|
||||
|----------|-------------|------|------|
|
||||
| **A. SMS service exposes a send API** | Endorser-ch calls `POST /api/sms/send` on the SMS service with the shared secret, target DID, and message body. The SMS service looks up the DID's verified phone number and sends the message. | Clean separation; endorser-ch never touches Twilio or phone numbers; SMS service owns all delivery logic, rate-limiting, and opt-out checks. | Adds a network hop; SMS service must be available for real-time sends. |
|
||||
| **B. Message queue between services** | Endorser-ch publishes a message to a queue (e.g., Redis, SQLite-based job queue, or a simple filesystem queue). The SMS service polls or subscribes and sends. | Decoupled; endorser-ch doesn't block on SMS delivery; resilient to SMS service downtime. | More infrastructure; adds latency; queue must be monitored. |
|
||||
| **C. Shared Twilio credentials** | Endorser-ch calls Twilio directly using the same credentials, and queries the SMS service database (or a shared table) for phone number lookup. | No inter-service call needed; lowest latency. | Defeats the purpose of separation; Twilio credentials and phone PII spread across services; harder to enforce rate limits and opt-out centrally. |
|
||||
| **D. Webhook/callback registration** | The SMS service registers a webhook URL with endorser-ch for specific event types. Endorser-ch fires the webhook when events occur, and the SMS service handles delivery. | Event-driven; endorser-ch doesn't need to know about SMS at all, just fires webhooks. | Endorser-ch needs a generic webhook/event system; more complex to build initially. |
|
||||
|
||||
**Recommended: Option A (SMS send API)** for initial implementation, with an eye toward **Option D (webhooks)** as the system matures. Option A is straightforward -- a single `POST /api/sms/send` endpoint on the SMS service with a service-to-service auth mechanism (e.g., a shared secret between the two services for this internal endpoint). The SMS service remains the sole owner of phone numbers, delivery logic, rate limits, and opt-out enforcement.
|
||||
|
||||
### Send API Design (Option A)
|
||||
|
||||
```
|
||||
POST /api/sms/send
|
||||
Authorization: X-Service-Secret: <shared secret>
|
||||
|
||||
{
|
||||
"recipientDid": "did:ethr:0x...",
|
||||
"message": "Someone wants to connect with you on Time Safari. Open the app to respond.",
|
||||
"senderDid": "did:ethr:0x...", // optional, for audit/rate-limiting
|
||||
"eventType": "profile_message" // for categorized opt-in/opt-out
|
||||
}
|
||||
```
|
||||
|
||||
- The SMS service checks that the recipient has a verified phone number and has opted in to messages of this `eventType`.
|
||||
- Rate limits apply per recipient (e.g., max 5 event-triggered messages per day).
|
||||
- The `sms_send_log` table records these sends alongside scheduled ones.
|
||||
- If the recipient has not registered or has opted out, the service returns a `404` or `204` (no message sent) so endorser-ch can fall back to other channels.
|
||||
|
||||
### Opt-in Granularity
|
||||
|
||||
This requires extending the notification preferences to support event-triggered message types:
|
||||
|
||||
```sql
|
||||
-- Additional rows in sms_notification_pref with eventType-based types:
|
||||
-- notificationType: 'event_profile_message', 'event_meeting_invite',
|
||||
-- 'event_match_result', 'event_claim_confirmation', etc.
|
||||
-- sendTimeUtc and reminderMessage are NULL for event-triggered types.
|
||||
```
|
||||
|
||||
Users should be able to opt in or out of each event type independently in the Time Safari settings.
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. ~~**International SMS**~~: **Decided: US-only for free tier.** International numbers will be supported in Phase 5 as part of the paid tier.
|
||||
2. **Message length**: AlertSearch results could be lengthy. Should we truncate to 160 chars (single SMS segment) or allow multi-segment messages?
|
||||
3. **Opt-out compliance**: US carriers require STOP/HELP keyword handling (TCPA compliance). Twilio handles this automatically, but we need to be aware of it.
|
||||
4. **User cap**: Should we limit the number of users who can register for SMS during an initial rollout period?
|
||||
5. **Alternative to Twilio**: Would a self-hosted solution (e.g., connecting to a GSM modem or using Matrix/Signal bridges) be preferable for cost or privacy reasons?
|
||||
Reference in New Issue
Block a user