From d6aa82e070e58eaa9ff1c95eb5d9ab910bd4581f Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 31 Jul 2026 16:25:25 +0100 Subject: [PATCH] feat(cloud): create subscribers --- apps/web/src/env.ts | 3 ++ cloud/docker-compose.yml | 3 ++ packages/auth/src/hooks.ts | 46 +++++++++++++++++----- packages/email/src/index.tsx | 1 + packages/email/src/subscriberClient.ts | 53 ++++++++++++++++++++++++++ 5 files changed, 96 insertions(+), 10 deletions(-) create mode 100644 packages/email/src/subscriberClient.ts diff --git a/apps/web/src/env.ts b/apps/web/src/env.ts index 3d00b33c..ee373512 100644 --- a/apps/web/src/env.ts +++ b/apps/web/src/env.ts @@ -53,6 +53,9 @@ export const env = createEnv({ LINKEDIN_CLIENT_ID: z.string().optional(), LINKEDIN_CLIENT_SECRET: z.string().optional(), NOVU_API_KEY: z.string().optional(), + SUBSCRIBER_API_URL: z.string().url().optional(), + SUBSCRIBER_API_KEY: z.string().optional(), + SUBSCRIBER_ENVIRONMENT_ID: z.string().optional(), EMAIL_UNSUBSCRIBE_SECRET: z.string().optional(), // Generic OIDC Provider OIDC_CLIENT_ID: z.string().optional(), diff --git a/cloud/docker-compose.yml b/cloud/docker-compose.yml index 659ff71a..c09d2f4a 100644 --- a/cloud/docker-compose.yml +++ b/cloud/docker-compose.yml @@ -64,6 +64,9 @@ services: - NOVU_API_KEY=${NOVU_API_KEY} - DISCORD_WEBHOOK_URL=${DISCORD_WEBHOOK_URL} - EMAIL_UNSUBSCRIBE_SECRET=${EMAIL_UNSUBSCRIBE_SECRET} + - SUBSCRIBER_API_URL=${SUBSCRIBER_API_URL} + - SUBSCRIBER_API_KEY=${SUBSCRIBER_API_KEY} + - SUBSCRIBER_ENVIRONMENT_ID=${SUBSCRIBER_ENVIRONMENT_ID} # S3 storage - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} diff --git a/packages/auth/src/hooks.ts b/packages/auth/src/hooks.ts index dcdc35aa..7ed41f85 100644 --- a/packages/auth/src/hooks.ts +++ b/packages/auth/src/hooks.ts @@ -6,14 +6,14 @@ import { env } from "next-runtime-env"; import type { dbClient } from "@kan/db/client"; import * as memberRepo from "@kan/db/repository/member.repo"; import * as userRepo from "@kan/db/repository/user.repo"; -import { notificationClient } from "@kan/email"; +import { createSubscriber, notificationClient } from "@kan/email"; import { createLogger } from "@kan/logger"; import { createEmailUnsubscribeLink, createS3Client } from "@kan/shared"; -const log = createLogger("auth"); - import { downloadImage } from "./utils"; +const log = createLogger("auth"); + type BetterAuthUser = { id: string; createdAt: Date; @@ -95,18 +95,26 @@ export function createDatabaseHooks(db: dbClient) { } if (notificationClient) { + const [firstName, ...rest] = (user.name || "") + .split(" ") + .filter(Boolean); + const lastName = rest.length ? rest.join(" ") : undefined; + try { - const [firstName, ...rest] = (user.name || "") - .split(" ") - .filter(Boolean); - const lastName = rest.length ? rest.join(" ") : undefined; const avatarUrl = avatarKey ? `${env("NEXT_PUBLIC_STORAGE_URL")}/${env("NEXT_PUBLIC_AVATAR_BUCKET_NAME")}/${avatarKey}` : undefined; const unsubscribeUrl = await createEmailUnsubscribeLink(user.id); - log.info({ workflowId: "user-signup", userId: user.id, email: user.email }, "Triggering Novu workflow"); + log.info( + { + workflowId: "user-signup", + userId: user.id, + email: user.email, + }, + "Triggering Novu workflow", + ); await notificationClient.trigger({ to: { subscriberId: user.id, @@ -126,7 +134,10 @@ export function createDatabaseHooks(db: dbClient) { }, workflowId: "user-signup", }); - log.info({ workflowId: "user-signup", userId: user.id }, "Novu workflow triggered"); + log.info( + { workflowId: "user-signup", userId: user.id }, + "Novu workflow triggered", + ); await notificationClient.subscribers.credentials.update( { @@ -139,7 +150,22 @@ export function createDatabaseHooks(db: dbClient) { user.id, ); } catch (error) { - log.error({ err: error }, "Error adding user to notification client"); + log.error( + { err: error }, + "Error adding user to notification client", + ); + } + + try { + await createSubscriber({ + email: user.email, + externalId: user.id, + firstName, + lastName, + name: user.name, + }); + } catch (error) { + log.error({ err: error }, "Error creating subscriber"); } } }, diff --git a/packages/email/src/index.tsx b/packages/email/src/index.tsx index 30ea59d3..7aa3cdb4 100644 --- a/packages/email/src/index.tsx +++ b/packages/email/src/index.tsx @@ -2,3 +2,4 @@ export const name = "email"; export { sendEmail } from "./sendEmail"; export { notificationClient } from "./notificationClient"; +export { createSubscriber } from "./subscriberClient"; diff --git a/packages/email/src/subscriberClient.ts b/packages/email/src/subscriberClient.ts new file mode 100644 index 00000000..00e68367 --- /dev/null +++ b/packages/email/src/subscriberClient.ts @@ -0,0 +1,53 @@ +import { createLogger } from "@kan/logger"; + +const log = createLogger("subscriberClient"); + +export const subscriberClient = + process.env.NEXT_PUBLIC_KAN_ENV === "cloud" && + process.env.SUBSCRIBER_API_URL && + process.env.SUBSCRIBER_API_KEY && + process.env.SUBSCRIBER_ENVIRONMENT_ID + ? { + apiUrl: process.env.SUBSCRIBER_API_URL, + apiKey: process.env.SUBSCRIBER_API_KEY, + environmentId: process.env.SUBSCRIBER_ENVIRONMENT_ID, + } + : null; + +interface CreateSubscriberInput { + email: string; + externalId: string; + firstName?: string; + lastName?: string; + name?: string; +} + +export async function createSubscriber(input: CreateSubscriberInput) { + if (!subscriberClient) return; + + try { + const response = await fetch( + `${subscriberClient.apiUrl}/environments/${subscriberClient.environmentId}/subscribers`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-API-Key": subscriberClient.apiKey, + }, + body: JSON.stringify(input), + }, + ); + + if (!response.ok) { + log.error( + { + status: response.status, + body: await response.text().catch(() => undefined), + }, + "Failed to create subscriber.dev subscriber", + ); + } + } catch (error) { + log.error({ err: error }, "Error creating subscriber.dev subscriber"); + } +}