Booper: Technical spec
TODO
Overview
TODO: overview of components: mobile app, API, database, web dashboard
Mobile app:
- Expo, React Native, TypeScript, (SQLite?)
API:
- Elixir, Phoenix, Oban (workers), Hammer (rate limiter), API key auth for apps, JWT auth for dashboard
Data storage:
- Redis (rate limiting), Postgres (db)
Web dashboard:
- NextJS, Vercel, Tailwind,
API endpoints
TODO: API endpoints (public vs restricted vs admin dashboard)
Channels
GET /api/channels- Retrieve a list of subscribed channels by user
- Retrieve a list of subscribed channels by app user, if the user has access to the app
- Used in: mobile app
- Request:
device_token(string, required) - the user device's push tokenapp_id(string, optional) - the ID of the app
- Response:
channels- a list of channels
- Errors:
400- missingdevice_token- If user does not have access to the app, or the device token is invalid, this will respond with an empty list of channels
POST /api/channels- Unlikely to be used, channels should just be auto-created when a user subscribes if it doesn't exist yet
- Could be used in mobile app, dashboard
POST /api/channels/[name]/subscribePOST /api/subscriptions- Subscribe a user to a channel by name or ID (only works for public channels?)
- Request:
device_token(string, required) - the user device's push tokenname(string) - the name of the public channelchannel_id(string|number) - the ID of the public channel
POST /api/channels/[name]/unsubscribeDELETE /api/subscriptions- Unsubscribe a user to a channel by name or ID (only works for public channels?)
- Request:
device_token(string, required) - the user device's push tokenname(string) - the name of the public channelchannel_id(string|number) - the ID of the public channel
GET /api/channels/[name]/subscribersGET /api/subscriptions- Retrieve list of subscribers by channel
- Dashboard only
Notifications
GET /api/notifications- Retrieve notifications by channel
POST /api/notifications- Create a new notification for a given channel
Apps
GET /api/apps- List owned apps in dashboard
POST /api/apps- Create a new app
- Used in: dashboard only
GET /api/apps/[id]- Retrieve app details
- Used in: dashboard (app owner), mobile app (subscribed users)
Access codes
POST /api/access_codes- Generates a temporary access code for a private app
- Request:
Authorization: Bearer {API key}- the API key for the appchannels[]- list of channel names or IDs that will be granted accessidentifier(optional) - how the device should be identified (e.g. user ID, email, etc)expires_at(optional) - expiration timestamp
- Response:
access_code: the 6 digit access codeexpires_at: the expiration timestamp (defaults to 5 minutes?)
POST /api/access_codes/[code]- Submits an access code
- Request:
code- the access codedevice_token- the device push token identifier
- Response:
- maybe respond with channels that are now accessible?
Public API
(Requires nothing.)
POST /api/notificationsPOST /api/notify- Send a notification to a public channel
POST /api/devicesPOST /api/identitiesPOST /api/identify- Register a device token
Admin API
(Requires app admin API key.)
POST /api/notificationsPOST /api/notify- Send a push notification to your app
POST /api/access_codes- Generate an access code for your app
Mobile app API
(Requires device push token, or device unique identifier.)
GET /api/channels- List user channels
GET /api/channels/[id]- Retrieve channel details
GET /api/apps- List user apps
GET /api/apps/[id]- Retrieve app details
GET /api/notifications- List notifications by channel (or app channel)
POST /api/access_codes/[code]- Submit access code for app
Dashboard API
(Requires JWT.)
GET /api/apps- List admin apps
POST /api/apps- Create admin app
GET /api/apps/[id]- Retrieve app details
DELETE /api/apps/[id]- Delete app
GET /api/channels- List admin app channels
POST /api/channels- Create admin app channel
GET /api/channels/[id]- Retrieve app channel details
DELETE /api/channels/[id]- Delete app channel
GET /api/subscriptions- List admin app subscriptions
POST /api/subscriptions- Create admin app subscription
GET /api/subscriptions/[id]- Retrieve app subscription user details
DELETE /api/subscriptions/[id]- Delete app subscription
Components
TODO: what are the main technical components?
The lifecycle of a notification
TODO: talk about chunking, sending to worker, logging receipts, tracking successes/failures, retrying failures based on error message, etc
For public channel:
- Hit
/api/notifyendpoint - Rate limit by IP or whatever identifier is available
- 10 requests per second per IP
- 10 requests per second per channel
- Create message record in db
- Enqueue message in Redis, or Genserver, or something that can handle heavy load if necessary
- Maybe try this if Oban queue is full?
- Enqueue message in Oban for push notification
- Execute push notification worker for each notification
- Worker should be idempotent
- Send notification to push notification server (Expo, Apple, FCM)
- Store receipts in db
- Check receipts every 5-15 mins until success/failure case
- If notification fails on first try
- Check error message, log error, trigger alert if necessary
- Retry if possible
- If notification fails after checking receipt
- Update receipt result in db with error
- Check error message, log error, trigger alert if necessary
- Retry if possible
- If notification succeeds, clear from queue
- Update receipt result in db with success
- Partial success? How should that be handled?
- Archive messages after 7 days
- Hard delete messages after 30 days
For private app, same as above, except for...
- 10 requests per second per app
- 100 notifications per day
- Pro plan:
- 1000 notifications per day
- archive messages after 30 days
- hard delete after 100 days
- Business plan:
- priority queue
- unlimited notifications
- never archive/delete messages
Realtime updates
TODO: talk about polling vs websockets, dealing with app focus events, app backgrounded, tab switching not rerendering, etc
Offline mode
TODO: look into sqlite vs AsyncStorage, how to handle syncing local data with server (I guess this is easier when client is basically readonly), how local data is structured (sql? serialized by id?)