Skip to content

feat: prepare Loops migration and shared email library - #2272

Merged
richiemcilroy merged 15 commits into
mainfrom
codex/loops-migration
Sep 11, 2026
Merged

feat: prepare Loops migration and shared email library#2272
richiemcilroy merged 15 commits into
mainfrom
codex/loops-migration

Conversation

@richiemcilroy

@richiemcilroy richiemcilroy commented Sep 10, 2026

Copy link
Copy Markdown
Member

Bento's external signup sequence can send upgrade emails to existing customers and people joining another organization. This prepares separate Loops journeys for independent free users, customers, teammates and former customers. Paid/license evidence, downstream audience checks and persistent teammate classification stop sales follow-ups after an invite or purchase. Teammates receive workspace help only, including existing accounts accepting a new invite after cutover.

Normal completed Cap signups create or reuse a Stripe customer; the native Loops integration imports customer creation and updates. Cap enriches that contact with current audience, greeting, plan and recording milestones through a durable loops_sync_jobs queue and an authenticated every-minute cron. SSO uses a held direct-create fallback. Transactional queue writes, expiring leases, revisions and retry backoff preserve work through interruptions and concurrent changes. Changed/deleted identities are held for reconciliation.

There is no marketing_contacts table or separate consent-capture step. Loops owns global and list preferences; routine enrichment never resubscribes or changes list membership. A missing list entry holds enrollment. Historical imports remain held, missing previously synced contacts are not recreated, and existing opt-outs/suppressions take precedence. The unshipped consent-table migration was replaced using Drizzle tooling with the origin field and delivery queue migrations; apply schema before deploying code.

The emails/ library owns all 12 marketing messages, shared branding, sender defaults, customer variations, audiences and flow definitions. Its generated catalogue shows four journeys, two campaign templates, timing, branches, copy and existing Resend send sources. The custom MJML uses the actual Cap logo artwork, complete greetings (Hey, without a name), consistent spacing and one compact company/address/unsubscribe footer. Existing Resend application mail is unchanged.

Loops' beta API cannot read custom MJML. The legacy provisioner refuses writes to these managed emails, and emails:check-loops --structure-only verifies graphs, filters, draft state and custom format. Changed content requires browser review and reconciliation to local source. Catalogue excerpts strip only allowlisted formatting tags and escape remaining markup; malformed and full-script regressions cover the CodeQL finding.

An independent GitHub Actions check reads an authenticated Cap queue-health endpoint. Unhealthy or unavailable sync holds all four downstream journey guards, and recovery requires an explicit healthy resume. Newly queued changes bypass an older attempt's backoff, and unchanged successful refreshes clear old failures. The check is disabled pending deployment/configuration; GitHub delays and Loops API outages can prevent a timely hold. Scheduled campaigns require separate manual health checks and cancellation.

Validation:

  • 65 email-script/profile/lifecycle/delivery-check tests and 83 existing SSO/subscription regression tests pass. The exact email CI type-check command, database/web TypeScript checks, scoped format/lint checks and catalogue freshness checks pass locally.
  • Nine isolated PlanetScale tests cover rollback, overlapping claims, expired leases, newer work surviving older success/failure acknowledgements, retry timing, recovery, queue health and the due index. Repository-wide db:push hit an unrelated storage prefix-index quoting bug; exact generated Loops SQL was applied and verified on the empty branch. The additive generated schema is now applied to production through PlanetScale deploy request Can't select which screen to record #94 and independently verified; the new queue was empty before code rollout.
  • Synthetic database records passed through the actual worker and real Loops API, including a simulated 429/retry, SSO fallback, teammate transition and unsubscribe preservation. Six additional owned-contact checks cover classification, imported holds and list removal. No emails were sent by these checks; contacts ended held and globally unsubscribed.
  • Native Stripe live-customer creation and updates were verified on one owned contact, including names, mailing-list assignment, same-contact updates and global unsubscribe preservation. Both saved event mappings were checked after reload. A separate recipient Preference Center mailing-list opt-out survived a native Stripe update to the same owned contact while global subscription remained on; the contact was globally unsubscribed again after verification. No new emails were sent.
  • Earlier isolated workflow tests delivered exactly 16 expected emails to owned inboxes across 18 scenarios; purchase, invite and opt-out transitions stopped later messages, milestones skipped reminders and replay produced no extras. QA workflows are paused. Final layout previews passed SPF, DKIM and DMARC; received logo bytes matched the canonical local asset.
  • A real API safety test held all four production downstream guards, verified simulated recovery did not release them, and sent no emails. Final API verification confirms all four production workflows and both campaigns remain drafts, with every journey held. No actual customer received a test email.

Native Stripe contact syncing is enabled. Production schema is applied. Vercel is configured for owned-address test mode, enrollment is disabled, and the code remains undeployed pending merge. GitHub requires one approving review. GitHub watchdog credentials are prepared; its enable flag remains absent. Before activation: reconcile the final Bento suppression delta, deploy in owned-address test mode, prove the deployed signup/purchase/invite/opt-out paths and sync latency, enable and test the independent delivery check and its notifications, and check Resend/Bento overlap. Loops uses the last synced properties during an outage; capVerifiedAt is not an automatic expiry rule. Activation and Bento shutdown require the explicit cutover described in scripts/loops/README.md.

Private exports, credentials, recipient data and detailed test receipts remain outside Git. Cross-client dark-mode rendering and future inbox placement have not been exhaustively established.

RetriggerConfidence Score: 5/5

The PR appears safe to merge based on the reviewed changes, with no outstanding actionable findings.

Summary

  • Adds queued, revision-aware Loops profile synchronization with leases, retries, and health reporting.
  • Adds audience classification and persistent teammate/customer lifecycle handling.
  • Adds authenticated cron and independent delivery-safety workflow integration.
  • Adds shared MJML email definitions, generated documentation, provisioning tools, and regression coverage.
  • Preserves missing, removed, and unsubscribed mailing-list states during historical migration.

Reviews (8) · Last reviewed commit: "fix: preserve absent Loops mailing list ..."

@richiemcilroy richiemcilroy changed the title feat: prepare Loops lifecycle migration feat: prepare Loops migration and shared email library Sep 11, 2026
Comment thread scripts/emails/catalog.ts Fixed
Comment thread scripts/emails/catalog.ts Fixed
@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

Comment thread packages/database/loops/worker.ts Outdated
@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

Comment thread emails/marketing/teammate-handoff.ts Outdated
@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

@superagent-security superagent-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Superagent found 2 security concern(s).

Comment thread .github/workflows/loops-safety.yml Outdated
Comment thread .github/workflows/loops-safety.yml Outdated
@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

@superagent-security superagent-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Superagent found 1 security concern(s).

Comment thread packages/database/loops/sync-policy.ts Outdated
@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

@richiemcilroy
richiemcilroy merged commit 28a20cd into main Sep 11, 2026
30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants