From 05453fc59dea54a3dec04addea5031fc503d0efc Mon Sep 17 00:00:00 2001 From: Makisuo Date: Thu, 5 Nov 2026 13:18:54 +0100 Subject: [PATCH] fix(api): stop managing the invite sending subdomain from the stack The prd deploy token has no Email Sending permission, so planning the SendingSubdomain resource from #398 failed with "Forbidden: Authentication error" and every prd deploy since has failed. mail.hazel.sh is now onboarded to Email Sending once, out of band (Cloudflare created its DKIM, SPF, MX and DMARC records on the zone), so the stack no longer declares it. Invites still default to invites@mail.hazel.sh. Co-Authored-By: Claude Opus 5.5 --- apps/backend/src/services/invite-mailer.ts | 8 +++++--- apps/backend/src/worker.ts | 19 +------------------ 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/apps/backend/src/services/invite-mailer.ts b/apps/backend/src/services/invite-mailer.ts index 77bbb44f2..cd1f3679a 100644 --- a/apps/backend/src/services/invite-mailer.ts +++ b/apps/backend/src/services/invite-mailer.ts @@ -7,9 +7,11 @@ import { Config, Context, Effect, Layer, Schema } from "effect" */ /** - * The domain invitations are sent from. The stack registers it as a Cloudflare Email Sending - * subdomain (`worker.ts`); a sender on a domain that is not onboarded can only reach verified - * addresses ("destination address is not a verified address"). + * The domain invitations are sent from. It is onboarded to Cloudflare Email Sending as a subdomain + * of the hazel.sh zone (DKIM, SPF and return-path records live on the zone). That is done once in + * the dashboard, not by the stack: the deploy token has no Email Sending permission. A sender on a + * domain that is not onboarded can only reach verified addresses ("destination address is not a + * verified address"). */ export const INVITE_EMAIL_DOMAIN = "mail.hazel.sh" diff --git a/apps/backend/src/worker.ts b/apps/backend/src/worker.ts index b637f3912..4ac44529c 100644 --- a/apps/backend/src/worker.ts +++ b/apps/backend/src/worker.ts @@ -5,7 +5,7 @@ * class tags of the previous deploy. */ import { bindBotGateways } from "@hazel/bot-gateway/object" -import { HazelStack, hazelWorkerProps, stageProps, ZONE } from "@hazel/infra/cloudflare" +import { HazelStack, hazelWorkerProps, stageProps } from "@hazel/infra/cloudflare" import { cachedRecoverable } from "@hazel/infra/cached-recoverable" import { hazelTelemetry } from "@hazel/infra/maple" import { isolateContext } from "@hazel/infra/worker-http" @@ -17,7 +17,6 @@ import { DiscordGatewayObjectLive, } from "./worker/discord-gateway-object" import { HTML_RENDER_PENDING_MAX_AGE_SECONDS, HTML_RENDER_PENDING_PREFIX } from "./services/html-renders" -import { INVITE_EMAIL_DOMAIN } from "./services/invite-mailer" import { apiEnv } from "./worker/env" import { buildApp, makeFetch } from "./worker/http" import { MessageLiveObject, MessageLiveObjectLive } from "./worker/live/message-live-object" @@ -62,28 +61,12 @@ export const HtmlRendersBucket = Cloudflare.R2.Bucket( })), ) -/** - * Onboards the invitation sender's domain to Cloudflare Email Sending (DKIM, SPF and return-path - * records are created on the zone). Without it the `EMAIL` binding only reaches verified - * addresses. One per zone, so only prd declares it; every stage sends from it. - */ -const InviteSendingSubdomain = Effect.gen(function* () { - const { accountId } = yield* yield* Cloudflare.CloudflareEnvironment - const zone = yield* Cloudflare.Zone.findZoneByName({ accountId, name: ZONE }) - if (zone === undefined) return yield* Effect.die(new Error(`Cloudflare zone ${ZONE} not found`)) - return yield* Cloudflare.Email.SendingSubdomain("invite-sending-subdomain", { - zoneId: zone.id, - name: INVITE_EMAIL_DOMAIN, - }) -}).pipe(Effect.orDie) - /** `__ALCHEMY_RUNTIME__` folds to `true` in the bundle, so the stack-side branch is tree-shaken. */ const props = Effect.gen(function* () { if (globalThis.__ALCHEMY_RUNTIME__) return { main: import.meta.url } const stack = yield* HazelStack const cache = yield* ApiCache const htmlRenders = yield* HtmlRendersBucket - if (stack.stage.kind === "prd") yield* InviteSendingSubdomain const email = yield* Cloudflare.Email.SendEmail(EMAIL_BINDING) return { main: import.meta.url,