Skip to content

fix dashboard shared email provider display for self-hosted env config#1353

Open
TanishValesha wants to merge 1 commit intostack-auth:devfrom
TanishValesha:fix/shared-email-dashboard-env-display
Open

fix dashboard shared email provider display for self-hosted env config#1353
TanishValesha wants to merge 1 commit intostack-auth:devfrom
TanishValesha:fix/shared-email-dashboard-env-display

Conversation

@TanishValesha
Copy link
Copy Markdown

@TanishValesha TanishValesha commented Apr 20, 2026

Summary

This PR fixes a dashboard/config mismatch for shared email provider mode in self-hosted setups.

  • Shared email sending already uses STACK_EMAIL_* env vars at runtime.
  • Dashboard previously showed hard-coded shared sender text (noreply@stackframe.co), which was misleading.
  • Shared config now surfaces safe env-derived metadata and dashboard renders it with fallback text.

Root Cause

Two different paths existed:

  • Runtime send path (apps/backend/src/lib/emails.tsx) reads shared SMTP env vars directly.
  • Project config path (apps/backend/src/lib/config.tsx) returned only email_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

  • Backend config serialization
    • Updated shared email_config to include non-secret fields:
      • host from STACK_EMAIL_HOST
      • port from STACK_EMAIL_PORT
      • sender_email from STACK_EMAIL_SENDER
  • Template/admin mapping
    • Extended shared AdminEmailConfig shape to optionally include:
      • senderEmail, host, port
    • Mapped those fields in admin app implementation.
  • Dashboard UI
    • Replaced hard-coded shared sender display with env-driven value when available.
    • Added safe fallback text: Configured via STACK_EMAIL_SENDER.
    • Updated shared server labels/tooltips to Shared (environment-configured).

Security Considerations

  • Exposed only non-secret metadata for shared mode.
  • Did not expose shared credentials (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

  • Low-to-moderate risk.
  • Change is additive for shared config metadata and UI consumption.
  • Non-shared modes (managed/resend/custom SMTP) remain unchanged.
  • Dashboard includes fallback text to avoid runtime display breakage when metadata is absent.

Follow-up Validation (CI / reviewer environment)

Please rerun:

  • 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

with backend available on expected local test endpoint.

Issue

Closes #818

Summary by CodeRabbit

  • Bug Fixes

    • Fixed shared email configuration to properly utilize environment variables for host, port, and sender email instead of displaying placeholder values.
  • New Features

    • Updated UI labels to display "environment-configured" for shared email servers, reflecting actual runtime settings from environment variables.

Expose non-secret shared email metadata in project config and render it in dashboard instead of hard-coded shared sender text.
Copilot AI review requested due to automatic review settings April 20, 2026 13:38
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 20, 2026

@TanishValesha is attempting to deploy a commit to the Stack Auth Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 20, 2026

📝 Walkthrough

Walkthrough

The 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 STACK_EMAIL_HOST, STACK_EMAIL_PORT, and STACK_EMAIL_SENDER environment variables for shared email configurations.

Changes

Cohort / File(s) Summary
Backend Configuration
apps/backend/src/lib/config.tsx
Updated renderedOrganizationConfigToProjectCrud to populate shared email config with parsed runtime environment variables (STACK_EMAIL_HOST, STACK_EMAIL_PORT, STACK_EMAIL_SENDER) instead of emitting an empty shared type object.
Dashboard Email UI Components
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx, apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx
Updated shared email server labels from hardcoded noreply@stackframe.co to "environment-configured" text. Extended components to derive and display sharedSenderEmail from config, with fallback to environment variable reference. Updated tooltips and type selection labels accordingly.
Template Library Types & Implementation
packages/template/src/lib/stack-app/project-configs/index.ts, packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
Added optional fields (senderEmail, host, port) to AdminEmailConfig union's shared variant. Expanded email config mapping to include these fields from CRUD payload instead of using empty shared type object.

Poem

🐰 The shared emails now take flight,
With environment vars shining bright,
No more noreply hardcoded in stone,
The dashboard reads what was sown,
Config flows true, the bug takes flight! 📧✨

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title directly summarizes the main change: fixing dashboard display of shared email provider configuration for self-hosted environments.
Description check ✅ Passed The description is comprehensive, covering summary, root cause, specific changes across multiple files, security considerations, test results, and risk assessment.
Linked Issues check ✅ Passed The PR directly addresses issue #818 by surfacing environment-configured email settings in the dashboard and replacing hard-coded sender text with env-derived values.
Out of Scope Changes check ✅ Passed All changes are directly related to the linked issue #818: updating backend config serialization, admin mapping, and dashboard UI for shared email mode.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 20, 2026

Greptile Summary

This PR surfaces STACK_EMAIL_* env-var metadata (host, port, sender) in the shared-mode project config response and updates the dashboard to display env-derived values with a safe fallback text, fixing the misleading hard-coded sender display in self-hosted setups.

  • The new isShared branch in renderedOrganizationConfigToProjectCrud calls getEnvVariable("STACK_EMAIL_HOST"), getEnvVariable("STACK_EMAIL_PORT"), and getEnvVariable("STACK_EMAIL_SENDER") without a default value. This function throws if the variable is absent, turning what was previously a safe config read into a hard failure for any self-hosted operator who has isShared: true but has not set all three env vars.

Confidence Score: 3/5

Not 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

Filename Overview
apps/backend/src/lib/config.tsx New shared email_config serialization calls getEnvVariable() unconditionally, throwing if vars are absent; also risks NaN port propagation
packages/template/src/lib/stack-app/project-configs/index.ts AdminEmailConfig type extended with optional senderEmail, host, port for shared mode — type change is backwards-compatible and correct
packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts Maps new shared email fields from API response to SDK type correctly; standard path is unchanged
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx Displays env-derived sender email with correct fallback text; tooltip wording updated; no logic issues
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx EmailServerCard reads sharedSenderEmail with safe null-check fallback; server type label updated; no issues

Sequence Diagram

sequenceDiagram
    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"
Loading
Prompt To Fix All 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.

---

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

Comment on lines 1166 to 1171
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" ? {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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")),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 emailConfig check is redundant.

Once emailConfig.type === "shared" narrows the union, senderEmail is already a known (optional) property of that variant per the updated AdminEmailConfig definition. The in check 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: Duplicate sharedSenderEmail derivation across two dashboard files.

The same 5-line derivation is now inlined in both emails/page-client.tsx and email-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

📥 Commits

Reviewing files that changed from the base of the PR and between 6bc1836 and 2ff3bba.

📒 Files selected for processing (5)
  • apps/backend/src/lib/config.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-settings/domain-settings.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx
  • packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
  • packages/template/src/lib/stack-app/project-configs/index.ts

Comment on lines 1166 to +1170
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"),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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 -50

Repository: stack-auth/stack-auth

Length of output: 2214


🏁 Script executed:

sed -n '49,65p' packages/stack-shared/src/utils/env.tsx

Repository: 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 -60

Repository: 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 -80

Repository: 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 -A2

Repository: 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 -A5

Repository: 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.

Suggested change
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.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_config serialization to include host, port, and sender_email metadata.
  • 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.

Comment on lines 1166 to +1170
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"),
Copy link

Copilot AI Apr 20, 2026

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 uses AI. Check for mistakes.
email_config: renderedConfig.emails.server.isShared ? {
type: 'shared',
host: getEnvVariable("STACK_EMAIL_HOST"),
port: parseInt(getEnvVariable("STACK_EMAIL_PORT")),
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;
})(),

Copilot uses AI. Check for mistakes.
Comment on lines 1166 to +1170
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"),
Copy link

Copilot AI Apr 20, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +1168 to +1170
host: getEnvVariable("STACK_EMAIL_HOST"),
port: parseInt(getEnvVariable("STACK_EMAIL_PORT")),
sender_email: getEnvVariable("STACK_EMAIL_SENDER"),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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

Fix on Vercel

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] stack-auth dashboard does not recognize custom email provider

2 participants