feat(self-host): SMTP/BunMail email, Docker stack, and MinIO presigned URL fixes - #3543
feat(self-host): SMTP/BunMail email, Docker stack, and MinIO presigned URL fixes#3543raedbrahem wants to merge 7 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.
…al URL fixes Add optional SMTP and BunMail email transports with direct send when Trigger.dev is unset, extend docker-compose with API and optional MinIO profile, improve local Postgres/Docker build compatibility, and centralize portal links for self-hosted installs.
When S3/MinIO runs on an internal Docker hostname, presigned attachment URLs need a browser-reachable host. Use APP_AWS_PUBLIC_ENDPOINT for signing while keeping APP_AWS_ENDPOINT for server-side object access.
There was a problem hiding this comment.
1 issue found and verified against the latest diff
Confidence score: 3/5
- In
packages/db/src/ssl-config.ts, the hostname handling fix is not applied for framework-editor andcombine-schemas.js-emitted clients, socomp-postgrescan still be treated as remote and plain Docker Postgres/self-hosted connections may use the wrong SSL behavior and fail to connect — apply the same hostname classification logic across all client creation paths and add a regression test for those emitters.
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">
P2: Self-hosted connections made through the framework-editor client or a client emitted by `combine-schemas.js` still treat `comp-postgres` as remote, so this hostname fix is not applied consistently and plain Docker Postgres connections can fail TLS negotiation. Keeping the hostname classification synchronized in the generated-client template and every Prisma client would make the self-hosted behavior consistent.
(Based on your team's feedback about generated Prisma client configuration and coordinated call sites.)</violation>
</file>
Tip: instead of fixing issues one by one fix them all with cubic
Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.
Re-trigger cubic
- Fix SMTP attachment base64 decoding for Trigger task payloads - Remove global RESEND_TO_TEST redirect; reject scheduled sends without Trigger.dev - Fall back from BunMail when attachments/headers/scheduling are required - Share unsubscribe header builder; reuse getPortalBaseUrl in app/API - Stream device agent downloads via transformToWebStream - Require explicit MinIO credentials and bucket name; pin image tags - Use sslmode=disable in DATABASE_URL instead of hardcoded comp-postgres host - Install pinned tsx via Bun in migrator image
|
Thanks for the review — pushed commit 0df2e54 addressing the cubic feedback: Email transport
Portal URLs
Docker / MinIO
DB TLS
Other
CLA signing is still pending on my side — will complete that separately. |
There was a problem hiding this comment.
1 issue found across 16 files (changes from recent commits).
Confidence score: 5/5
- In
apps/api/src/email/send-batch-email.ts, unsubscribe URL/header construction is still duplicated instead of usingbuildUnsubscribeHeaders()fromapps/api/src/email/unsubscribe-headers.ts, which creates a drift risk where future updates could make batch emails behave differently for one-click unsubscribe and cause subtle compliance/user-experience inconsistencies — route batch sending through the shared helper to keep behavior centralized.
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/unsubscribe-headers.ts">
<violation number="1" location="apps/api/src/email/unsubscribe-headers.ts:3">
P3: The PR extracts buildUnsubscribeHeaders() to share one-click unsubscribe logic, but send-batch-email.ts still inlines the exact same URL/header construction instead of calling the new helper. Two copies of the unsubscribe URL logic now drift independently; recommend updating send-batch-email.ts to use buildUnsubscribeHeaders(email.to) so the shared helper is actually the single source of truth.</violation>
</file>
Tip: instead of fixing issues one by one fix them all with cubic
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| @@ -0,0 +1,12 @@ | |||
| import { generateUnsubscribeToken } from '@trycompai/email'; | |||
|
|
|||
| export function buildUnsubscribeHeaders(to: string): Record<string, string> { | |||
There was a problem hiding this comment.
P3: The PR extracts buildUnsubscribeHeaders() to share one-click unsubscribe logic, but send-batch-email.ts still inlines the exact same URL/header construction instead of calling the new helper. Two copies of the unsubscribe URL logic now drift independently; recommend updating send-batch-email.ts to use buildUnsubscribeHeaders(email.to) so the shared helper is actually the single source of truth.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/email/unsubscribe-headers.ts, line 3:
<comment>The PR extracts buildUnsubscribeHeaders() to share one-click unsubscribe logic, but send-batch-email.ts still inlines the exact same URL/header construction instead of calling the new helper. Two copies of the unsubscribe URL logic now drift independently; recommend updating send-batch-email.ts to use buildUnsubscribeHeaders(email.to) so the shared helper is actually the single source of truth.</comment>
<file context>
@@ -0,0 +1,12 @@
+import { generateUnsubscribeToken } from '@trycompai/email';
+
+export function buildUnsubscribeHeaders(to: string): Record<string, string> {
+ const apiBaseUrl =
+ process.env.NEXT_PUBLIC_API_URL || 'https://api.trycomp.ai';
</file context>
- Allow BunMail for standard triggerEmail flows (headers no longer block it) - Route scheduled sends to Resend only; reject when Resend is unavailable - Only override S3 presigning client when APP_AWS_PUBLIC_ENDPOINT is set - Validate MinIO env vars at container start, not compose parse time - Restore Node.js in migrator image for tsx/Prisma seed compatibility - Sync sslmode=disable handling in framework-editor and combine-schemas - Trim API base URL in unsubscribe helper; reuse in batch email task
|
Pushed
|
There was a problem hiding this comment.
All reported issues were addressed across 9 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
Co-authored-by: Cursor <cursoragent@cursor.com>
29652c5 to
7af047b
Compare
|
Pushed
|
Summary
Self-hosted deployment improvements from production use of Comp on Docker:
TRIGGER_SECRET_KEYis unset (no Trigger.dev required for basic email).apiservice, optional MinIO profile (docker compose --profile minio up), build args forNEXT_PUBLIC_API_URL/ portal URL, and local Postgres hostname support (comp-postgres).packages/dbsource; Prisma seed via Node/tsx; workspace package build order fixes.getPortalBaseUrl()helper so emails and invites respectPORTAL_URLon self-hosted installs.APP_AWS_PUBLIC_ENDPOINTfor browser-reachable presigned URLs when the internal endpoint is not reachable from clients.Motivation
The upstream Docker guide assumes Resend + Trigger.dev + external Postgres. These changes make a fully self-contained Docker stack workable for teams that prefer SMTP/BunMail and optional local MinIO, without changing cloud behavior when those env vars are unset.
Test plan
docker compose build app portal apisucceeds with exportedBETTER_AUTH_URL/NEXT_PUBLIC_API_URLbuild argsTRIGGER_SECRET_KEYunset andSMTP_HOSTset, invitation/policy emails send directlydocker compose --profile minio up, evidence upload and attachment download work whenAPP_AWS_ENDPOINT=http://minio:9000andAPP_AWS_PUBLIC_ENDPOINTpoints to a browser-reachable hostNotes
minioCompose profile so existing deployments are unaffected.Made with Cursor
Summary by cubic
Adds self‑hosted email and a fuller Docker stack: optional
BunMailREST andSMTPtransports with direct send whenTRIGGER_SECRET_KEYis unset,apiservice wired into Compose, optionalMinIO, and safer presigned URLs. Also centralizes portal links and respectssslmode=disablefor local Postgres across apps and tools.New Features
BunMailREST andSMTP; sends directly ifTRIGGER_SECRET_KEYis unset; scheduled sends route toResendonly (errors if unavailable); usesBunMailfor normal sends and falls back toSMTP/Resendfor attachments; adds one‑click List‑Unsubscribe headers.apiservice (port 3333) with health checks andNEXT_PUBLIC_API_URLbuild args; migrator seeds via Node/tsx; optionalminioprofile pins images and auto‑creates the bucket.APP_AWS_PUBLIC_ENDPOINTonly for signing browser‑reachable presigned URLs; portal agent downloads stream viatransformToWebStream().getPortalBaseUrl()used in app/API/emails; honorssslmode=disableinDATABASE_URLacross app, portal, API, framework editor, and DB tools.Migration
BUNMAIL_API_URL,BUNMAIL_API_KEY,BUNMAIL_FROM(takes priority), orSMTP_HOST,SMTP_PORT,SMTP_SECURE,SMTP_USER,SMTP_PASS,SMTP_FROM.docker compose --profile minio up; setAPP_AWS_ENDPOINT(internal),APP_AWS_PUBLIC_ENDPOINT(browser),MINIO_ROOT_USER,MINIO_ROOT_PASSWORD,APP_AWS_BUCKET_NAME.PORTAL_URLandNEXT_PUBLIC_API_URL.TRIGGER_SECRET_KEYunset for direct sends (no scheduling), or set it to enable scheduled emails viaResend/Trigger.dev.Written for commit 7af047b. Summary will update on new commits.