Operations & platform
Notification Preferences
Generated from docs/plan/core/notification-preferences/PLAN.md in the AstroBaaS repository. Nothing described below is implemented — it is the written plan for it.
Customers get overwhelmed by marketing emails and mark them as spam. This feature gives customers control over which notification types they receive, reducing unsubscribe/spam rates.
The problem
My customers get too many emails and block my domain. I need to let them opt out of specific notification types (marketing, order updates, review requests) while keeping critical emails (password reset, orders).
What it does
- Notification category: marketing, order-updates, review-requests, account-alerts; each category has a frequency (immediately, daily-digest, weekly, never)
- Preference storage: per customer, JSON object with category → frequency mapping, stored with customer entity
- Preference UI: customer preference page (public) or account settings page (authenticated), toggle each category on/off or select frequency
- Email headers: add ‘Unsubscribe’ link and List-Unsubscribe header to all emails; link goes to preference page
- Consent default: critical emails (orders, password reset) are mandatory; marketing defaults to ‘on’ but customer can opt out immediately
- Preference honor: before sending any email, check customer preferences; if category is ‘never’, skip send; if ‘daily-digest’, queue and batch
- Bulk operations: admin can export all customer preferences, import from file (e.g., consent list from third-party provider)
- API endpoints: GET/PATCH customer preferences
What it deliberately does NOT do
Each boundary carries its reason. A boundary without a reason gets crossed by the next person who reads this.
- Preference per email type (e.g., separate opt-out for each product) — granularity is per category, not per message
- Preference history (who opted out when) — not tracked; only current preference state
- SMS/push notification preferences — only email preferences are managed here
- Automatic preference reset on new email signup — preferences are persistent; re-subscribing requires explicit opt-in
Data model
New field on Customer entity: notification_preferences (JSON). Example: {marketing: ‘never’, order_updates: ‘immediately’, review_requests: ‘weekly’, account_alerts: ‘immediately’}. No new table; just a JSON column. Schema migration: add field with default value {marketing: ‘immediately’, order_updates: ‘immediately’, review_requests: ‘never’, account_alerts: ‘immediately’}.
API
- GET /api/customers/:id/notification-preferences — fetch preferences (customer or admin)
- PATCH /api/customers/:id/notification-preferences — update preferences (customer or admin)
- GET /api/customers/by-email/:email/notification-preferences?token=unsubscribe_token — fetch preferences via public link (no auth)
- POST /api/notification-preferences/batch-import — bulk import preferences from CSV (admin only)
Every route added here must also appear in src/pages/openapi.json.ts — a test
fails the build if it does not.
Admin
Customer preferences section: per-customer view with toggles for each notification category (marketing, order updates, review requests, account alerts). Frequency selector: immediately/daily/weekly/never. Admin can override customer preference (rare case: customer calls and asks to be unsubscribed from all email). Bulk import: upload CSV with email + category + frequency, import all at once.
The seam — why this is core
Core owns the preference model, storage, and REST API. The seam is: core provides the interface and honest preference controller (what you store is what you honor). If a customer sets marketing=‘never’, marketing email is not sent (simple check before send). Payment/refund emails (core) are always sent. Marketing emails (operator-written) must check preferences before render.
Core owns the interface + honest preference controller; customer consent is infrastructure, not a support commitment or credential.
Dependencies
- Customer entity with JSON preference field (already shipped in core)
- Email sending system (already shipping in core; must check preferences before send)
- Public URL routing (unsubscribe link must be public and not require login)
Acceptance checks
Each of these must be able to fail. Before claiming this is done, break the code deliberately and watch each one go red.
- A customer can access their notification preferences page (authenticated) and see toggle for each category
- A customer can set marketing=‘never’; the next marketing email is not sent
- A customer who has never set preferences sees defaults: marketing=on, order_updates=on, reviews=off, account_alerts=on
- An unsubscribe link in an email (List-Unsubscribe header) points to a public page where customer can confirm and opt out
- If a customer opts out, they are unsubscribed immediately; future emails in that category are skipped
- A password reset email is sent regardless of preferences (critical email is always sent)
- Admin can view and override a customer’s preferences in the admin UI
- A bulk import file (email, category, frequency per row) is processed correctly; preferences are updated for all rows
Risks
Respecting preferences must happen in the email-sending code, not just in data model. If an operator writes marketing email code and doesn’t check preferences, the feature is useless — code review required. Unsubscribe links must not be easily faked; use a signed token in the URL (HMAC of customer ID + timestamp). Preference export can include PII (customer name, email); exports must be restricted to admin and securely shared. If an email is sent despite preference=‘never’, merchant faces complaint and FTC violation (CAN-SPAM Act) — this is critical.
Commercial context
| Suggested price | free (core) |
| Rival anchor | Shopify: included; Magento: included |
The anchor is what the nearest equivalent charges on Shopify or Magento today. It is context for a pricing decision, not the decision.
Generated from the commerce plan. See docs/COMMERCE-PLAN.md for the full
catalogue and ../../AI-GUIDE.md for how to work on this repository.