Skip to content

Design: SendGrid batch email migration for workshop invitations#2591

Closed
mroderick wants to merge 1 commit into
codebar:masterfrom
mroderick:feature/sendgrid-batch-email-design
Closed

Design: SendGrid batch email migration for workshop invitations#2591
mroderick wants to merge 1 commit into
codebar:masterfrom
mroderick:feature/sendgrid-batch-email-design

Conversation

@mroderick
Copy link
Copy Markdown
Collaborator

@mroderick mroderick commented Apr 24, 2026

Summary

Design document for migrating workshop invitation emails from ActionMailer/SMTP to SendGrid's v3 batch API with dynamic templates.

Problem

Bulk workshop invitations currently exceed the 9-minute DelayedJob timeout for large chapters, causing jobs to be killed mid-send.

Solution

  • New parallel delivery path using SendGrid v3 mail/send API (up to 1,000 recipients per call)
  • Dynamic templates authored in SendGrid UI, referenced by template ID
  • Chapter-by-chapter opt-in via temporary sendgrid_migration_configs table
  • Admin/beta whitelist via existing Rolify :sendgrid_beta role
  • Idempotency guard via sendgrid_deliveries table prevents duplicate sends on retry
  • Structured logging with correlation IDs for observability
  • ActionMailer path remains untouched as fallback

Documents

  • docs/superpowers/specs/2026-04-24-sendgrid-batch-email-design.md — design spec

SendGrid Documentation

Scope

This PR contains only design documentation. Implementation will follow in a separate PR.

@mroderick mroderick force-pushed the feature/sendgrid-batch-email-design branch from 633584e to 7305631 Compare April 24, 2026 19:13
@mroderick mroderick force-pushed the feature/sendgrid-batch-email-design branch from 7305631 to e29200f Compare April 24, 2026 19:18

## 2. Data Model

### `sendgrid_migration_configs` (temporary)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

So I would create templates on Sendgrid and reference them, e.g. one for students and one for coaches. No chapter specific templates.

|---------------|----------|--------------|-------------------------|
| `id` | serial | PK | |
| `workshop_id` | integer | FK, not null | |
| `member_id` | integer | FK, not null | |
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not sure this is needed, as SendGrid will keep track of this for us.

.find_by(chapter: workshop.chapter, email_type: "invite_student")
```

### `sendgrid_deliveries` (temporary)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This table is not temporary.

| `member_id` | integer | FK, not null | |
| `email_type` | string | not null | |
| `sg_batch_id` | string | | SendGrid `x-message-id` |
| `sent_at` | datetime | not null | |
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Standardize filenames on created_at vs. domain specifics.

| `sg_batch_id` | string | | SendGrid `x-message-id` |
| `sent_at` | datetime | not null | |

**Index:** `workshop_id + member_id + email_type` (unique)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Drop member_id


**Index:** `workshop_id + member_id + email_type` (unique)

### Whitelist: Rolify role
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Allowlist


log_response(response, batch, batch_number, Time.current - start_time)
response
rescue Net::OpenTimeout, Net::ReadTimeout => e
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Consider catching/rescue more broad error, not sure what else there is.


**Search:**
```bash
heroku logs --app codebar | grep "correlation_id=abc-123-def"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I am assuming we should log sendgrid's batch ID instead.

def initialize(workshop, email_type)
@workshop = workshop
@email_type = email_type
@correlation_id = SecureRandom.uuid
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maybe instead of creating a unique ID here, we can re-use the Sendgrid batch ID for tracking. Also double-check if this actually needs to be injected back into the email customisations.

- Records `sendgrid_deliveries` on 202
- Is safe to retry

Use WebMock to stub SendGrid API.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@mroderick
Copy link
Copy Markdown
Collaborator Author

Thank you for the feedback @till. I think I need to go back to the drawing board on this one, to make better use of the API like we discussed in our call.

@mroderick mroderick closed this Apr 25, 2026
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