-
Notifications
You must be signed in to change notification settings - Fork 513
fix dashboard shared email provider display for self-hosted env config #1353
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1165,6 +1165,9 @@ export const renderedOrganizationConfigToProjectCrud = (renderedConfig: Complete | |||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| email_config: renderedConfig.emails.server.isShared ? { | ||||||||||||||||||||||||||||||||||||||||||||
| type: 'shared', | ||||||||||||||||||||||||||||||||||||||||||||
| host: getEnvVariable("STACK_EMAIL_HOST"), | ||||||||||||||||||||||||||||||||||||||||||||
| port: parseInt(getEnvVariable("STACK_EMAIL_PORT")), | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
| 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; | |
| })(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.
Copilot
AI
Apr 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Copilot
AI
Apr 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
parseIntcan silently produceNaNIf
STACK_EMAIL_PORTis set but contains a non-integer value (e.g."465abc"or whitespace),parseInt(...)returnsNaN. ThisNaNvalue will be serialised and sent to the dashboard, where it may render as"NaN"in the port fields. Adding a validation guard or usingNumber()with aNumber.isIntegercheck would make the failure explicit rather than silent.Prompt To Fix With AI