Enable self-hosted deployment with BunMail, SMTP, and Docker fixes. - #3492
Enable self-hosted deployment with BunMail, SMTP, and Docker fixes.#3492raedbrahem wants to merge 2 commits 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.
7 issues found across 12 files
Confidence score: 2/5
- In
docker-compose.yml, the service env overrides point at Postgres host/credentials that do not exist in this repo’s DB setup, so self-hosted services are likely to fail at startup or crash-loop when they try to connect to the database — align these env values with the actual compose Postgres service definitions. - In
apps/api/src/email/email-transport.ts, the SMTP path dropsscheduledAt, which can send scheduled emails immediately and create user-facing timing/compliance issues — defer SMTP sends untilscheduledAtor explicitly reject scheduled SMTP requests. - In
apps/api/src/email/email-transport.ts(sendHtmlEmail), the BunMail branch omitsheadersandattachments, so unsubscribe headers and file attachments can be silently lost in production emails — forward both fields in the BunMail payload and add coverage for these cases. docker-compose.ymlnow requires an externalcomp_network, andpackages/db/src/ssl-config.tslacks a regression test for the new Docker hostname, making local/self-hosted bring-up more fragile and harder to debug when networking or TLS behavior shifts — document/provision the network requirement and add the hostname SSL test case.
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="packages/db/src/ssl-config.ts">
<violation number="1" location="packages/db/src/ssl-config.ts:6">
P3: The new Docker hostname has no regression test, so an allowlist typo or later URL-parsing change can make self-hosted containers attempt TLS and fail to connect. Add a `resolveSslConfig('postgresql://u:p@comp-postgres:5432/x', {})` case asserting `undefined`.</violation>
</file>
<file name="docker-compose.yml">
<violation number="1" location="docker-compose.yml:40">
P1: The self-hosted services cannot connect to the repository's Postgres container: these overrides replace each service's env-file URL with a host and credentials that do not exist in the provided database compose setup. Use the actual database service/credentials or parameterize `DATABASE_URL` so deployments can supply their own connection string.</violation>
<violation number="2" location="docker-compose.yml:104">
P2: The `networks.default` overrides the project's default network to `comp_network` with `external: true`, which means this Docker network must already exist before `docker-compose up` will succeed. Running the compose file for the first time without a prior `docker network create comp_network` will fail with an opaque network-not-found error. Consider either dropping `external: true` so Docker Compose creates the network automatically, or adding a `docker network create` step to the project's setup documentation.</violation>
</file>
<file name="apps/api/src/email/email-transport.ts">
<violation number="1" location="apps/api/src/email/email-transport.ts:212">
P1: When BunMail is configured, the `sendHtmlEmail` function silently drops both `headers` and `attachments` from the outgoing email payload. This means the RFC 8058 one-click unsubscribe headers (`List-Unsubscribe` / `List-Unsubscribe-Post`) that both callers (`send-email.ts` and `trigger-email.ts`) explicitly build and pass through will not reach the BunMail API, breaking unsubscribe compliance for Gmail and other providers. Any attachments sent through the BunMail path are also silently discarded. Consider either adding `headers` and `attachments` support to the `sendViaBunMail` function and the BunMail API call, or documenting this as a known limitation so callers can handle it appropriately.</violation>
<violation number="2" location="apps/api/src/email/email-transport.ts:222">
P1: Scheduled emails are delivered immediately when SMTP is configured because this branch discards `scheduledAt`. Queue/defer SMTP sends until the requested time, or reject scheduled SMTP requests so users do not receive notifications early.</violation>
</file>
<file name="Dockerfile">
<violation number="1" location="Dockerfile:54">
P3: Docker builds can change unexpectedly because this resolves the latest global `tsx` outside the lockfile, and it bypasses the repository’s Bun-only dependency workflow. Use a pinned, Bun-managed `tsx` dependency instead.</violation>
</file>
<file name="apps/api/src/email/trigger-email.ts">
<violation number="1" location="apps/api/src/email/trigger-email.ts:75">
P3: The `console.log` on line 75 is the only informational log in this function, while the error path on line 95 uses `console.error`. In a NestJS application, consider using a structured `Logger` service for consistency, so that log levels and formatting are uniform across the codebase.</violation>
</file>
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
| - 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 self-hosted services cannot connect to the repository's Postgres container: these overrides replace each service's env-file URL with a host and credentials that do not exist in the provided database compose setup. Use the actual database service/credentials or parameterize DATABASE_URL so deployments can supply their own connection string.
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 self-hosted services cannot connect to the repository's Postgres container: these overrides replace each service's env-file URL with a host and credentials that do not exist in the provided database compose setup. Use the actual database service/credentials or parameterize `DATABASE_URL` so deployments can supply their own connection string.</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>
| } | ||
|
|
||
| if (isSmtpConfigured()) { | ||
| return sendViaSmtp({ |
There was a problem hiding this comment.
P1: Scheduled emails are delivered immediately when SMTP is configured because this branch discards scheduledAt. Queue/defer SMTP sends until the requested time, or reject scheduled SMTP requests so users do not receive notifications early.
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 222:
<comment>Scheduled emails are delivered immediately when SMTP is configured because this branch discards `scheduledAt`. Queue/defer SMTP sends until the requested time, or reject scheduled SMTP requests so users do not receive notifications early.</comment>
<file context>
@@ -0,0 +1,243 @@
+ }
+
+ if (isSmtpConfigured()) {
+ return sendViaSmtp({
+ from: fromAddress,
+ to: toAddress,
</file context>
| } | ||
|
|
||
| if (isBunMailConfigured()) { | ||
| return sendViaBunMail({ |
There was a problem hiding this comment.
P1: When BunMail is configured, the sendHtmlEmail function silently drops both headers and attachments from the outgoing email payload. This means the RFC 8058 one-click unsubscribe headers (List-Unsubscribe / List-Unsubscribe-Post) that both callers (send-email.ts and trigger-email.ts) explicitly build and pass through will not reach the BunMail API, breaking unsubscribe compliance for Gmail and other providers. Any attachments sent through the BunMail path are also silently discarded. Consider either adding headers and attachments support to the sendViaBunMail function and the BunMail API call, or documenting this as a known limitation so callers can handle it appropriately.
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 212:
<comment>When BunMail is configured, the `sendHtmlEmail` function silently drops both `headers` and `attachments` from the outgoing email payload. This means the RFC 8058 one-click unsubscribe headers (`List-Unsubscribe` / `List-Unsubscribe-Post`) that both callers (`send-email.ts` and `trigger-email.ts`) explicitly build and pass through will not reach the BunMail API, breaking unsubscribe compliance for Gmail and other providers. Any attachments sent through the BunMail path are also silently discarded. Consider either adding `headers` and `attachments` support to the `sendViaBunMail` function and the BunMail API call, or documenting this as a known limitation so callers can handle it appropriately.</comment>
<file context>
@@ -0,0 +1,243 @@
+ }
+
+ if (isBunMailConfigured()) {
+ return sendViaBunMail({
+ from: fromAddress,
+ to: toAddress,
</file context>
| networks: | ||
| default: | ||
| name: comp_network | ||
| external: true |
There was a problem hiding this comment.
P2: The networks.default overrides the project's default network to comp_network with external: true, which means this Docker network must already exist before docker-compose up will succeed. Running the compose file for the first time without a prior docker network create comp_network will fail with an opaque network-not-found error. Consider either dropping external: true so Docker Compose creates the network automatically, or adding a docker network create step to the project's setup documentation.
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>The `networks.default` overrides the project's default network to `comp_network` with `external: true`, which means this Docker network must already exist before `docker-compose up` will succeed. Running the compose file for the first time without a prior `docker network create comp_network` will fail with an opaque network-not-found error. Consider either dropping `external: true` so Docker Compose creates the network automatically, or adding a `docker network create` step to the project's setup documentation.</comment>
<file context>
@@ -49,14 +75,30 @@ services:
+networks:
+ default:
+ name: comp_network
+ external: true
</file context>
| | { rejectUnauthorized: false }; | ||
|
|
||
| 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.
P3: The new Docker hostname has no regression test, so an allowlist typo or later URL-parsing change can make self-hosted containers attempt TLS and fail to connect. Add a resolveSslConfig('postgresql://u:p@comp-postgres:5432/x', {}) case asserting undefined.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/db/src/ssl-config.ts, line 6:
<comment>The new Docker hostname has no regression test, so an allowlist typo or later URL-parsing change can make self-hosted containers attempt TLS and fail to connect. Add a `resolveSslConfig('postgresql://u:p@comp-postgres:5432/x', {})` case asserting `undefined`.</comment>
<file context>
@@ -3,7 +3,7 @@ export type SslConfig =
| { rejectUnauthorized: false };
-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 isLocalhostUrl(connectionString: string): boolean {
</file context>
| && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ | ||
| && apt-get install -y nodejs \ | ||
| && rm -rf /var/lib/apt/lists/* \ | ||
| && npm install -g tsx |
There was a problem hiding this comment.
P3: Docker builds can change unexpectedly because this resolves the latest global tsx outside the lockfile, and it bypasses the repository’s Bun-only dependency workflow. Use a pinned, Bun-managed tsx dependency instead.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Dockerfile, line 54:
<comment>Docker builds can change unexpectedly because this resolves the latest global `tsx` outside the lockfile, and it bypasses the repository’s Bun-only dependency workflow. Use a pinned, Bun-managed `tsx` dependency instead.</comment>
<file context>
@@ -26,28 +31,29 @@ COPY apps/portal/package.json ./apps/portal/
+ && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
+ && apt-get install -y nodejs \
+ && rm -rf /var/lib/apt/lists/* \
+ && npm install -g tsx
-# Run migrations against the combined schema published by @trycompai/db
</file context>
| }; | ||
|
|
||
| if (!process.env.TRIGGER_SECRET_KEY) { | ||
| console.log( |
There was a problem hiding this comment.
P3: The console.log on line 75 is the only informational log in this function, while the error path on line 95 uses console.error. In a NestJS application, consider using a structured Logger service for consistency, so that log levels and formatting are uniform across the codebase.
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 75:
<comment>The `console.log` on line 75 is the only informational log in this function, while the error path on line 95 uses `console.error`. In a NestJS application, consider using a structured `Logger` service for consistency, so that log levels and formatting are uniform across the codebase.</comment>
<file context>
@@ -30,16 +60,26 @@ export async function triggerEmail(params: {
+ };
+
+ if (!process.env.TRIGGER_SECRET_KEY) {
+ console.log(
+ 'TRIGGER_SECRET_KEY not set; sending email directly via configured transport',
+ );
</file context>
There was a problem hiding this comment.
4 issues found across 8 files (changes from recent commits).
Confidence score: 2/5
- In
apps/api/src/auth/auth.server.ts,comp-app.dctrl.aiandcomp-portal.dctrl.aiare not included in Better AuthtrustedOrigins/API CORS checks, so self-hosted clients can receive the cookie but still fail cross-origin auth flows — add both hosts to the trusted origins and CORS allowlist paths used by auth. - In
docker-compose.yml,TRUST_APP_URLis set to the employee portal host even though the API uses it as the base for trust-site/access links, which can send users to the wrong destination and break trust-portal journeys — pointTRUST_APP_URLto the trust app domain and verify generated links end-to-end. - In
docker-compose.yml, usingminio/minio:latestadds upgrade drift risk across environments, and inpackages/email/emails/policy-acknowledgment-digest.tsxthe extra trailing-slash replace is dead code that can obscure intent — pin MinIO to a release tag and remove the redundant replace for predictability and clarity.
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/auth/auth.server.ts">
<violation number="1" location="apps/api/src/auth/auth.server.ts:45">
P1: Default self-hosted dctrl clients cannot complete cross-origin auth/CORS: `comp-app.dctrl.ai` and `comp-portal.dctrl.ai` receive the shared cookie but remain absent from Better Auth `trustedOrigins` and API CORS checks when `AUTH_TRUSTED_ORIGINS` is unset. Add the dctrl origins/pattern alongside this cookie-domain support, or require and provide `AUTH_TRUSTED_ORIGINS` in the deployment config.</violation>
</file>
<file name="docker-compose.yml">
<violation number="1" location="docker-compose.yml:65">
P1: Trust portal links generated by the API are pointed at the employee portal host. `TRUST_APP_URL` is consumed as the base for trust-site and access URLs, but this compose change sets it to `https://comp-portal.dctrl.ai`; recipients will be sent links such as `https://comp-portal.dctrl.ai/<friendlyUrl>/access/<token>`, which can route to the wrong application. The deployment should set `TRUST_APP_URL` to the actual trust portal URL (or leave it unset if `PORTAL_URL` is the intended trust host) and verify the corresponding route exists.</violation>
<violation number="2" location="docker-compose.yml:108">
P3: The MinIO service uses `minio/minio:latest` which is not pinned to a specific version. `latest` can change unexpectedly and break the compose setup. Consider pinning to a specific release tag (e.g., `minio/minio:RELEASE.2024-01-01T00-00-00Z`) for reproducible deployments.</violation>
</file>
<file name="packages/email/emails/policy-acknowledgment-digest.tsx">
<violation number="1" location="packages/email/emails/policy-acknowledgment-digest.tsx:65">
P3: The trailing-slash replacement applied to `getPortalBaseUrl()` is redundant because the helper already strips trailing slashes internally. The `.replace(//+$/, '')` wrapping on line 64 is guaranteed to be a no-op, which adds unnecessary indirection and makes the code slightly harder to read. Consider removing the outer `.replace()` and using the return value of `getPortalBaseUrl()` directly.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| if (baseUrl.includes('trycomp.ai')) { | ||
| return '.trycomp.ai'; | ||
| } | ||
| if (baseUrl.includes('dctrl.ai')) { |
There was a problem hiding this comment.
P1: Default self-hosted dctrl clients cannot complete cross-origin auth/CORS: comp-app.dctrl.ai and comp-portal.dctrl.ai receive the shared cookie but remain absent from Better Auth trustedOrigins and API CORS checks when AUTH_TRUSTED_ORIGINS is unset. Add the dctrl origins/pattern alongside this cookie-domain support, or require and provide AUTH_TRUSTED_ORIGINS in the deployment config.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/auth/auth.server.ts, line 45:
<comment>Default self-hosted dctrl clients cannot complete cross-origin auth/CORS: `comp-app.dctrl.ai` and `comp-portal.dctrl.ai` receive the shared cookie but remain absent from Better Auth `trustedOrigins` and API CORS checks when `AUTH_TRUSTED_ORIGINS` is unset. Add the dctrl origins/pattern alongside this cookie-domain support, or require and provide `AUTH_TRUSTED_ORIGINS` in the deployment config.</comment>
<file context>
@@ -42,6 +42,9 @@ function getCookieDomain(): string | undefined {
if (baseUrl.includes('trycomp.ai')) {
return '.trycomp.ai';
}
+ if (baseUrl.includes('dctrl.ai')) {
+ return '.dctrl.ai';
+ }
</file context>
| DATABASE_URL: postgresql://comp:comp@comp-postgres:5432/comp?sslmode=disable | ||
| PORTAL_URL: https://comp-portal.dctrl.ai | ||
| NEXT_PUBLIC_PORTAL_URL: https://comp-portal.dctrl.ai | ||
| TRUST_APP_URL: https://comp-portal.dctrl.ai |
There was a problem hiding this comment.
P1: Trust portal links generated by the API are pointed at the employee portal host. TRUST_APP_URL is consumed as the base for trust-site and access URLs, but this compose change sets it to https://comp-portal.dctrl.ai; recipients will be sent links such as https://comp-portal.dctrl.ai/<friendlyUrl>/access/<token>, which can route to the wrong application. The deployment should set TRUST_APP_URL to the actual trust portal URL (or leave it unset if PORTAL_URL is the intended trust host) and verify the corresponding route exists.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docker-compose.yml, line 65:
<comment>Trust portal links generated by the API are pointed at the employee portal host. `TRUST_APP_URL` is consumed as the base for trust-site and access URLs, but this compose change sets it to `https://comp-portal.dctrl.ai`; recipients will be sent links such as `https://comp-portal.dctrl.ai/<friendlyUrl>/access/<token>`, which can route to the wrong application. The deployment should set `TRUST_APP_URL` to the actual trust portal URL (or leave it unset if `PORTAL_URL` is the intended trust host) and verify the corresponding route exists.</comment>
<file context>
@@ -59,6 +60,10 @@ services:
DATABASE_URL: postgresql://comp:comp@comp-postgres:5432/comp?sslmode=disable
+ PORTAL_URL: https://comp-portal.dctrl.ai
+ NEXT_PUBLIC_PORTAL_URL: https://comp-portal.dctrl.ai
+ TRUST_APP_URL: https://comp-portal.dctrl.ai
+ NEXT_PUBLIC_APP_URL: https://comp-app.dctrl.ai
volumes:
</file context>
| TRUST_APP_URL: https://comp-portal.dctrl.ai | |
| TRUST_APP_URL: https://comp-trust.dctrl.ai |
| logging: *default-logging | ||
|
|
||
| minio: | ||
| image: minio/minio:latest |
There was a problem hiding this comment.
P3: The MinIO service uses minio/minio:latest which is not pinned to a specific version. latest can change unexpectedly and break the compose setup. Consider pinning to a specific release tag (e.g., minio/minio:RELEASE.2024-01-01T00-00-00Z) for reproducible deployments.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docker-compose.yml, line 108:
<comment>The MinIO service uses `minio/minio:latest` which is not pinned to a specific version. `latest` can change unexpectedly and break the compose setup. Consider pinning to a specific release tag (e.g., `minio/minio:RELEASE.2024-01-01T00-00-00Z`) for reproducible deployments.</comment>
<file context>
@@ -98,7 +103,42 @@ services:
logging: *default-logging
+
+ minio:
+ image: minio/minio:latest
+ container_name: comp-minio
+ command: server /data --console-address ":9001"
</file context>
| image: minio/minio:latest | |
| image: minio/minio:RELEASE.2024-05-10T22-45-00Z |
|
|
||
| const portalBase = ( | ||
| process.env.NEXT_PUBLIC_PORTAL_URL ?? 'https://portal.trycomp.ai' | ||
| getPortalBaseUrl() |
There was a problem hiding this comment.
P3: The trailing-slash replacement applied to getPortalBaseUrl() is redundant because the helper already strips trailing slashes internally. The .replace(//+$/, '') wrapping on line 64 is guaranteed to be a no-op, which adds unnecessary indirection and makes the code slightly harder to read. Consider removing the outer .replace() and using the return value of getPortalBaseUrl() directly.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/email/emails/policy-acknowledgment-digest.tsx, line 65:
<comment>The trailing-slash replacement applied to `getPortalBaseUrl()` is redundant because the helper already strips trailing slashes internally. The `.replace(//+$/, '')` wrapping on line 64 is guaranteed to be a no-op, which adds unnecessary indirection and makes the code slightly harder to read. Consider removing the outer `.replace()` and using the return value of `getPortalBaseUrl()` directly.</comment>
<file context>
@@ -61,7 +62,7 @@ export const PolicyAcknowledgmentDigestEmail = ({
const portalBase = (
- process.env.NEXT_PUBLIC_PORTAL_URL ?? 'https://portal.trycomp.ai'
+ getPortalBaseUrl()
).replace(/\/+$/, '');
const subjectText = computePolicyAcknowledgmentDigestSubject(orgsWithPolicies);
</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
Enables self-hosted deployments with BunMail/SMTP email, a Dockerized API, and reliable local Postgres. Adds MinIO-backed S3, dctrl.ai cookie/portal URL support, and smoother Docker/Prisma builds.
New Features
TRIGGER_SECRET_KEYis unset, with one‑click unsubscribe headers.apps/apiservice indocker-compose(port 3333) with healthchecks and DB/env wiring; propagatesNEXT_PUBLIC_API_URLto app and portal.APP_AWS_*,FLEET_AGENT_BUCKET_NAME) to MinIO..dctrl.aicookie domain and centralizes portal links viaPORTAL_URL/NEXT_PUBLIC_PORTAL_URLhelper; updates invite emails and UI links.packages/dbfirst, usestsxfor seeding, increases Node memory; app runs on 3001 and portal on 3002. Addsnodemailerand@types/nodemailertoapps/api.Migration
BUNMAIL_API_URL,BUNMAIL_API_KEY,BUNMAIL_FROM.SMTP_HOST,SMTP_PORT,SMTP_USER,SMTP_PASS,SMTP_SECURE,SMTP_FROM.RESEND_*vars are set.NEXT_PUBLIC_API_URLfor app/portal; setPORTAL_URL(self‑host) orNEXT_PUBLIC_PORTAL_URLfor portal links.comp-postgres; DB URL usessslmode=disableandPGSSLMODE=disable.comp-postgresis treated as local for Prisma TLS.TRIGGER_SECRET_KEYunset to send emails directly; set it to use the queued task path.minio/minio-initservices, or pointAPP_AWS_*to your S3 endpoint.comp_networkexists beforedocker compose up.Written for commit 734cd98. Summary will update on new commits.