fix dashboard shared email provider display for self-hosted env config#1353
fix dashboard shared email provider display for self-hosted env config#1353TanishValesha wants to merge 1 commit intostack-auth:devfrom
Conversation
Expose non-secret shared email metadata in project config and render it in dashboard instead of hard-coded shared sender text.
|
@TanishValesha is attempting to deploy a commit to the Stack Auth Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughThe changes fix recognition of custom email provider configuration via environment variables by updating backend config conversion logic and dashboard UI labels to properly display and handle Changes
Poem
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR surfaces
Confidence Score: 3/5Not safe to merge as-is — missing env vars would crash the project config endpoint for all shared-mode projects One P1 finding: the unconditional getEnvVariable() calls in the new shared branch will throw for self-hosted setups that have isShared=true but haven't configured the three STACK_EMAIL_* vars, breaking the project config read entirely. This is a regression on the exact deployments this PR targets. The rest of the changes (type extension, SDK mapping, dashboard fallbacks) are clean and correct. apps/backend/src/lib/config.tsx — the shared email_config block at lines 1166-1171 needs safe env-var access (process.env with undefined fallback) instead of getEnvVariable() which throws on missing vars Important Files Changed
Sequence DiagramsequenceDiagram
participant Dashboard
participant Backend as Backend API
participant Config as renderedOrganizationConfigToProjectCrud
participant Env as process.env / getEnvVariable
Dashboard->>Backend: GET /api/v1/internal/projects/:id
Backend->>Config: renderedOrganizationConfigToProjectCrud(renderedConfig)
alt isShared (NEW path)
Config->>Env: getEnvVariable("STACK_EMAIL_HOST") [throws if unset]
Config->>Env: getEnvVariable("STACK_EMAIL_PORT") [throws if unset]
Config->>Env: getEnvVariable("STACK_EMAIL_SENDER") [throws if unset]
Config-->>Backend: email_config { type:'shared', host, port, sender_email }
else standard / managed / resend
Config-->>Backend: email_config { type:'standard', ... }
end
Backend-->>Dashboard: ProjectsCrud Admin Read response
Dashboard->>Dashboard: sharedSenderEmail = emailConfig.senderEmail ?? null
Dashboard->>Dashboard: Display senderEmail ?? "Configured via STACK_EMAIL_SENDER"
Prompt To Fix All With AIThis is a comment left during a code review.
Path: apps/backend/src/lib/config.tsx
Line: 1166-1171
Comment:
**Unconditional throw breaks config reads when env vars are absent**
`getEnvVariable` throws `Missing environment variable: <name>` if the named variable is not set and no default is provided. Before this PR, the `isShared` branch returned only `{ type: 'shared' }` and never touched the env vars. After this PR every call to `renderedOrganizationConfigToProjectCrud` (which backs the `/api/v1/internal/projects` read endpoint) will throw for any project in shared-email mode that doesn't have all three of `STACK_EMAIL_HOST`, `STACK_EMAIL_PORT`, and `STACK_EMAIL_SENDER` defined. Self-hosted operators who haven't set these vars (or deliberately run without email) would find their entire project config endpoint broken after this change, even though no email-sending code was touched.
A safe approach is to fall back to `undefined`/`null` when the var is absent rather than throwing:
```typescript
email_config: renderedConfig.emails.server.isShared ? {
type: 'shared',
...(process.env.STACK_EMAIL_HOST ? { host: process.env.STACK_EMAIL_HOST } : {}),
...(process.env.STACK_EMAIL_PORT ? { port: parseInt(process.env.STACK_EMAIL_PORT) } : {}),
...(process.env.STACK_EMAIL_SENDER ? { sender_email: process.env.STACK_EMAIL_SENDER } : {}),
} : ...
```
Note: `getEnvVariable` cannot be used here with a fallback because an empty string is treated as "not set" by that utility; using `process.env` directly and checking truthiness is safer.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: apps/backend/src/lib/config.tsx
Line: 1169
Comment:
**`parseInt` can silently produce `NaN`**
If `STACK_EMAIL_PORT` is set but contains a non-integer value (e.g. `"465abc"` or whitespace), `parseInt(...)` returns `NaN`. This `NaN` value will be serialised and sent to the dashboard, where it may render as `"NaN"` in the port fields. Adding a validation guard or using `Number()` with a `Number.isInteger` check would make the failure explicit rather than silent.
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix dashboard shared email provider disp..." | Re-trigger Greptile |
| email_config: renderedConfig.emails.server.isShared ? { | ||
| type: 'shared', | ||
| host: getEnvVariable("STACK_EMAIL_HOST"), | ||
| port: parseInt(getEnvVariable("STACK_EMAIL_PORT")), | ||
| sender_email: getEnvVariable("STACK_EMAIL_SENDER"), | ||
| } : renderedConfig.emails.server.provider === "managed" ? { |
There was a problem hiding this comment.
Unconditional throw breaks config reads when env vars are absent
getEnvVariable throws Missing environment variable: <name> if the named variable is not set and no default is provided. Before this PR, the isShared branch returned only { type: 'shared' } and never touched the env vars. After this PR every call to renderedOrganizationConfigToProjectCrud (which backs the /api/v1/internal/projects read endpoint) will throw for any project in shared-email mode that doesn't have all three of STACK_EMAIL_HOST, STACK_EMAIL_PORT, and STACK_EMAIL_SENDER defined. Self-hosted operators who haven't set these vars (or deliberately run without email) would find their entire project config endpoint broken after this change, even though no email-sending code was touched.
A safe approach is to fall back to undefined/null when the var is absent rather than throwing:
email_config: renderedConfig.emails.server.isShared ? {
type: 'shared',
...(process.env.STACK_EMAIL_HOST ? { host: process.env.STACK_EMAIL_HOST } : {}),
...(process.env.STACK_EMAIL_PORT ? { port: parseInt(process.env.STACK_EMAIL_PORT) } : {}),
...(process.env.STACK_EMAIL_SENDER ? { sender_email: process.env.STACK_EMAIL_SENDER } : {}),
} : ...Note: getEnvVariable cannot be used here with a fallback because an empty string is treated as "not set" by that utility; using process.env directly and checking truthiness is safer.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/backend/src/lib/config.tsx
Line: 1166-1171
Comment:
**Unconditional throw breaks config reads when env vars are absent**
`getEnvVariable` throws `Missing environment variable: <name>` if the named variable is not set and no default is provided. Before this PR, the `isShared` branch returned only `{ type: 'shared' }` and never touched the env vars. After this PR every call to `renderedOrganizationConfigToProjectCrud` (which backs the `/api/v1/internal/projects` read endpoint) will throw for any project in shared-email mode that doesn't have all three of `STACK_EMAIL_HOST`, `STACK_EMAIL_PORT`, and `STACK_EMAIL_SENDER` defined. Self-hosted operators who haven't set these vars (or deliberately run without email) would find their entire project config endpoint broken after this change, even though no email-sending code was touched.
A safe approach is to fall back to `undefined`/`null` when the var is absent rather than throwing:
```typescript
email_config: renderedConfig.emails.server.isShared ? {
type: 'shared',
...(process.env.STACK_EMAIL_HOST ? { host: process.env.STACK_EMAIL_HOST } : {}),
...(process.env.STACK_EMAIL_PORT ? { port: parseInt(process.env.STACK_EMAIL_PORT) } : {}),
...(process.env.STACK_EMAIL_SENDER ? { sender_email: process.env.STACK_EMAIL_SENDER } : {}),
} : ...
```
Note: `getEnvVariable` cannot be used here with a fallback because an empty string is treated as "not set" by that utility; using `process.env` directly and checking truthiness is safer.
How can I resolve this? If you propose a fix, please make it concise.| email_config: renderedConfig.emails.server.isShared ? { | ||
| type: 'shared', | ||
| host: getEnvVariable("STACK_EMAIL_HOST"), | ||
| port: parseInt(getEnvVariable("STACK_EMAIL_PORT")), |
There was a problem hiding this comment.
parseInt can silently produce NaN
If STACK_EMAIL_PORT is set but contains a non-integer value (e.g. "465abc" or whitespace), parseInt(...) returns NaN. This NaN value will be serialised and sent to the dashboard, where it may render as "NaN" in the port fields. Adding a validation guard or using Number() with a Number.isInteger check would make the failure explicit rather than silent.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/backend/src/lib/config.tsx
Line: 1169
Comment:
**`parseInt` can silently produce `NaN`**
If `STACK_EMAIL_PORT` is set but contains a non-integer value (e.g. `"465abc"` or whitespace), `parseInt(...)` returns `NaN`. This `NaN` value will be serialised and sent to the dashboard, where it may render as `"NaN"` in the port fields. Adding a validation guard or using `Number()` with a `Number.isInteger` check would make the failure explicit rather than silent.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx (1)
367-371: Minor:"senderEmail" in emailConfigcheck is redundant.Once
emailConfig.type === "shared"narrows the union,senderEmailis already a known (optional) property of that variant per the updatedAdminEmailConfigdefinition. Theincheck adds noise and doesn't help the type narrowing. You can simplify to:- const sharedSenderEmail = project.config.emailConfig?.type === "shared" - && "senderEmail" in project.config.emailConfig - && typeof project.config.emailConfig.senderEmail === "string" - ? project.config.emailConfig.senderEmail - : null; + const sharedSenderEmail = project.config.emailConfig?.type === "shared" + ? project.config.emailConfig.senderEmail ?? null + : null;Same applies to the duplicated derivation in
emails/page-client.tsx.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx around lines 367 - 371, The `in` property check is unnecessary: simplify the sharedSenderEmail assignment by removing the `"senderEmail" in project.config.emailConfig` clause since `project.config.emailConfig?.type === "shared"` already narrows the union (per AdminEmailConfig) and guarantees the optional senderEmail property; update the expression in domain-settings.tsx (symbol: sharedSenderEmail) and the duplicated logic in emails/page-client.tsx to only check type === "shared" and typeof senderEmail === "string" before using it.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx (1)
62-66: DuplicatesharedSenderEmailderivation across two dashboard files.The same 5-line derivation is now inlined in both
emails/page-client.tsxandemail-settings/domain-settings.tsx. Consider extracting a tiny helper (e.g.getSharedSenderEmail(project)or a shared util) to keep the two call sites in sync when the fallback string or shape evolves. Also see the simplification suggestion on the sibling file.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/emails/page-client.tsx around lines 62 - 66, The sharedSenderEmail derivation is duplicated; extract it into a single helper (e.g. getSharedSenderEmail(project)) exported from a shared module (e.g. emailUtils) and replace the inline 5-line logic in both emails/page-client.tsx and email-settings/domain-settings.tsx with calls to that helper; ensure the helper checks project.config.emailConfig?.type === "shared", verifies "senderEmail" in the config and that it's a string, and returns the string or null so both call sites stay consistent when the fallback/shape changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/backend/src/lib/config.tsx`:
- Around line 1166-1170: The config code currently calls
getEnvVariable("STACK_EMAIL_HOST"/"STACK_EMAIL_PORT"/"STACK_EMAIL_SENDER")
without defaults which throws when those envs are empty; update the email_config
shared branch so it passes explicit defaults (use undefined) to getEnvVariable
for host and sender and for port parse the returned string with parseInt(...,
10) and guard NaN to produce undefined (i.e., call
getEnvVariable("STACK_EMAIL_PORT", undefined), then const port = parsed === NaN
? undefined : parsed), ensuring email_config construction (the email_config
object and its shared branch) yields undefined for missing values instead of
throwing.
---
Nitpick comments:
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx:
- Around line 367-371: The `in` property check is unnecessary: simplify the
sharedSenderEmail assignment by removing the `"senderEmail" in
project.config.emailConfig` clause since `project.config.emailConfig?.type ===
"shared"` already narrows the union (per AdminEmailConfig) and guarantees the
optional senderEmail property; update the expression in domain-settings.tsx
(symbol: sharedSenderEmail) and the duplicated logic in emails/page-client.tsx
to only check type === "shared" and typeof senderEmail === "string" before using
it.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/emails/page-client.tsx:
- Around line 62-66: The sharedSenderEmail derivation is duplicated; extract it
into a single helper (e.g. getSharedSenderEmail(project)) exported from a shared
module (e.g. emailUtils) and replace the inline 5-line logic in both
emails/page-client.tsx and email-settings/domain-settings.tsx with calls to that
helper; ensure the helper checks project.config.emailConfig?.type === "shared",
verifies "senderEmail" in the config and that it's a string, and returns the
string or null so both call sites stay consistent when the fallback/shape
changes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: bf4c8d56-4898-43ad-b1c8-a40fc3a101ef
📒 Files selected for processing (5)
apps/backend/src/lib/config.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsxpackages/template/src/lib/stack-app/apps/implementations/admin-app-impl.tspackages/template/src/lib/stack-app/project-configs/index.ts
| email_config: renderedConfig.emails.server.isShared ? { | ||
| type: 'shared', | ||
| host: getEnvVariable("STACK_EMAIL_HOST"), | ||
| port: parseInt(getEnvVariable("STACK_EMAIL_PORT")), | ||
| sender_email: getEnvVariable("STACK_EMAIL_SENDER"), |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check whether STACK_EMAIL_HOST/PORT/SENDER are set unconditionally in deployment/env configs.
rg -nP 'STACK_EMAIL_(HOST|PORT|SENDER)\b' -C2
# Verify getEnvVariable semantics
rg -nP --type=ts -C3 'export function getEnvVariable\b'Repository: stack-auth/stack-auth
Length of output: 7314
🏁 Script executed:
cat -n packages/stack-shared/src/utils/env.tsx | head -50Repository: stack-auth/stack-auth
Length of output: 2214
🏁 Script executed:
sed -n '49,65p' packages/stack-shared/src/utils/env.tsxRepository: stack-auth/stack-auth
Length of output: 443
🏁 Script executed:
# Check when isShared is set to true in email config
rg -n 'isShared' apps/backend/src/lib/config.tsx -B5 -A5 | head -60Repository: stack-auth/stack-auth
Length of output: 2856
🏁 Script executed:
# Look for where emails.server.isShared is actually set/determined
rg -n 'emails.server' apps/backend/src/lib/config.tsx -B3 -A3 | head -80Repository: stack-auth/stack-auth
Length of output: 1745
🏁 Script executed:
# Find where emails.server configuration is built/rendered
rg -n 'emails.*server' packages/stack-shared/src -A2 -B2 | grep -A5 -B5 'isShared'Repository: stack-auth/stack-auth
Length of output: 765
🏁 Script executed:
# Find where isShared is assigned/set for emails
rg -n 'isShared.*=' packages/stack-shared/src/config/schema.ts -B5 -A2Repository: stack-auth/stack-auth
Length of output: 1685
🏁 Script executed:
# Search for where isShared is computed in the config schema
rg -n 'isShared:' packages/stack-shared/src/config/schema.ts -B5 -A5Repository: stack-auth/stack-auth
Length of output: 3169
🏁 Script executed:
# Check if STACK_EMAIL_HOST/PORT/SENDER are set in any standard deployment configs
find . -name ".env*" -o -name "*.env" | head -20 | xargs grep -l "STACK_EMAIL" 2>/dev/null || echo "No .env files found with STACK_EMAIL"Repository: stack-auth/stack-auth
Length of output: 165
🏁 Script executed:
# Check contents of these env files for STACK_EMAIL variables
echo "=== apps/backend/.env ===" && cat apps/backend/.env | grep -i "STACK_EMAIL" || echo "Not found"
echo -e "\n=== apps/backend/.env.development ===" && cat apps/backend/.env.development | grep -i "STACK_EMAIL" || echo "Not found"
echo -e "\n=== docker/server/.env ===" && cat docker/server/.env | grep -i "STACK_EMAIL" || echo "Not found"Repository: stack-auth/stack-auth
Length of output: 3318
Critical: getEnvVariable throws when STACK_EMAIL_* are unset, breaking all config rendering in shared mode.
getEnvVariable(name) throws Missing environment variable: ... when the variable is absent or an empty string without a provided default (see packages/stack-shared/src/utils/env.tsx). The default email configuration sets isShared: true, so any self-hosted deployment without explicit STACK_EMAIL_HOST / STACK_EMAIL_PORT / STACK_EMAIL_SENDER values — including the provided docker/server/.env template which defines these variables as empty strings — will crash when rendering organization configs on the dashboard hot path.
The downstream UI already handles undefined values for these fields (shown in apps/dashboard/.../email-settings/domain-settings.tsx with fallback text "Configured via STACK_EMAIL_SENDER"). Pass explicit defaults or undefined instead of throwing. Additionally, parseInt lacks an explicit radix; a non-numeric STACK_EMAIL_PORT produces NaN, which then fails the admin read schema validation.
🛠️ Proposed fix
email_config: renderedConfig.emails.server.isShared ? {
type: 'shared',
- host: getEnvVariable("STACK_EMAIL_HOST"),
- port: parseInt(getEnvVariable("STACK_EMAIL_PORT")),
- sender_email: getEnvVariable("STACK_EMAIL_SENDER"),
+ host: getEnvVariable("STACK_EMAIL_HOST", "") || undefined,
+ port: (() => {
+ const raw = getEnvVariable("STACK_EMAIL_PORT", "");
+ if (!raw) return undefined;
+ const parsed = parseInt(raw, 10);
+ return Number.isFinite(parsed) ? parsed : undefined;
+ })(),
+ sender_email: getEnvVariable("STACK_EMAIL_SENDER", "") || undefined,
} : renderedConfig.emails.server.provider === "managed" ? {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| email_config: renderedConfig.emails.server.isShared ? { | |
| type: 'shared', | |
| host: getEnvVariable("STACK_EMAIL_HOST"), | |
| port: parseInt(getEnvVariable("STACK_EMAIL_PORT")), | |
| sender_email: getEnvVariable("STACK_EMAIL_SENDER"), | |
| email_config: renderedConfig.emails.server.isShared ? { | |
| type: 'shared', | |
| host: getEnvVariable("STACK_EMAIL_HOST", "") || undefined, | |
| port: (() => { | |
| const raw = getEnvVariable("STACK_EMAIL_PORT", ""); | |
| if (!raw) return undefined; | |
| const parsed = parseInt(raw, 10); | |
| return Number.isFinite(parsed) ? parsed : undefined; | |
| })(), | |
| sender_email: getEnvVariable("STACK_EMAIL_SENDER", "") || undefined, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/backend/src/lib/config.tsx` around lines 1166 - 1170, The config code
currently calls
getEnvVariable("STACK_EMAIL_HOST"/"STACK_EMAIL_PORT"/"STACK_EMAIL_SENDER")
without defaults which throws when those envs are empty; update the email_config
shared branch so it passes explicit defaults (use undefined) to getEnvVariable
for host and sender and for port parse the returned string with parseInt(...,
10) and guard NaN to produce undefined (i.e., call
getEnvVariable("STACK_EMAIL_PORT", undefined), then const port = parsed === NaN
? undefined : parsed), ensuring email_config construction (the email_config
object and its shared branch) yields undefined for missing values instead of
throwing.
There was a problem hiding this comment.
Pull request overview
Fixes a dashboard/config mismatch for shared email provider mode in self-hosted setups by surfacing env-derived (non-secret) shared email metadata to the dashboard instead of showing hard-coded Stack sender details.
Changes:
- Extend shared
email_configserialization to includehost,port, andsender_emailmetadata. - Update template/admin mapping types to carry optional shared metadata through to the dashboard.
- Update dashboard UI labels/tooltips and sender display for shared mode with env-driven value + fallback text.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/template/src/lib/stack-app/project-configs/index.ts | Extends AdminEmailConfig shared variant with optional senderEmail/host/port. |
| packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts | Maps shared email_config metadata from CRUD response into project.config.emailConfig. |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx | Uses shared sender metadata when available; updates shared labels/tooltips and removes hard-coded sender. |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx | Updates shared label and shared sender display to use metadata/fallback. |
| apps/backend/src/lib/config.tsx | Adds env-derived shared email metadata to serialized email_config. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| email_config: renderedConfig.emails.server.isShared ? { | ||
| type: 'shared', | ||
| host: getEnvVariable("STACK_EMAIL_HOST"), | ||
| port: parseInt(getEnvVariable("STACK_EMAIL_PORT")), | ||
| sender_email: getEnvVariable("STACK_EMAIL_SENDER"), |
There was a problem hiding this comment.
renderedOrganizationConfigToProjectCrud now calls getEnvVariable("STACK_EMAIL_*") whenever renderedConfig.emails.server.isShared is true. Since the default config sets emails.server.isShared: true (see packages/stack-shared/src/config/schema.ts:643-655), this can make every project config serialization throw (and potentially break the dashboard) in environments where STACK_EMAIL_* isn’t configured yet. Consider reading these env vars optionally (e.g. via process.env and omitting undefined/empty values) so the config endpoint remains usable, and let the UI fall back as intended.
| email_config: renderedConfig.emails.server.isShared ? { | ||
| type: 'shared', | ||
| host: getEnvVariable("STACK_EMAIL_HOST"), | ||
| port: parseInt(getEnvVariable("STACK_EMAIL_PORT")), |
There was a problem hiding this comment.
port: parseInt(getEnvVariable("STACK_EMAIL_PORT")) can yield NaN for invalid input, which would serialize to null in JSON and violate the expected number | undefined shape. It’d be safer to parse with Number.parseInt(..., 10) and explicitly validate Number.isFinite(port) (throwing a clear error if invalid) or omit the field when the env var is unset.
| port: parseInt(getEnvVariable("STACK_EMAIL_PORT")), | |
| port: (() => { | |
| const port = Number.parseInt(getEnvVariable("STACK_EMAIL_PORT"), 10); | |
| if (!Number.isFinite(port)) { | |
| throw new StackAssertionError("Invalid STACK_EMAIL_PORT: expected a finite number"); | |
| } | |
| return port; | |
| })(), |
| email_config: renderedConfig.emails.server.isShared ? { | ||
| type: 'shared', | ||
| host: getEnvVariable("STACK_EMAIL_HOST"), | ||
| port: parseInt(getEnvVariable("STACK_EMAIL_PORT")), | ||
| sender_email: getEnvVariable("STACK_EMAIL_SENDER"), |
There was a problem hiding this comment.
This new shared-email serialization behavior isn’t covered by the in-source vitest tests in this file. Adding a small test for renderedOrganizationConfigToProjectCrud that (1) includes STACK_EMAIL_HOST/PORT/SENDER when set, and (2) does not throw / omits fields when unset, would help prevent regressions.
| host: getEnvVariable("STACK_EMAIL_HOST"), | ||
| port: parseInt(getEnvVariable("STACK_EMAIL_PORT")), | ||
| sender_email: getEnvVariable("STACK_EMAIL_SENDER"), |
There was a problem hiding this comment.
| host: getEnvVariable("STACK_EMAIL_HOST"), | |
| port: parseInt(getEnvVariable("STACK_EMAIL_PORT")), | |
| sender_email: getEnvVariable("STACK_EMAIL_SENDER"), | |
| host: getEnvVariable("STACK_EMAIL_HOST", ""), | |
| port: parseInt(getEnvVariable("STACK_EMAIL_PORT", "0")), | |
| sender_email: getEnvVariable("STACK_EMAIL_SENDER", ""), |
Missing default values for getEnvVariable calls cause all project read endpoints to crash when shared email env vars are not configured
Summary
This PR fixes a dashboard/config mismatch for shared email provider mode in self-hosted setups.
STACK_EMAIL_*env vars at runtime.noreply@stackframe.co), which was misleading.Root Cause
Two different paths existed:
apps/backend/src/lib/emails.tsx) reads shared SMTP env vars directly.apps/backend/src/lib/config.tsx) returned onlyemail_config.type = "shared"with no sender/host/port, so dashboard had no real values and hard-coded Stack sender text.This made it look like env vars were ignored even when sending behavior was correct.
Changes
email_configto include non-secret fields:hostfromSTACK_EMAIL_HOSTportfromSTACK_EMAIL_PORTsender_emailfromSTACK_EMAIL_SENDERAdminEmailConfigshape to optionally include:senderEmail,host,portConfigured via STACK_EMAIL_SENDER.Shared (environment-configured).Security Considerations
username,password).Test Plan
Executed
pnpm -C packages/template typecheck✅Attempted (blocked by local infra)
pnpm test run apps/e2e/tests/backend/endpoints/api/v1/internal/config.test.ts❌pnpm test run apps/e2e/tests/backend/endpoints/api/v1/internal/projects.test.ts❌pnpm test run apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts❌Failure mode for all above:
ECONNREFUSED 127.0.0.1:8102(backend endpoint unavailable in local environment)Risk / Impact
Follow-up Validation (CI / reviewer environment)
Please rerun:
pnpm test run apps/e2e/tests/backend/endpoints/api/v1/internal/config.test.tspnpm test run apps/e2e/tests/backend/endpoints/api/v1/internal/projects.test.tspnpm test run apps/e2e/tests/backend/endpoints/api/v1/projects.test.tswith backend available on expected local test endpoint.
Issue
Closes #818
Summary by CodeRabbit
Bug Fixes
New Features