Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.self-host.example
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ USE_HTTPS=false
AWS_SES_REGION=us-east-1
AWS_SES_ACCESS_KEY_ID=
AWS_SES_SECRET_ACCESS_KEY=
# Exact SNS topics allowed to call /webhooks/sns. Include a separate inbound
# topic too, if used, as a comma-separated ARN.
SNS_TOPIC_ARNS=arn:aws:sns:us-east-1:123456789012:plunk-ses-events

# Configuration sets for email tracking
# SES_CONFIGURATION_SET: Default configuration with open/click tracking enabled
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ jobs:
AWS_SES_REGION=us-east-1
AWS_SES_ACCESS_KEY_ID=mock
AWS_SES_SECRET_ACCESS_KEY=mock
SNS_TOPIC_ARNS=arn:aws:sns:us-east-1:123456789012:plunk-ses-events
SES_CONFIGURATION_SET=test
SES_CONFIGURATION_SET_NO_TRACKING=test-no-tracking
EOF
Expand Down Expand Up @@ -195,6 +196,7 @@ jobs:
AWS_SES_REGION=us-east-1
AWS_SES_ACCESS_KEY_ID=mock
AWS_SES_SECRET_ACCESS_KEY=mock
SNS_TOPIC_ARNS=arn:aws:sns:us-east-1:123456789012:plunk-ses-events
SES_CONFIGURATION_SET=test
SES_CONFIGURATION_SET_NO_TRACKING=test
EOF
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,8 @@ Required for builds and deployment (see turbo.json and .env.example):
optional)
- S3-compatible Storage (Minio): `S3_ENDPOINT`, `S3_ACCESS_KEY_ID`, `S3_ACCESS_KEY_SECRET`, `S3_BUCKET`,
`S3_PUBLIC_URL`, `S3_FORCE_PATH_STYLE`
- AWS SES: `AWS_SES_REGION`, `AWS_SES_ACCESS_KEY_ID`, `AWS_SES_SECRET_ACCESS_KEY`, `SES_CONFIGURATION_SET`,
`SES_CONFIGURATION_SET_NO_TRACKING`
- AWS SES: `AWS_SES_REGION`, `AWS_SES_ACCESS_KEY_ID`, `AWS_SES_SECRET_ACCESS_KEY`, `SNS_TOPIC_ARNS`,
`SES_CONFIGURATION_SET`, `SES_CONFIGURATION_SET_NO_TRACKING`
- OAuth (optional): `GITHUB_OAUTH_CLIENT`, `GITHUB_OAUTH_SECRET`, `GOOGLE_OAUTH_CLIENT`, `GOOGLE_OAUTH_SECRET`
- Stripe (optional): `STRIPE_SK`, `STRIPE_WEBHOOK_SECRET`, `STRIPE_PRICE_ONBOARDING`, `STRIPE_PRICE_EMAIL_USAGE`,
`STRIPE_METER_EVENT_NAME`
Expand Down
2 changes: 2 additions & 0 deletions apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ S3_FORCE_PATH_STYLE=true
AWS_SES_REGION=eu-north-1
AWS_SES_ACCESS_KEY_ID=
AWS_SES_SECRET_ACCESS_KEY=
# Exact SNS topics allowed to call /webhooks/sns (comma-separated when using more than one)
SNS_TOPIC_ARNS=arn:aws:sns:eu-north-1:123456789012:plunk-ses-events

# Configuration sets for email tracking
SES_CONFIGURATION_SET=plunk-configuration-set # Default: with open/click tracking
Expand Down
25 changes: 14 additions & 11 deletions apps/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,20 +523,23 @@ void prisma.$connect().then(async () => {

signale.info('[BACKGROUND-JOB] API request cleanup scheduled (BullMQ repeatable job, runs daily at 3 AM)');

// Set up repeatable job for expired idempotency key cleanup (BullMQ)
// Runs hourly: expiry is what makes a key reusable, so the sweep should track the TTL
await idempotencyKeyCleanupQueue.add(
// Set up repeatable durable-state maintenance (BullMQ). Event dispatch uses a
// five-minute grace window, so a minutely sweep retries failed ingestion in
// under six minutes during normal operation without racing live requests.
// v0.14.0 registered this job hourly through BullMQ's legacy repeat API.
// Remove that exact entry before moving the stable scheduler to one minute.
await idempotencyKeyCleanupQueue.removeRepeatable(
'cleanup-expired-keys',
{},
{
repeat: {
pattern: '0 * * * *', // Hourly, on the hour
},
jobId: 'idempotency-key-cleanup-repeatable', // Fixed ID to prevent duplicates
},
{pattern: '0 * * * *'},
'idempotency-key-cleanup-repeatable',
);
await idempotencyKeyCleanupQueue.upsertJobScheduler(
'idempotency-key-cleanup-repeatable',
{pattern: '* * * * *'},
{name: 'cleanup-expired-keys', data: {}},
);

signale.info('[BACKGROUND-JOB] Idempotency key cleanup scheduled (BullMQ repeatable job, runs hourly)');
signale.info('[BACKGROUND-JOB] Durable-state maintenance scheduled (BullMQ repeatable job, runs every minute)');

// Set up repeatable job for email body cleanup (BullMQ)
// Run daily at 4 AM, offset from the API request cleanup so the two don't overlap
Expand Down
13 changes: 13 additions & 0 deletions apps/api/src/app/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,19 @@ export const AWS_SES_REGION = validateEnv('AWS_SES_REGION');
export const AWS_SES_ACCESS_KEY_ID = validateEnv('AWS_SES_ACCESS_KEY_ID');
export const AWS_SES_SECRET_ACCESS_KEY = validateEnv('AWS_SES_SECRET_ACCESS_KEY');

// Exact SNS topic ARNs authorized to deliver SES events to /webhooks/sns.
// Multiple topics support deployments that separate outbound and inbound SES.
const snsTopicArns = validateEnv('SNS_TOPIC_ARNS')
.split(',')
.map(topicArn => topicArn.trim())
.filter(Boolean);

if (snsTopicArns.length === 0) {
throw new Error('SNS_TOPIC_ARNS must contain at least one topic ARN');
}

export const SNS_TOPIC_ARNS: ReadonlySet<string> = new Set(snsTopicArns);

// Custom MAIL FROM subdomain used to construct `<subdomain>.<your-domain>`
// when a domain is added. Defaults to `plunk`. Override when `plunk.<your-domain>`
// is already used for something else (e.g. a CDN), since the MAIL FROM hostname
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/controllers/Actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export class Actions {
* Response:
* - success: boolean
* - data: object with contact ID, event ID, and timestamp
* - 200 once the event is stored; workflow dispatch may finish synchronously
* or through Plunk's bounded internal reconciliation sweep
*
* Example:
* {
Expand Down
Loading