Notification preferences: from settings request to migration spec

This case shows how a broad "let users manage notifications" request becomes a bounded packet with consent rules, existing-user migration, duplicate-send evidence, and AI coding guardrails.

RiskConsent, duplicate sends, user trust
BoundaryEmail and push only
EvidenceMigration, opt-out, delivery checks

The weak request

Original ticket

Add notification preferences.

Users should be able to turn emails and push notifications on or off.
Make sure we do not spam people.

Spec-first rewrite

Feature: Notification preference migration
Owner: Lifecycle Messaging
Status: Ready for review

Context:
- Existing users have email_opt_out in users.
- Push tokens live in device_tokens.
- Marketing emails already respect unsubscribe records.

Goal:
- Add one preference model for product email and push.
- Preserve all existing opt-outs during migration.
- Prevent duplicate sends while old and new readers coexist.

Non-goals:
- No SMS preferences.
- No marketing email redesign.
- No bulk admin editor.
- No new notification history page.

Decisions the spec must force

Default state

Existing opt-outs stay off. Users without an explicit opt-out keep their current delivery behavior until they change settings.

Dual-read window

Old and new preference readers may coexist for one release, but delivery must choose one source of truth for each notification.

Channel ownership

This change owns product email and push only. Marketing unsubscribe rules and SMS consent stay outside the diff.

The packet reviewers actually need

spec.md

Preference categories:
- product_updates_email
- product_updates_push
- billing_alert_email
- billing_alert_push

Rules:
- Opt-out records migrate as disabled preferences.
- Billing alerts cannot be disabled in this release.
- Push disabled means no new push jobs are enqueued.
- Email disabled means delivery worker suppresses send.

tasks.md

- [ ] Add preference table and backfill script.
- [ ] Add read API for settings UI.
- [ ] Add update API with audit fields.
- [ ] Update email worker suppression.
- [ ] Update push enqueue suppression.
- [ ] Add dual-read safety tests.

acceptance-criteria.md

- Given an existing email opt-out
  When preferences are migrated
  Then product_updates_email is disabled.

- Given a user disables push
  When an event triggers a product update
  Then no push job is enqueued for that user.

evidence.md

Required:
- backfill dry-run count
- opt-out preservation test
- duplicate-send regression test
- worker suppression test
- settings UI screenshot
- rollback plan for preference reader flag

Reviewer walkthrough

The first review question is whether the spec preserves current consent. A settings UI is easy to generate, but the dangerous work is migration: existing opt-outs, device tokens, marketing unsubscribe records, and product alert rules all look similar until the spec gives each one an owner.

The second review question is duplicate delivery. During a migration, old and new readers often run side by side. If both enqueue messages, users may receive two notifications for the same event. A good packet names the dual-read window, the source of truth, and the regression test that proves only one job is created.

The third review question is rollback. Turning off the UI does not undo migrated preference rows. The packet should name the flag that controls the new reader and the query that verifies migrated opt-outs remain preserved if the release is paused.

Scope check

Reject SMS, marketing redesign, notification history, and admin tooling unless they have their own spec. They are tempting but separate products.

Evidence check

Require a dry-run count, a migrated opt-out fixture, a duplicate-send regression test, and one UI screenshot that matches API state.

Release check

Release should happen behind a reader flag. The stop signal is any duplicate product notification or mismatch between preference API and worker decision.

AI coding guardrail

Implement the notification preference migration only.

Allowed areas:
- preference schema and migration
- settings API read/update
- email worker suppression
- push enqueue suppression
- tests and fixtures for the acceptance criteria

Do not add:
- SMS consent
- notification history
- admin bulk editor
- marketing email redesign
- unrelated settings page refactor

Every changed file must map to one task and one acceptance criterion.

This guardrail is deliberately strict. Notification work invites adjacent improvements: new channels, a nicer settings page, admin overrides, campaign history, and analytics dashboards. Those may be valuable, but adding them inside this migration makes review weaker and consent behavior harder to audit.

How to adapt this case

Use this pattern whenever a preference, consent, or settings change affects delivery behavior. Replace the channel names with your own, then write the existing source of truth before the new model. If the current system has unsubscribe rows, device tokens, profile flags, team-level settings, or regional consent rules, list each one explicitly. Do not let the new UI become the only thing the spec describes.

For smaller teams, the packet can stay short. The essential parts are still the same: current behavior, migration rule, dual-read rule, non-goals, and evidence. If any of those are missing, the AI implementation may look polished while changing consent semantics in a way nobody reviewed.

For larger teams, add one more section: ownership. Product messaging, lifecycle marketing, billing alerts, and mobile push often have different owners. A spec that fails to name them may pass engineering review while violating another team's delivery policy.

Anti-patterns to reject

UI-only preference logic

The worker must enforce preferences. A settings toggle that is not checked during delivery only creates false confidence.

Silent default changes

Changing default delivery for existing users is a product and consent decision. It must be visible in the spec.

Backfill without counts

A migration without dry-run counts and mismatch checks cannot prove it preserved current opt-outs.

What to watch after launch

Notification preference migrations often fail after the UI looks correct, because delivery workers and queued jobs are where user trust is actually protected.

Duplicate jobs

During one release window, watch whether old and new readers enqueue the same event. A duplicate job is a stop signal.

Opt-out preservation

Sample users who opted out before migration and verify the preference row, API response, and worker decision all agree.

Use this pattern before changing notification settings

Start with the packet generator, then paste the migration rules and non-goals into the implementation prompt before any AI coding session.

Editorial note

This case is a teaching scenario based on common notification migration failures: consent drift, duplicate sends, missing backfill evidence, and UI-only preference checks.