Notifications

The three-day notifier tells users their subscription billing date is coming: notifier-scheduler scans DynamoDB for SCHEDULED subscriptions due in the notification window and enqueues them; notifier-worker sends each user a three_day_notification Iterable event and conditionally submits an ACH prenote (Prenote Submission). Notifications go to Iterable only — the notifier emits no Segment or AppsFlyer events.

Payment-failure notifications (user_subscription_payment_failed) are emitted by the ach-handler Lambda, not the notifier — see ACH Callbacks.

Notifier pipeline

Schedule and Query

The pre_subscription_notifier EventBridge rule fires notifier-scheduler at cron(0 12 ? * * *) (12:00 UTC daily, prod-only) — see Schedules. The rule injects detail.status = "SCHEDULED", which becomes the queried billing_status.

The scheduler queries the billing_status-index GSI on billing-activity (GetNotificationSubscriptionPage) for subscriptions with billing_date in [today + 4 days, today + 5 days) at 00:00 UTC — a one-day window giving users roughly three days' notice. A manual invocation can set detail.date (YYYYMMDD) to move the window start for backfill runs, or a different detail.status.

Scheduler

Per invocation the scheduler:

  1. Pages the query up to PAGES_PER_INVOKE pages, batching each page onto the worker SQS queue as ThreeDayNotificationEvent messages.

  2. Stops when DynamoDB returns no continuation key.

  3. When the page budget is exhausted with work remaining, re-enqueues itself onto its own scheduler SQS queue (batch size 1) with the saved continuation key; the SQS-triggered invocation resumes from there.

An SQS batch containing more than one record fails the invocation (ErrTooManyRecords); a DynamoDB query error fails the message, which retries up to the queue’s maxReceiveCount of 5 before the DLQ.

Source: pkg/notification/notifier/scheduler.go.

Worker

The worker consumes the worker queue (batch size 10, ReportBatchItemFailures, max concurrency 5). Per subscription it:

  1. Skips subscriptions whose updated_event is PENDING_CANCELLATION (not a failure).

  2. Fetches the user; skips users whose status is not ACTIVE. In the test environment, skips any user whose email is not an integration-test address.

  3. Builds the notification: account mask from the txn-service main account (Plaid mask, not auth account numbers), tier defaulting to LITE when unset.

  4. Sends the three_day_notification Iterable track event keyed by the user’s email — contract in Events.

  5. Runs the prenote path (Prenote Submission); prenote errors never fail the record.

Parse, decode, user-service, and Iterable errors are returned as batch-item failures and retried by SQS. The exception is an Iterable 4xx other than 429 (e.g. an invalid email address): retrying cannot succeed, so the worker logs a warning with Iterable’s response body and does not fail the record, and no prenote is submitted for it.

Source: pkg/notification/notifier/worker.go.

Prenote Submission

After the notification, the worker submits a zero-dollar ACH prenote to validate the user’s bank account ahead of collection, gated in order:

  1. The user has a good Plaid item: a primary_account_id and a non-empty accounts list via txn-service.

  2. Users joined less than 30 days ago submit immediately; older users must also have a good recent payment — an ACH debit from JPM or USIO with status COMPLETED completed within the last 40 days (pinless card debits and RTP credits do not qualify).

The provider is selected by the subscriptions.payments.provider flag: usio submits a USIO prenote (usio_account: "subscription"); any other value submits a JPM prenote. A non-201 response counts as a failed submission.

Each outcome emits the subscriptions.notifier.prenotes Datadog metric (submitted:true|false, submitted_reason:, provider:jpm|usio) — a metric name, not a feature flag.

Source: pkg/notification/notifier/worker.go (prenoteUser, submitPrenote).