Enable self-hosted deployment with BunMail, SMTP, and Docker fixes.#3491
Enable self-hosted deployment with BunMail, SMTP, and Docker fixes.#3491raedbrahem wants to merge 1 commit into
Conversation
Add BunMail REST and SMTP email transports with direct send fallback when Trigger.dev is unset, wire the API service into docker-compose, and fix Prisma TLS for local Postgres hostnames.
|
@raedbrahem is attempting to deploy a commit to the Comp AI Team on Vercel. A member of the Team first needs to authorize it. |
|
|
There was a problem hiding this comment.
11 issues found across 12 files
Confidence score: 2/5
docker-compose.ymlcurrently has conflicting network/DB settings (including an externalcomp_networkthat isn’t provisioned), so a clean localdocker compose upcan fail or the API can’t reach Postgres; this blocks reliable local bring-up and testing — align hosts/credentials withpackages/db/docker-compose.ymland let Compose create/provision the shared network.docker-compose.ymlmaps the app to 3001 while auth docs/URLs still point to 3000, so local sign-in and callback flows can route to the wrong service and appear broken — keep the app on 3000 or update all related auth URL configuration/docs together.apps/api/src/email/email-transport.tsdropsheaders,attachments, and (for non-Resend direct transports)scheduledAt, which can break Gmail one-click unsubscribe compliance, lose files, and send scheduled mail immediately — preserve these fields across transports or fail fast when unsupported.Dockerfilepipes a mutable remote NodeSource script tobashas root during builds, creating a concrete supply-chain execution risk; move to a pinned Node image/stage or add checksum/signature verification before execution.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/api/src/email/email-transport.ts">
<violation number="1" location="apps/api/src/email/email-transport.ts:5">
P3: A second `EmailAttachment` interface is defined here, duplicating the one already exported from `./resend.ts`. Since `trigger-email.ts` still imports from `./resend`, the codebase now has two sources of truth. Import and re-export from `./resend.ts` (or move the shared type there) instead of redefining it.</violation>
<violation number="2" location="apps/api/src/email/email-transport.ts:77">
P1: The BunMail transport silently drops `headers` and `attachments` — both `sendViaSmtp` and `sendViaResend` support them. This means `List-Unsubscribe` headers (needed for Gmail one-click unsubscribe compliance) and file attachments are lost when BunMail is active. Either add `headers`/`attachments` to the BunMail API call body, or document the limitation and throw if those params are provided but unsupported.</violation>
<violation number="3" location="apps/api/src/email/email-transport.ts:150">
P2: Scheduled emails are sent immediately whenever either new direct transport is selected, because `scheduledAt` is silently discarded outside the Resend path. Preserve scheduling semantics for those transports or fail explicitly when scheduling is requested.</violation>
</file>
<file name="Dockerfile">
<violation number="1" location="Dockerfile:51">
P2: A changed or compromised NodeSource setup response executes arbitrary code as root during migrator builds because this mutable remote script is piped directly to `bash`. Use a pinned Node base/stage or verify a checksummed/signed artifact before execution.</violation>
</file>
<file name="docker-compose.yml">
<violation number="1" location="docker-compose.yml:35">
P1: The documented local auth URL still targets port 3000, while this mapping exposes the app at 3001; default local sign-in/callback links therefore target the wrong service. Keep the app on 3000 or update the associated documented/build-time URLs together.</violation>
<violation number="2" location="docker-compose.yml:40">
P2: Database password hardcoded in three services' environment sections. The password `comp` is repeated across `app`, `api`, and `portal` services. Each service already loads a `.env` file via `env_file`, so the DATABASE_URL in the `environment` section overrides whatever the operator has configured in their env file. Consider moving the DATABASE_URL (with its password) out of the inline `environment` block and into the respective `.env` files, or use an env-var substitution like `${DATABASE_URL}` so the docker-compose.yml itself contains no credentials.</violation>
<violation number="3" location="docker-compose.yml:40">
P1: The new API cannot connect to the repository's local Postgres setup: its hard-coded host, credentials, and network do not match `packages/db/docker-compose.yml`. Wire both Compose projects to a shared network and use the database service's configured URL, or source the URL from deployment configuration.</violation>
<violation number="4" location="docker-compose.yml:89">
P2: Portal S3 credentials are committed as fixed values and override environment-file credentials, preventing self-hosted deployments from supplying their own MinIO/AWS secret. Source both values from environment configuration instead.</violation>
<violation number="5" location="docker-compose.yml:104">
P1: A clean local `docker compose up` cannot start because `comp_network` is marked external but is not provisioned by this stack. Let Compose create the named network, or document/provision it as a prerequisite alongside the database.</violation>
</file>
<file name="apps/api/src/email/trigger-email.ts">
<violation number="1" location="apps/api/src/email/trigger-email.ts:31">
P2: The `sendEmailDirect` function in this file duplicates the full `List-Unsubscribe` header construction that already exists in `send-email.ts` and `send-batch-email.ts`. Extract this into a shared helper (e.g., `buildUnsubscribeHeaders(to: string)` in the email utils) so changes to the URL template, header format, or API base URL don't require updating three separate locations.</violation>
</file>
<file name="apps/api/prisma/client.ts">
<violation number="1" location="apps/api/prisma/client.ts:6">
P2: The `LOCAL_HOSTNAMES` set and SSL configuration helpers (`isLocalhostUrl`, `stripSslMode`) are duplicated across 4 files instead of being centralized. This diff required adding `'comp-postgres'` to all 4 locations identically — a pattern that is fragile and prone to drift between environments. Consider exporting the shared hostname set and helpers from `packages/db/src/ssl-config.ts` (which already exports `resolveSslConfig`) and importing them in the apps' prisma client files, or aligning the apps to use `@trycompai/db`'s client directly.</violation>
</file>
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
| NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL} | ||
| ports: | ||
| - '3000:3000' | ||
| - '3001:3000' |
There was a problem hiding this comment.
P1: The documented local auth URL still targets port 3000, while this mapping exposes the app at 3001; default local sign-in/callback links therefore target the wrong service. Keep the app on 3000 or update the associated documented/build-time URLs together.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docker-compose.yml, line 35:
<comment>The documented local auth URL still targets port 3000, while this mapping exposes the app at 3001; default local sign-in/callback links therefore target the wrong service. Keep the app on 3000 or update the associated documented/build-time URLs together.</comment>
<file context>
@@ -30,17 +30,43 @@ services:
+ NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL}
ports:
- - '3000:3000'
+ - '3001:3000'
env_file:
- apps/app/.env
</file context>
| - '3001:3000' | |
| - '3000:3000' |
| - apps/app/.env | ||
| environment: | ||
| PGSSLMODE: disable | ||
| DATABASE_URL: postgresql://comp:comp@comp-postgres:5432/comp?sslmode=disable |
There was a problem hiding this comment.
P1: The new API cannot connect to the repository's local Postgres setup: its hard-coded host, credentials, and network do not match packages/db/docker-compose.yml. Wire both Compose projects to a shared network and use the database service's configured URL, or source the URL from deployment configuration.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docker-compose.yml, line 40:
<comment>The new API cannot connect to the repository's local Postgres setup: its hard-coded host, credentials, and network do not match `packages/db/docker-compose.yml`. Wire both Compose projects to a shared network and use the database service's configured URL, or source the URL from deployment configuration.</comment>
<file context>
@@ -30,17 +30,43 @@ services:
- apps/app/.env
+ environment:
+ PGSSLMODE: disable
+ DATABASE_URL: postgresql://comp:comp@comp-postgres:5432/comp?sslmode=disable
restart: unless-stopped
healthcheck:
</file context>
| networks: | ||
| default: | ||
| name: comp_network | ||
| external: true |
There was a problem hiding this comment.
P1: A clean local docker compose up cannot start because comp_network is marked external but is not provisioned by this stack. Let Compose create the named network, or document/provision it as a prerequisite alongside the database.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docker-compose.yml, line 104:
<comment>A clean local `docker compose up` cannot start because `comp_network` is marked external but is not provisioned by this stack. Let Compose create the named network, or document/provision it as a prerequisite alongside the database.</comment>
<file context>
@@ -49,14 +75,30 @@ services:
+networks:
+ default:
+ name: comp_network
+ external: true
</file context>
| external: true | |
| external: false |
| })); | ||
| } | ||
|
|
||
| async function sendViaBunMail(params: { |
There was a problem hiding this comment.
P1: The BunMail transport silently drops headers and attachments — both sendViaSmtp and sendViaResend support them. This means List-Unsubscribe headers (needed for Gmail one-click unsubscribe compliance) and file attachments are lost when BunMail is active. Either add headers/attachments to the BunMail API call body, or document the limitation and throw if those params are provided but unsupported.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/email/email-transport.ts, line 77:
<comment>The BunMail transport silently drops `headers` and `attachments` — both `sendViaSmtp` and `sendViaResend` support them. This means `List-Unsubscribe` headers (needed for Gmail one-click unsubscribe compliance) and file attachments are lost when BunMail is active. Either add `headers`/`attachments` to the BunMail API call body, or document the limitation and throw if those params are provided but unsupported.</comment>
<file context>
@@ -0,0 +1,243 @@
+ }));
+}
+
+async function sendViaBunMail(params: {
+ from: string;
+ to: string;
</file context>
| html: string; | ||
| cc?: string | string[]; | ||
| headers?: Record<string, string>; | ||
| scheduledAt?: string; |
There was a problem hiding this comment.
P2: Scheduled emails are sent immediately whenever either new direct transport is selected, because scheduledAt is silently discarded outside the Resend path. Preserve scheduling semantics for those transports or fail explicitly when scheduling is requested.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/email/email-transport.ts, line 150:
<comment>Scheduled emails are sent immediately whenever either new direct transport is selected, because `scheduledAt` is silently discarded outside the Resend path. Preserve scheduling semantics for those transports or fail explicitly when scheduling is requested.</comment>
<file context>
@@ -0,0 +1,243 @@
+ html: string;
+ cc?: string | string[];
+ headers?: Record<string, string>;
+ scheduledAt?: string;
+ attachments?: EmailAttachment[];
+}): Promise<{ id: string }> {
</file context>
| @@ -21,7 +21,7 @@ services: | |||
| target: migrator | |||
There was a problem hiding this comment.
P2: Database password hardcoded in three services' environment sections. The password comp is repeated across app, api, and portal services. Each service already loads a .env file via env_file, so the DATABASE_URL in the environment section overrides whatever the operator has configured in their env file. Consider moving the DATABASE_URL (with its password) out of the inline environment block and into the respective .env files, or use an env-var substitution like ${DATABASE_URL} so the docker-compose.yml itself contains no credentials.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docker-compose.yml, line 40:
<comment>Database password hardcoded in three services' environment sections. The password `comp` is repeated across `app`, `api`, and `portal` services. Each service already loads a `.env` file via `env_file`, so the DATABASE_URL in the `environment` section overrides whatever the operator has configured in their env file. Consider moving the DATABASE_URL (with its password) out of the inline `environment` block and into the respective `.env` files, or use an env-var substitution like `${DATABASE_URL}` so the docker-compose.yml itself contains no credentials.</comment>
<file context>
@@ -30,17 +30,43 @@ services:
- apps/app/.env
+ environment:
+ PGSSLMODE: disable
+ DATABASE_URL: postgresql://comp:comp@comp-postgres:5432/comp?sslmode=disable
restart: unless-stopped
healthcheck:
</file context>
| scheduledAt?: string; | ||
| attachments?: EmailAttachment[]; | ||
| }): Promise<{ id: string }> { | ||
| const apiBaseUrl = process.env.NEXT_PUBLIC_API_URL || 'https://api.trycomp.ai'; |
There was a problem hiding this comment.
P2: The sendEmailDirect function in this file duplicates the full List-Unsubscribe header construction that already exists in send-email.ts and send-batch-email.ts. Extract this into a shared helper (e.g., buildUnsubscribeHeaders(to: string) in the email utils) so changes to the URL template, header format, or API base URL don't require updating three separate locations.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/email/trigger-email.ts, line 31:
<comment>The `sendEmailDirect` function in this file duplicates the full `List-Unsubscribe` header construction that already exists in `send-email.ts` and `send-batch-email.ts`. Extract this into a shared helper (e.g., `buildUnsubscribeHeaders(to: string)` in the email utils) so changes to the URL template, header format, or API base URL don't require updating three separate locations.</comment>
<file context>
@@ -17,6 +19,34 @@ function resolveChannel(flags: TriggerEmailFlags): EmailChannel {
+ scheduledAt?: string;
+ attachments?: EmailAttachment[];
+}): Promise<{ id: string }> {
+ const apiBaseUrl = process.env.NEXT_PUBLIC_API_URL || 'https://api.trycomp.ai';
+ const token = generateUnsubscribeToken(params.to);
+ const oneClickUrl = `${apiBaseUrl}/v1/email/unsubscribe?email=${encodeURIComponent(params.to)}&token=${encodeURIComponent(token)}`;
</file context>
| MOCK_REDIS: "true" | ||
| FLEET_AGENT_BUCKET_NAME: comp | ||
| DEVICE_AGENT_S3_ENV: production | ||
| APP_AWS_ACCESS_KEY_ID: minioadmin |
There was a problem hiding this comment.
P2: Portal S3 credentials are committed as fixed values and override environment-file credentials, preventing self-hosted deployments from supplying their own MinIO/AWS secret. Source both values from environment configuration instead.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docker-compose.yml, line 89:
<comment>Portal S3 credentials are committed as fixed values and override environment-file credentials, preventing self-hosted deployments from supplying their own MinIO/AWS secret. Source both values from environment configuration instead.</comment>
<file context>
@@ -49,14 +75,30 @@ services:
+ MOCK_REDIS: "true"
+ FLEET_AGENT_BUCKET_NAME: comp
+ DEVICE_AGENT_S3_ENV: production
+ APP_AWS_ACCESS_KEY_ID: minioadmin
+ APP_AWS_SECRET_ACCESS_KEY: minioadmin
+ APP_AWS_BUCKET_NAME: comp
</file context>
| const globalForPrisma = global as unknown as { prisma?: PrismaClient }; | ||
|
|
||
| const LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1']); | ||
| const LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', 'comp-postgres']); |
There was a problem hiding this comment.
P2: The LOCAL_HOSTNAMES set and SSL configuration helpers (isLocalhostUrl, stripSslMode) are duplicated across 4 files instead of being centralized. This diff required adding 'comp-postgres' to all 4 locations identically — a pattern that is fragile and prone to drift between environments. Consider exporting the shared hostname set and helpers from packages/db/src/ssl-config.ts (which already exports resolveSslConfig) and importing them in the apps' prisma client files, or aligning the apps to use @trycompai/db's client directly.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/prisma/client.ts, line 6:
<comment>The `LOCAL_HOSTNAMES` set and SSL configuration helpers (`isLocalhostUrl`, `stripSslMode`) are duplicated across 4 files instead of being centralized. This diff required adding `'comp-postgres'` to all 4 locations identically — a pattern that is fragile and prone to drift between environments. Consider exporting the shared hostname set and helpers from `packages/db/src/ssl-config.ts` (which already exports `resolveSslConfig`) and importing them in the apps' prisma client files, or aligning the apps to use `@trycompai/db`'s client directly.</comment>
<file context>
@@ -3,7 +3,7 @@ import { PrismaPg } from '@prisma/adapter-pg';
const globalForPrisma = global as unknown as { prisma?: PrismaClient };
-const LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1']);
+const LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', 'comp-postgres']);
function stripSslMode(connectionString: string): string {
</file context>
| import type Mail from 'nodemailer/lib/mailer'; | ||
| import { resend } from './resend'; | ||
|
|
||
| export interface EmailAttachment { |
There was a problem hiding this comment.
P3: A second EmailAttachment interface is defined here, duplicating the one already exported from ./resend.ts. Since trigger-email.ts still imports from ./resend, the codebase now has two sources of truth. Import and re-export from ./resend.ts (or move the shared type there) instead of redefining it.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/email/email-transport.ts, line 5:
<comment>A second `EmailAttachment` interface is defined here, duplicating the one already exported from `./resend.ts`. Since `trigger-email.ts` still imports from `./resend`, the codebase now has two sources of truth. Import and re-export from `./resend.ts` (or move the shared type there) instead of redefining it.</comment>
<file context>
@@ -0,0 +1,243 @@
+import type Mail from 'nodemailer/lib/mailer';
+import { resend } from './resend';
+
+export interface EmailAttachment {
+ filename: string;
+ content: Buffer | string;
</file context>
Add BunMail REST and SMTP email transports with direct send fallback when Trigger.dev is unset, wire the API service into docker-compose, and fix Prisma TLS for local Postgres hostnames.
What does this PR do?
Visual Demo (For contributors especially)
A visual demonstration is strongly recommended, for both the original and new change (video / image - any one).
Video Demo (if applicable):
Image Demo (if applicable):
Mandatory Tasks (DO NOT REMOVE)
How should this be tested?
Checklist
Summary by cubic
Adds BunMail and SMTP email transports with a direct-send fallback when Trigger.dev is not set. Updates Docker for self‑hosting by adding the API service and fixing Prisma TLS for local Postgres.
New Features
apps/apiwith priority: BunMail → SMTP → Resend; attachments and one‑click unsubscribe headers supported. Addsnodemailerfor SMTP.TRIGGER_SECRET_KEYis unset; the Trigger task now uses the same transport.apiservice on port 3333; App moves to 3001 and Portal to 3002; health checks usewget; images build localpackages/dband seed viatsx. Prisma treatscomp-postgresas local and defaultssslto false unless explicitly set.Migration
BUNMAIL_API_URL,BUNMAIL_API_KEY,BUNMAIL_FROM), or SMTP (SMTP_*), or Resend; seeapps/api/.env.example.docker network create comp_network, thendocker compose up -d.comp-postgresinDATABASE_URLwithsslmode=disable, and setNEXT_PUBLIC_API_URLfor App/Portal. New ports: App3001, Portal3002, API3333.Written for commit b70915b. Summary will update on new commits.