Skip to content

Commit de9cfb3

Browse files
committed
Environment variables for disabling email queue
1 parent 5e20e0f commit de9cfb3

7 files changed

Lines changed: 45 additions & 23 deletions

File tree

.github/workflows/e2e-api-tests.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,6 @@ jobs:
135135
uses: JarvusInnovations/background-action@v1.0.7
136136
with:
137137
run: pnpm -C apps/backend run run-email-queue --log-order=stream &
138-
wait-on: |
139-
http://localhost:8102
140138
tail: true
141139
wait-for: 30s
142140
log-output-if: true

.github/workflows/e2e-custom-base-port-api-tests.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,6 @@ jobs:
129129
uses: JarvusInnovations/background-action@v1.0.7
130130
with:
131131
run: pnpm -C apps/backend run run-email-queue --log-order=stream &
132-
wait-on: |
133-
http://localhost:8102
134132
tail: true
135133
wait-for: 30s
136134
log-output-if: true

.github/workflows/e2e-source-of-truth-api-tests.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,6 @@ jobs:
135135
uses: JarvusInnovations/background-action@v1.0.7
136136
with:
137137
run: pnpm -C apps/backend run run-email-queue --log-order=stream &
138-
wait-on: |
139-
http://localhost:8102
140138
tail: true
141139
wait-for: 30s
142140
log-output-if: true

apps/backend/.env

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ STACK_EMAILABLE_API_KEY=# for Emailable email validation, see https://emailable.
4444

4545
STACK_DEFAULT_EMAIL_CAPACITY_PER_HOUR=# the number of emails a new project can send. Defaults to 200
4646

47+
# Email branching configuration
48+
# If you have multiple deployments of compute accessing the same DB or multiple copies of a DBs connected to compute (as
49+
# you would in preview/branching environments), you may want to either disable the auto-triggered email queue steps
50+
# (those that trigger whenever an email is sent, besides the cron job), or disable email sending as a whole.
51+
STACK_EMAIL_BRANCHING_DISABLE_QUEUE_AUTO_TRIGGER=# set to 'true' to disable the automatic triggering of the email queue step. the cron job must call /email-queue-step to run the queue step. Most useful on production domains where you know the cron job will run on the correct deployment and you don't need the auto-trigger (which may be on the wrong deployment)
52+
STACK_EMAIL_BRANCHING_DISABLE_QUEUE_SENDING=# set to 'true' to throw an error instead of sending emails in the email queue step. Most useful on development branches that have a copy of the production DB, but should not send any emails (as otherwise some emails could be sent twice)
53+
4754

4855
# Database
4956
# For local development: `docker run -it --rm -e POSTGRES_PASSWORD=password -p "8128:5432" postgres`
@@ -75,8 +82,7 @@ STACK_QSTASH_TOKEN=
7582
STACK_QSTASH_CURRENT_SIGNING_KEY=
7683
STACK_QSTASH_NEXT_SIGNING_KEY=
7784

78-
79-
# Misc, optional
85+
# Misc
8086
STACK_ACCESS_TOKEN_EXPIRATION_TIME=# enter the expiration time for the access token here. Optional, don't specify it for default value
8187
STACK_SETUP_ADMIN_GITHUB_ID=# enter the account ID of the admin user here, and after running the seed script they will be able to access the internal project in the Stack dashboard. Optional, don't specify it for default value
8288
OTEL_EXPORTER_OTLP_ENDPOINT=# enter the OpenTelemetry endpoint here. Optional, default is `http://localhost:8131`

apps/backend/src/lib/email-queue-step.tsx

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { withTraceSpan } from "@/utils/telemetry";
88
import { allPromisesAndWaitUntilEach } from "@/utils/vercel";
99
import { EmailOutbox, EmailOutboxSkippedReason, Prisma } from "@prisma/client";
1010
import { groupBy } from "@stackframe/stack-shared/dist/utils/arrays";
11+
import { getEnvBoolean } from "@stackframe/stack-shared/dist/utils/env";
1112
import { captureError, errorToNiceString, StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
1213
import { Json } from "@stackframe/stack-shared/dist/utils/json";
1314
import { filterUndefined } from "@stackframe/stack-shared/dist/utils/objects";
@@ -139,7 +140,7 @@ async function updateLastExecutionTime(): Promise<number> {
139140
-- Concurrent insert race: another worker just inserted, skip this run
140141
WHEN NOT EXISTS (SELECT 1 FROM result) THEN 0.0
141142
-- First run (inserted new row), use reasonable default delta
142-
WHEN (SELECT previous_timestamp FROM result) IS NULL THEN 60.0
143+
WHEN (SELECT previous_timestamp FROM result) IS NULL THEN 20.0
143144
-- Normal update case: compute actual delta
144145
ELSE EXTRACT(EPOCH FROM
145146
(SELECT new_timestamp FROM result) -
@@ -150,9 +151,14 @@ async function updateLastExecutionTime(): Promise<number> {
150151

151152
if (delta < 0) {
152153
// TODO: why does this happen, actually? investigate.
154+
console.warn("Email queue step delta is negative. Not sure why it happened. Ignoring the delta. TODO investigate", { delta });
153155
return 0;
154156
}
155157

158+
if (delta > 30) {
159+
captureError("email-queue-step-delta-too-large", new StackAssertionError(`Email queue step delta is too large: ${delta}. Either the previous step took too long, or something is wrong.`));
160+
}
161+
156162
return delta;
157163
}
158164

@@ -491,15 +497,17 @@ async function processSingleEmail(context: TenancyProcessingContext, row: EmailO
491497
return;
492498
}
493499

494-
const result = await lowLevelSendEmailDirectViaProvider({
495-
tenancyId: context.tenancy.id,
496-
emailConfig: context.emailConfig,
497-
to: resolution.emails,
498-
subject: row.renderedSubject ?? "",
499-
html: row.renderedHtml ?? undefined,
500-
text: row.renderedText ?? undefined,
501-
shouldSkipDeliverabilityCheck: row.shouldSkipDeliverabilityCheck,
502-
});
500+
const result = getEnvBoolean("STACK_EMAIL_BRANCHING_DISABLE_QUEUE_SENDING")
501+
? Result.error({ errorType: "email-sending-disabled", canRetry: false, message: "Email sending is disabled", rawError: new Error("Email sending is disabled") })
502+
: await lowLevelSendEmailDirectViaProvider({
503+
tenancyId: context.tenancy.id,
504+
emailConfig: context.emailConfig,
505+
to: resolution.emails,
506+
subject: row.renderedSubject ?? "",
507+
html: row.renderedHtml ?? undefined,
508+
text: row.renderedText ?? undefined,
509+
shouldSkipDeliverabilityCheck: row.shouldSkipDeliverabilityCheck,
510+
});
503511

504512
if (result.status === "error") {
505513
await globalPrismaClient.emailOutbox.update({

apps/backend/src/lib/emails.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { runAsynchronouslyAndWaitUntil } from '@/utils/vercel';
33
import { EmailOutboxCreatedWith } from '@prisma/client';
44
import { DEFAULT_TEMPLATE_IDS } from '@stackframe/stack-shared/dist/helpers/emails';
55
import { UsersCrud } from '@stackframe/stack-shared/dist/interface/crud/users';
6-
import { getEnvVariable } from '@stackframe/stack-shared/dist/utils/env';
6+
import { getEnvBoolean, getEnvVariable } from '@stackframe/stack-shared/dist/utils/env';
77
import { StackAssertionError } from '@stackframe/stack-shared/dist/utils/errors';
88
import { Json } from '@stackframe/stack-shared/dist/utils/json';
99
import { runEmailQueueStep, serializeRecipient } from './email-queue-step';
@@ -67,9 +67,12 @@ export async function sendEmailToMany(options: {
6767
overrideNotificationCategoryId: options.overrideNotificationCategoryId,
6868
})),
6969
});
70-
// The cron job should run runEmailQueueStep() to process the emails, but we call it here again for those self-hosters
71-
// who didn't set up the cron job correctly, and also just in case something happens to the cron job.
72-
runAsynchronouslyAndWaitUntil(runEmailQueueStep());
70+
71+
if (!getEnvBoolean("STACK_EMAIL_BRANCHING_DISABLE_QUEUE_AUTO_TRIGGER")) {
72+
// The cron job should run runEmailQueueStep() to process the emails, but we call it here again for those self-hosters
73+
// who didn't set up the cron job correctly, and also just in case something happens to the cron job.
74+
runAsynchronouslyAndWaitUntil(runEmailQueueStep());
75+
}
7376
}
7477

7578
export async function sendEmailFromDefaultTemplate(options: {

packages/stack-shared/src/utils/env.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { throwErr } from "./errors";
1+
import { StackAssertionError, throwErr } from "./errors";
22
import { deindent } from "./strings";
33

44
export function isBrowserLike() {
@@ -57,6 +57,17 @@ export function getEnvVariable(name: string, defaultValue?: string | undefined):
5757
return value;
5858
}
5959

60+
export function getEnvBoolean(name: string): boolean {
61+
const value = getEnvVariable(name, "false");
62+
if (value === "true") {
63+
return true;
64+
} else if (value === "false") {
65+
return false;
66+
} else {
67+
throw new StackAssertionError(`Environment variable ${name} must be either "true" or "false": found ${JSON.stringify(value)}`);
68+
}
69+
}
70+
6071
export function getNextRuntime() {
6172
// This variable is compiled into the client bundle, so we can't use getEnvVariable here.
6273
return process.env.NEXT_RUNTIME || throwErr("Missing environment variable: NEXT_RUNTIME");

0 commit comments

Comments
 (0)