adjust doc for full SMS flow
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# 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.
|
||||
Problem: Users have no way to receive daily SMS notifications for 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.
|
||||
- Allow a daily digest of new activity from the `alertSearch` endpoint.
|
||||
- Allow users to choose their preferred time of day.
|
||||
- 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.
|
||||
@@ -21,17 +21,19 @@ This should be a **separate service** rather than code inside endorser-ch, for s
|
||||
|
||||
### Access to Endorser-ch Partner Endpoints
|
||||
|
||||
The SMS service needs to call `alertSearch` and potentially other partner endpoints on behalf of users. Options considered:
|
||||
The SMS service needs to call `alertSearch` and potentially other partner endpoints on behalf of users.
|
||||
|
||||
**User-issued long-lived JWT** 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 authentication 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).
|
||||
|
||||
Options considered:
|
||||
|
||||
| Approach | Pros | Cons |
|
||||
|----------|------|------|
|
||||
| **User-issued long-lived JWT** | No new auth mechanism; user controls scope | JWTs expire; user must re-authorize periodically |
|
||||
| **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
|
||||
@@ -63,7 +65,7 @@ The SMS service needs to call `alertSearch` and potentially other partner endpoi
|
||||
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.
|
||||
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).
|
||||
|
||||
- **DID reassignment (lost key scenario)**:
|
||||
A user who loses their DID and creates a new one can re-register the same phone number:
|
||||
@@ -76,7 +78,7 @@ The SMS service needs to call `alertSearch` and potentially other partner endpoi
|
||||
|
||||
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:
|
||||
Schema to support this:
|
||||
```sql
|
||||
CREATE TABLE sms_user (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -104,11 +106,10 @@ The SMS service needs to call `alertSearch` and potentially other partner endpoi
|
||||
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'
|
||||
notificationType TEXT NOT NULL, -- '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,
|
||||
@@ -139,12 +140,11 @@ The SMS service needs to call `alertSearch` and potentially other partner endpoi
|
||||
- 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:
|
||||
2. 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`.
|
||||
3. 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
|
||||
@@ -174,39 +174,36 @@ All endpoints require a signed JWT validated against endorser-ch (see section 5
|
||||
|
||||
### 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.
|
||||
The SMS settings live in the Account screen's notification section, next to the existing on-device New Activity notification. The SMS channel is gated behind an explicit enablement step, so the UI has three stacked controls:
|
||||
|
||||
- **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:
|
||||
1. **Enable Text Messages** (master toggle). While no phone number is registered, the SMS row is labeled "Enable Text Messages". Turning it on prompts for a phone number, then advances to a screen for the verification code texted to that number. Only after the code is confirmed does the toggle turn on and the per-notification controls below become available. Once a number is registered the row is labeled "SMS Text Message" and shows the number.
|
||||
2. **New Activity Text** (per-notification toggle). Available only while text messages are enabled. Turning it on opens the daily-time modal (the same time picker as the on-device channel) so the user chooses when their new-activity text arrives each day. Turning it off just removes that one notification; the phone number and SMS enablement are untouched.
|
||||
3. **Forget Phone Number** (trash link, shown with the registered number). Removes the number entirely: Enable Text Messages turns off and New Activity Text becomes unavailable until a number is verified again. This maps to `DELETE /api/sms/register` on the service.
|
||||
|
||||
```
|
||||
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 Notification
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ 📱 In-App Notification [ON] │
|
||||
│ Time: 6:00 PM │
|
||||
├──────────────────────────────────────────────┤
|
||||
│ 💬 Enable Text Messages [OFF] │ <- phone + code prompts on enable
|
||||
└──────────────────────────────────────────────┘
|
||||
|
||||
New Activity Digest
|
||||
┌─────────────────────────────────────┐
|
||||
│ 📱 On-Device Notification [ON] │
|
||||
│ Time: 6:00 PM │
|
||||
├─────────────────────────────────────┤
|
||||
│ 💬 SMS Notification [OFF] │
|
||||
│ Time: 6:00 PM │
|
||||
└─────────────────────────────────────┘
|
||||
...and once a number is verified:
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ 💬 SMS Text Message [ON] │
|
||||
│ Texting: +1 555 123 4567 │
|
||||
│ 🗑 Forget Phone Number │
|
||||
│ ├─ New Activity Text [OFF] │ <- opens the daily-time modal
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
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).
|
||||
- In-app and SMS are two **delivery channels** for the same logical notification, each configured independently.
|
||||
- The phone number is a channel-level credential, verified once and reused by every SMS notification type (New Activity Text now; event-triggered types later).
|
||||
- Removing the number is an explicit, separate act -- not a side effect of toggling a notification off.
|
||||
|
||||
- **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.
|
||||
- **Master toggle off vs. Forget**: turning Enable Text Messages off pauses the SMS channel but keeps the verified number, so re-enabling skips re-verification. Forget Phone Number is the destructive path that requires verifying again.
|
||||
|
||||
- **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).
|
||||
|
||||
@@ -220,9 +217,11 @@ Minimal changes to the endorser-ch server:
|
||||
|
||||
## Deployment
|
||||
|
||||
This may be folded into the notification-wakeup-service.
|
||||
|
||||
- **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.
|
||||
- **Deployment**: Same server as endorser-ch or a small adjacent container.
|
||||
- **Environment variables**:
|
||||
- `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`, `TWILIO_PHONE_NUMBER`
|
||||
- `ENDORSER_API_URL` (e.g., `https://partner-api.endorser.ch`)
|
||||
@@ -245,6 +244,14 @@ Minimal changes to the endorser-ch server:
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 0: UI Mock-up in Time Safari (no back end)
|
||||
|
||||
Work lives on the `notify-api-sms` branch of crowd-funder-for-time-pwa. All state is component-local fake data (deliberately not persisted -- it only needs to survive a walk-through on an emulator) and every back-end round-trip is stubbed, so each modal can be demoed without notify-api calls (any 6-digit code passes verification).
|
||||
|
||||
- [x] "Enable Text Messages" master toggle on the Account screen that runs the phone-entry -> verification-code dialogs (`SmsVerificationDialog`)
|
||||
- [x] "New Activity Text" toggle, available only while text messages are enabled, that opens the existing daily-time modal
|
||||
- [x] "Forget Phone Number" control that clears the number, turns off the master toggle, and hides New Activity Text
|
||||
|
||||
### 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)
|
||||
@@ -255,12 +262,10 @@ Minimal changes to the endorser-ch server:
|
||||
### 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
|
||||
- [ ] Wire the Phase 0 mock-up UI to the real endpoints (register, verify, preferences, delete/forget), replacing the component-local fake data
|
||||
- [ ] Add send history view
|
||||
|
||||
### Phase 4: Hardening
|
||||
@@ -301,9 +306,7 @@ Beyond the scheduled daily notifications, there are future needs for **on-demand
|
||||
| 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. |
|
||||
| Others below | | | |
|
||||
|
||||
**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.
|
||||
|
||||
@@ -334,15 +337,26 @@ This requires extending the notification preferences to support event-triggered
|
||||
-- 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.
|
||||
-- sendTimeUtc is 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
|
||||
## Decided 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?
|
||||
2. **Message length**: **Decided: truncate to single SMS.** 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?
|
||||
4. **User cap**: **Decided: no user limit.** Should we limit the number of users who can register for SMS during an initial rollout period?
|
||||
5. **Alternative to Twilio**: **Decided: start with the simplest SMS service.** Would a self-hosted solution (e.g., connecting to a GSM modem or using Matrix/Signal bridges) be preferable for cost or privacy reasons?
|
||||
|
||||
### Rejected Options
|
||||
|
||||
#### Implementation
|
||||
|
||||
| Approach | How it works | Pros | Cons |
|
||||
|----------|-------------|------|------|
|
||||
| **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. |
|
||||
|
||||
|
||||
+4
-2
@@ -1,6 +1,8 @@
|
||||
|
||||
tasks:
|
||||
|
||||
- Fix SMS
|
||||
|
||||
- outreach
|
||||
|
||||
- funding :
|
||||
@@ -15,7 +17,7 @@ tasks:
|
||||
- interview & match?
|
||||
- talk?
|
||||
- give-away items?
|
||||
- first event due:2026-02-13
|
||||
- first event due:2027-02-11
|
||||
|
||||
- 1000 P2P mesh network implementation id:mesh-network :
|
||||
details : tech/progress/PROJECT-mesh-network.md
|
||||
@@ -53,7 +55,7 @@ tasks:
|
||||
|
||||
- identify & help local activist influencers
|
||||
|
||||
- accounting report (board meeting?) due:2026-06-01 :
|
||||
- accounting report (board meeting?) due:2027-01-01 :
|
||||
- include a total accounting of money, eg time as money, for investment-minded people
|
||||
|
||||
- Alpha Assembly :
|
||||
|
||||
Reference in New Issue
Block a user