Tax & compliance
Email Consent Manager
Generated from docs/plan/core/email-consent-manager/PLAN.md in the AstroBaaS repository. Nothing described below is implemented — it is the written plan for it.
Core-free compliance primitive that tracks explicit email consent per customer, enforces opt-in before any promotional sends, and maintains an audit log of when each customer consented and through which channel. Merchants send email only to customers who have positively opted in; CAN-SPAM (USA) and GDPR (EU) require documented consent, and both regulate double-opt-in flows and consent revocation.
The problem
Merchants send marketing emails to customers who never opted in. CAN-SPAM fines start at USD 43,280 per violation; GDPR fines can be EUR 20 million or 4% of global turnover. A customer unsubscribes but still receives promos because unsubscribe logic is not wired. There is no proof of consent when regulators audit.
What it does
- Customer record carries an email_consent field (enum: none | single_opt_in | double_opt_in | revoked) frozen at the date it was first set
- Checkout captures email-consent checkbox state and records it (single opt-in by default; double opt-in flow optional)
- Email consent audit log records every state change with timestamp, channel (signup form / checkout / admin / API), and IP address of consent event
- API endpoints to read, request, and revoke consent per customer; revocation is irreversible without admin intervention
- Admin UI shows each customer’s consent state, date of most recent consent, and full audit trail
- Email sending routes (transactional and promotional) check consent state before queueing; rejected sends are logged
- Settings toggle to enable/disable double-opt-in flow (sends confirmation email, customer must click link to confirm)
- Unsubscribe link in email footer auto-revokes consent on click (no confirmation needed per CAN-SPAM)
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.
- No ESP sync — the audit log stays in AstroBaaS; third-party email platforms keep their own lists (reason: ESP consent is a separate contract with the ESP, not AstroBaaS’s responsibility)
- No consent templates or template selection — email body is entirely the merchant’s responsibility (reason: compliance is the merchant’s legal obligation; AstroBaaS provides the enforcement mechanism only)
- No consent translation — all consent UI and emails are in the shop’s configured language; GDPR-required translations are the merchant’s job (reason: AstroBaaS does not provide legal translations)
Data model
Customer.email_consent (enum: none | single_opt_in | double_opt_in | revoked), Customer.email_consent_date (timestamp), Customer.email_consent_ip (text). New AuditLog entity: {customer_id, event: consent_changed, old_value, new_value, timestamp, ip, channel}. Migration required.
API
- GET /api/customers/:id/email-consent
- POST /api/customers/:id/email-consent/request (start double opt-in flow)
- POST /api/customers/:id/email-consent/confirm (complete double opt-in)
- POST /api/customers/:id/email-consent/revoke
- GET /api/email-consent/audit-log (paginated, filterable by customer/date)
Every route added here must also appear in src/pages/openapi.json.ts — a test
fails the build if it does not.
Admin
Customers table gains email-consent column (state + date). Customer detail page shows full audit log (who/when/channel/IP for every change). Admin can manually set consent state (locked with reason: ‘manually set by [admin email]’). Settings → Email shows double-opt-in toggle and confirmation email template editor.
The seam — why this is core
Core owns the consent tracking interface, audit log, and enforcement (no email goes out without checking). Core owns checkout capture. Paid modules (if any) that send email must call the core’s consent check before queueing. Email transport layer is agnostic to consent; the route that builds the send queue enforces the gate.
Core owns the interface + honest consent tracker; email compliance is infrastructure, not a support commitment or credential.
Dependencies
- Customer entity (core)
- Email sending pipeline (core)
- Audit logging framework (core)
- Settings (core)
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 transactional email (order confirmation) sends to a customer with email_consent=none without error (transactional is not consent-gated)
- A promotional email (newsletter) queued to a customer with email_consent=none is rejected with error ‘customer has not consented to marketing emails’
- A customer with email_consent=none can request double-opt-in, receives a confirmation email, and remains email_consent=none until they click the link
- Clicking the confirmation link sets email_consent=double_opt_in and records the event in the audit log
- Revoking consent via unsubscribe link sets email_consent=revoked and immediately stops promotional sends
- The audit log shows IP address and channel (checkout, form, admin) for the original consent event
- Admin can manually override consent state and the override is logged with the admin’s name
Risks
A backwards-incompatible migration that sets all existing customers to email_consent=none or single_opt_in without a merchant decision step leaves existing mailing lists blocked. Email routes that send to a consent field that does not exist on old records will throw. Checkout can double-record consent if the form is submitted twice; need idempotency. Audit log can become huge; need to index by customer_id and date for performance.
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.