Skip to content

Email templates and project logos#852

Merged
BilalG1 merged 28 commits intodevfrom
email-templates
Nov 20, 2025
Merged

Email templates and project logos#852
BilalG1 merged 28 commits intodevfrom
email-templates

Conversation

@fomalhautb
Copy link
Contributor

@fomalhautb fomalhautb commented Aug 19, 2025

https://www.loom.com/share/7628a0b5f14e4367bcde93e4817a50e8

image

Important

Enhance email templates with project branding and dark-mode support, update schemas and API responses for new logo fields.

  • Behavior:
    • Email templates now support project branding with compact/full logos and dark-mode variants in email-rendering.tsx and emails.ts.
    • Project name displays beside logos in Light and Dark themes in email-rendering.tsx.
    • Email rendering groups branding and unsubscribe data for consistent theming.
  • API Changes:
    • Project payloads and admin/project schemas rename full_logo to logo_full and add dark-mode logo fields in projects.ts and schema-fields.ts.
    • Updates to projects.tsx and page-client.tsx to handle new logo fields.
  • Style:
    • Improved unsubscribe link contrast in Dark theme in emails.ts.
  • Database:
    • Adds logoFullUrl, logoDarkModeUrl, and logoFullDarkModeUrl columns to Project table in migration.sql.

This description was created by Ellipsis for 0fbb79d. You can customize this summary. It will automatically update as commits are pushed.


Summary by CodeRabbit

  • New Features

    • Added dark‑mode logo support for projects with separate logo and full‑logo variants.
    • Email themes/templates now render project logos and automatically select light/dark variants with fallbacks.
  • Refactor

    • Project logo fields and public payloads renamed/reorganized to support the new dark‑mode variants and consistent naming.
  • Tests

    • Updated test snapshots to reflect the new project logo fields.
  • Chores

    • Database migration applied to add/rename logo columns.

Note

Adds light/dark logo variants, renames full-logo fields, and passes project logos into email themes; updates DB, schemas, API, UI, and tests.

  • Database/Prisma:
    • Rename Project.fullLogoUrl to logoFullUrl; add logoDarkModeUrl and logoFullDarkModeUrl.
  • Backend:
    • Email rendering now accepts themeProps with projectLogos and spreads into EmailTheme (single/batched).
    • Provide project logo URLs to email render/send and internal template preview routes.
    • Add @stackframe/emails components (Logo, FullLogo, ProjectLogo) with light/dark fallbacks.
    • Projects CRUD: map/upload new logo fields; rename API fields to logo_full_url and add dark-mode fields.
  • Emails (themes):
    • Light/Dark themes render <ProjectLogo>; improve dark-theme unsubscribe link contrast.
  • Dashboard/UI:
    • Project settings support logoFullUrl, logoDarkModeUrl, logoFullDarkModeUrl uploads.
    • Code editor types: add ThemeProps.projectLogos, relax TS option, and Tailwind DTS fix.
  • Shared Schemas/Types:
    • Update schema-fields and CRUD read/update models to new/extra logo fields; propagate through template app types.
  • Tests:
    • Update snapshots for new project logo fields and theme source output.

Written by Cursor Bugbot for commit 4d97561. This will update automatically on new commits. Configure here.

@vercel
Copy link

vercel bot commented Aug 19, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
stack-backend Ready Ready Preview Comment Nov 19, 2025 11:40am
stack-dashboard Ready Ready Preview Comment Nov 19, 2025 11:40am
stack-demo Ready Ready Preview Comment Nov 19, 2025 11:40am
stack-docs Ready Ready Preview Comment Nov 19, 2025 11:40am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 19, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds dark-mode and full-logo variants to Project data and surfaces them through DB migrations, Prisma schema, API shapes, admin UI, editor types, email rendering, and tests. Email rendering now accepts a themeProps.projectLogos object and introduces ProjectLogo/Logo/FullLogo components for mode-aware logo selection.

Changes

Cohort / File(s) Summary
Database & Migration
apps/backend/prisma/schema.prisma, apps/backend/prisma/migrations/.../migration.sql
Rename fullLogoUrllogoFullUrl; add logoDarkModeUrl and logoFullDarkModeUrl columns.
Project Data Layer
apps/backend/src/lib/projects.tsx
Read/write mapping updated to expose/store logoUrl, logoFullUrl, logoDarkModeUrl, logoFullDarkModeUrl and emit logo_full_url, logo_dark_mode_url, logo_full_dark_mode_url in public shapes.
Email Rendering Core
apps/backend/src/lib/email-rendering.tsx, apps/backend/src/lib/emails.tsx
renderEmailWithTemplate and batched renderer now accept themeProps (wrapping unsubscribeLink and projectLogos); EmailTheme props composed from merged themeProps; new exported components Logo, FullLogo, ProjectLogo added to support light/dark variants.
Email Backend Routes
apps/backend/src/app/api/latest/emails/render-email/route.tsx, apps/backend/src/app/api/latest/emails/send-email/route.tsx, apps/backend/src/app/api/latest/internal/email-templates/[templateId]/route.tsx
Route handlers pass themeProps.projectLogos (sourced from tenancy.project fields) into renderEmailWithTemplate / render-and-send flows.
Email Theme Components
packages/stack-shared/src/helpers/emails.ts
Light/Dark EmailTheme signatures expanded to accept logo props; header rendering logic added with fallbacks for full/small logos and dark-mode styling; unsubscribe link color tweak for dark theme.
Types & Editor
apps/dashboard/src/components/vibe-coding/code-editor.tsx
Module augmentation extended: ThemeProps.projectLogos added; ProjectLogo declaration added; tailwind type transform and TS compiler option tweak included.
Admin & Template APIs
packages/template/src/lib/stack-app/projects/index.ts, .../apps/implementations/admin-app-impl.ts
AdminProject and update options renamed fullLogoUrllogoFullUrl and add logoDarkModeUrl, logoFullDarkModeUrl; CRUD payload mapping updated to emit new keys.
Schemas & CRUD Interfaces
packages/stack-shared/src/schema-fields.ts, packages/stack-shared/src/interface/crud/projects.ts
Rename projectFullLogoUrlSchemaprojectLogoFullUrlSchema; add projectLogoDarkModeUrlSchema, projectLogoFullDarkModeUrlSchema; CRUD read/update schemas updated to use logo_full_url, logo_dark_mode_url, logo_full_dark_mode_url.
Dashboard Settings UI
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx
UI inputs added for "Logo (Dark Mode)" and "Full Logo (Dark Mode)"; references updated from fullLogoUrl to logoFullUrl.
E2E Tests / Snapshots
apps/e2e/tests/backend/endpoints/api/v1/.../*.test.ts
Snapshots updated to remove full_logo_url and add logo_full_url, logo_dark_mode_url, logo_full_dark_mode_url; email-themes test imports/renders ProjectLogo.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Route as API Route
    participant DB as Project Store
    participant Renderer as Email Renderer
    participant Theme as EmailTheme / ProjectLogo

    Route->>DB: fetch project (logoUrl, logoFullUrl, logoDarkModeUrl, logoFullDarkModeUrl)
    DB-->>Route: return project logo fields
    Route->>Renderer: renderEmailWithTemplate(templateId, data, { themeProps: { unsubscribeLink, projectLogos } })
    Renderer->>Theme: mount EmailTheme with {...themeProps}
    Theme->>Theme: ProjectLogo selects variant by mode (light/dark) and chooses Logo/FullLogo
    Theme-->>Renderer: rendered email HTML with chosen logo
    Renderer-->>Route: return rendered HTML / payload
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Focus areas:
    • apps/backend/src/lib/email-rendering.tsx — merging themeProps across single and batched flows, previewMode behavior.
    • packages/stack-shared/src/helpers/emails.ts — fallback logic and dark-mode selection in header rendering.
    • Prisma migration & apps/backend/src/lib/projects.tsx — column rename mapping and new fields read/write correctness.
    • Snapshot updates across e2e tests — ensure expectations match public API changes.

Possibly related PRs

  • Project logo upload #817 — prior PR touching project logo fields and schema; strongly related to the rename/addition of logo fields.
  • sending new templates #794 — earlier changes to email rendering/theme selection that overlap with themeProps and ProjectLogo plumbing.
  • resend api key config #851 — touches the same project create/update codepath; related at the code-level for project logo handling.

"I hopped through code and fields so bright,
Four logos now dance in dark and light,
ProjectLogo picks the perfect view,
Emails wear banners both old and new,
Hooray — a rabbit's stamp of logo delight!" 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main changes: adding email template enhancements and project logo support with dark-mode variants.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering behavior changes, API modifications, database updates, and test changes, though it exceeds the minimal template requirements.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch email-templates

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6abb9ee and 4d97561.

📒 Files selected for processing (3)
  • apps/backend/prisma/schema.prisma (1 hunks)
  • packages/stack-shared/src/schema-fields.ts (1 hunks)
  • packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
  • packages/stack-shared/src/schema-fields.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Vercel Agent Review
  • GitHub Check: docker
  • GitHub Check: build (22.x)
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: build (22.x)
  • GitHub Check: setup-tests
  • GitHub Check: all-good
  • GitHub Check: restart-dev-and-test-with-custom-base-port
  • GitHub Check: build (22.x)
  • GitHub Check: restart-dev-and-test
  • GitHub Check: check_prisma_migrations (22.x)
🔇 Additional comments (1)
apps/backend/prisma/schema.prisma (1)

34-37: Migration correctly renames fullLogoUrllogoFullUrl with data preservation.

Verification confirms the migration file (apps/backend/prisma/migrations/20251114221105_project_logos/migration.sql) includes an explicit RENAME COLUMN "fullLogoUrl" TO "logoFullUrl" statement, which preserves existing data. The two new dark-mode logo fields are added correctly in the same migration. No data loss issue.


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.

@claude
Copy link
Contributor

claude bot commented Aug 19, 2025

Claude finished @fomalhautb's task —— View job


Pull Request Review

Reviewing the "Email templates with logo" changes for code quality, security, performance, and potential issues.

Todo List

  • Read and analyze all 5 changed files
  • Review code quality and best practices
  • Check for potential bugs and security issues
  • Assess performance considerations
  • Evaluate test coverage implications
  • Provide comprehensive feedback with specific references

Review Summary

This PR adds logo support to email templates by extending the theme props to include logoUrl and fullLogoUrl properties. The changes are well-structured and follow existing patterns in the codebase.

Positive Aspects

  1. Consistent Implementation: Logo support is properly threaded through all relevant layers:

    • API endpoints (render-email/route.tsx, send-email/route.tsx)
    • Email rendering logic (email-rendering.tsx)
    • TypeScript definitions (code-editor.tsx, emails.ts)
  2. Good Fallback Logic: The theme templates handle logo display gracefully:

    • fullLogoUrl takes precedence over logoUrl
    • Falls back to project display name when no logo is available
    • Proper conditional rendering prevents empty logo elements
  3. Type Safety: TypeScript definitions are properly updated in both the @stackframe/emails module declaration and ThemeProps interface.

⚠️ Issues & Recommendations

Code Quality Issues

  1. Duplicate Import Statement in packages/stack-shared/src/helpers/emails.ts:45-46:

    import { Html, Head, Tailwind, Body, Container, Link } from '@react-email/components';
    import { Html, Head, Tailwind, Body, Container, Link, Img } from '@react-email/components';

    The first import is redundant and should be removed.

  2. Missing Alt Text Validation: While alt attributes are provided, there's no validation that logoUrl and fullLogoUrl are valid URLs or accessible images. Consider adding basic URL validation.

Security Considerations

  1. URL Validation Missing: The logo URLs are passed directly to <Img> components without validation. While this follows existing patterns in the codebase, consider:
    • Adding URL format validation
    • Sanitizing URLs to prevent potential XSS (though React provides some protection)
    • Consider if there should be allowlisting of domains for logo URLs

Performance Considerations

  1. Image Loading: No optimization for image loading is implemented:

    • No lazy loading attributes
    • No width/height hints for layout stability
    • Consider adding image optimization hints
  2. Double Rendering: In send-email/route.tsx:107-115, when using template_id, the email is rendered twice - once to get the notification category and again for the final send. This is inefficient but appears to be existing behavior, not introduced by this PR.

Minor Issues

  1. Styling Inconsistency: The logo styling differs between light and dark themes:
    • Light theme: className="h-16" for full logo, className="h-8" for regular logo
    • Dark theme: Same classes but different text color handling
    • This is consistent but consider documenting the sizing choices

Test Coverage Recommendations

  1. Missing Test Scenarios:
    • Logo rendering with valid logoUrl
    • Logo rendering with valid fullLogoUrl
    • Fallback behavior when logos are undefined/null
    • Email rendering with both logo types simultaneously
    • Logo URL validation edge cases

Code Locations

  • Logo props added to theme props: apps/backend/src/app/api/latest/emails/render-email/route.tsx:58-62
  • Email sending with logo props: apps/backend/src/app/api/latest/emails/send-email/route.tsx:158-163
  • Theme template updates: packages/stack-shared/src/helpers/emails.ts:49-63, 87-101
  • TypeScript definitions: apps/dashboard/src/components/vibe-coding/code-editor.tsx:97-98

Recommended Actions

  1. Fix duplicate import in emails.ts:45-46
  2. Add basic URL validation for logo URLs
  3. Add tests for logo rendering scenarios
  4. Consider image optimization attributes for better performance
  5. Document logo sizing decisions in code comments

Overall, this is a solid implementation that follows established patterns. The main concern is the duplicate import that should be fixed before merging.


@claude
Copy link
Contributor

claude bot commented Aug 19, 2025

Claude finished @fomalhautb's task —— View job


Code Review Complete ✅

I've thoroughly reviewed PR #852 that adds logo support to email templates. Here's my comprehensive feedback:

Todo Checklist

  • Read and analyze all 5 changed files
  • Review code quality and best practices
  • Check for potential bugs and issues
  • Assess performance considerations
  • Identify security concerns
  • Evaluate test coverage needs
  • Provide comprehensive feedback

🔍 Review Summary

The implementation correctly adds logo support to email templates with proper type safety and consistent patterns. The changes are well-structured and follow existing codebase conventions.

Strengths

  1. Consistent Type Safety:

    • Proper TypeScript types added in code-editor.tsx:93-99 for ThemeProps
    • Consistent interface across all components
  2. Good Conditional Rendering Logic:

    • Smart fallback hierarchy: fullLogoUrllogoUrl → no logo in emails.ts:56-63, 94-101
    • Prevents broken image displays
  3. Proper Data Flow:

    • Logo URLs correctly passed from project data through the entire rendering chain
    • Consistent prop naming (logoUrl, fullLogoUrl, projectDisplayName)
  4. Security:

    • Uses null coalescing operator (??) to handle undefined values safely
    • No XSS risks introduced

⚠️ Issues & Concerns

Code Quality Issues

  1. Duplicate Import in emails.ts:45-46:

    import { Html, Head, Tailwind, Body, Container, Link } from '@react-email/components';
    import { Html, Head, Tailwind, Body, Container, Link, Img } from '@react-email/components';

    The first import is redundant and should be removed.

  2. Missing Alt Text Accessibility:

    • Hard-coded alt text "Logo" and "Full Logo" in emails.ts:58, 60, 95, 98
    • Should use dynamic alt text like alt={${projectDisplayName} logo} for better accessibility
  3. Inconsistent Styling:

    • Dark theme missing text color for project name: emails.ts:99 should have text-white class
    • Light theme h2 element lacks styling classes

Potential Bugs

  1. Unsafe Logo URL Handling:

    • No validation that logoUrl/fullLogoUrl are valid URLs
    • Could potentially break email rendering if invalid URLs are provided
    • Consider adding URL validation or error handling
  2. Missing Preview Props:

    • Email themes have PreviewProps but don't include logo URLs for preview mode
    • Should add preview logo URLs to emails.ts:78-80, 116-118

Performance Considerations

  1. Image Loading:

    • No lazy loading or size optimization for logo images
    • Consider adding loading="lazy" or size constraints
  2. Multiple Renderings:

    • In send-email/route.tsx:107-115, template is rendered twice when template_id is provided
    • Second rendering includes theme props that first doesn't have - this is intentional but worth noting

🧪 Test Coverage

Missing Test Scenarios:

  • Logo rendering with various URL formats
  • Fallback behavior when logos fail to load
  • Email rendering with missing/invalid logo URLs
  • Theme preview with logo props
  • XSS protection with malicious logo URLs

📋 Recommendations

  1. Fix duplicate import in emails.ts:45
  2. Improve accessibility with dynamic alt text
  3. Add logo URL validation or error handling
  4. Include logo URLs in preview props for better development experience
  5. Consider adding tests for logo rendering scenarios
  6. Add CSS classes for consistent styling in both themes

🎯 Overall Assessment

The implementation is solid and well-integrated with existing patterns. The main concerns are minor code quality issues and missing edge case handling. The feature works as intended and maintains type safety throughout the application.

Recommendation: ✅ Approve with minor fixes suggested


@recurseml
Copy link

recurseml bot commented Aug 19, 2025

Review by RecurseML

🔍 Review performed on 933c60d..7bce171

✨ No bugs found, your code is sparkling clean

✅ Files analyzed, no issues (5)

packages/stack-shared/src/helpers/emails.ts
apps/backend/src/lib/email-rendering.tsx
apps/backend/src/app/api/latest/emails/send-email/route.tsx
apps/backend/src/app/api/latest/emails/render-email/route.tsx
apps/dashboard/src/components/vibe-coding/code-editor.tsx

Need help? Join our Discord

`;

export const LightEmailTheme = `import { Html, Head, Tailwind, Body, Container, Link } from '@react-email/components';
import { Html, Head, Tailwind, Body, Container, Link, Img } from '@react-email/components';
Copy link
Contributor

Choose a reason for hiding this comment

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

Duplicate import in LightEmailTheme string literal: remove the redundant import and combine required modules (include Img in a single import).

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Greptile Summary

This review covers only the changes made since the last review (commit 7bce171), not the entire PR.

This PR adds logo support to Stack Auth's email template system, enabling projects to display their branding in email communications. The implementation introduces a new themeProps object structure that replaces the simple unsubscribeLink parameter across the email rendering pipeline.

Key changes include:

  1. Email Rendering Pipeline: Updated renderEmailWithTemplate() in email-rendering.tsx to accept a comprehensive themeProps object containing unsubscribeLink, projectDisplayName, logoUrl, and fullLogoUrl. This props object is properly spread to the EmailTheme component during rendering.

  2. API Route Updates: Both render-email/route.tsx and send-email/route.tsx now construct themeProps from the tenancy project configuration, extracting logo URLs and display names from tenancy.project.logo_url, tenancy.project.full_logo_url, and tenancy.project.display_name.

  3. Email Theme Templates: The LightEmailTheme and DarkEmailTheme in emails.ts now include conditional logo rendering logic. They prioritize displaying a full logo (fullLogoUrl) if available, otherwise show a smaller logo alongside the project name (logoUrl + projectDisplayName).

  4. Type Safety: Extended the ThemeProps type definition in code-editor.tsx to include the new optional logo properties, ensuring proper TypeScript intellisense for developers writing custom email templates.

The implementation maintains backward compatibility by making all logo properties optional and using null coalescing operators to handle missing values. The changes integrate well with the existing email rendering architecture, following established patterns for prop passing and theme customization.

Confidence score: 3/5

  • This PR has implementation issues that should be addressed before merging, particularly duplicate imports in email themes
  • Score reflects code quality concerns and potential runtime issues with malformed template strings
  • Pay close attention to packages/stack-shared/src/helpers/emails.ts which contains duplicate import statements that will cause compilation errors

5 files reviewed, 1 comment

Edit Code Review Bot Settings | Greptile

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
@claude
Copy link
Contributor

claude bot commented Aug 19, 2025

Claude finished @fomalhautb's task —— View job


Pull Request Review

Reviewing the "Email templates with logo" changes for code quality, security, performance, and potential issues.

Todo List

  • Read and analyze all 5 changed files
  • Review code quality and best practices
  • Check for potential bugs and security issues
  • Assess performance considerations
  • Evaluate test coverage implications
  • Provide comprehensive feedback with specific references

Review Summary

This PR adds logo support to email templates by extending the theme props to include logoUrl and fullLogoUrl properties. The changes are well-structured and follow existing patterns in the codebase.

Positive Aspects

  1. Consistent Implementation: Logo support is properly threaded through all relevant layers:

    • API endpoints (render-email/route.tsx, send-email/route.tsx)
    • Email rendering logic (email-rendering.tsx)
    • TypeScript definitions (code-editor.tsx, emails.ts)
  2. Good Fallback Logic: The theme templates handle logo display gracefully:

    • fullLogoUrl takes precedence over logoUrl
    • Falls back to project display name when no logo is available
    • Proper conditional rendering prevents empty logo elements
  3. Type Safety: TypeScript definitions are properly updated in both the @stackframe/emails module declaration and ThemeProps interface.

⚠️ Issues & Recommendations

Code Quality Issues

  1. Duplicate Import Statement in packages/stack-shared/src/helpers/emails.ts:45-46:

    import { Html, Head, Tailwind, Body, Container, Link } from '@react-email/components';
    import { Html, Head, Tailwind, Body, Container, Link, Img } from '@react-email/components';

    The first import is redundant and should be removed.

  2. Missing Alt Text Validation: While alt attributes are provided, there's no validation that logoUrl and fullLogoUrl are valid URLs or accessible images. Consider adding basic URL validation.

Security Considerations

  1. URL Validation Missing: The logo URLs are passed directly to <Img> components without validation. While this follows existing patterns in the codebase, consider:
    • Adding URL format validation
    • Sanitizing URLs to prevent potential XSS (though React provides some protection)
    • Consider if there should be allowlisting of domains for logo URLs

Performance Considerations

  1. Image Loading: No optimization for image loading is implemented:

    • No lazy loading attributes
    • No width/height hints for layout stability
    • Consider adding image optimization hints
  2. Double Rendering: In send-email/route.tsx:107-115, when using template_id, the email is rendered twice - once to get the notification category and again for the final send. This is inefficient but appears to be existing behavior, not introduced by this PR.

Minor Issues

  1. Styling Inconsistency: The logo styling differs between light and dark themes:
    • Light theme: className="h-16" for full logo, className="h-8" for regular logo
    • Dark theme: Same classes but different text color handling
    • This is consistent but consider documenting the sizing choices

Test Coverage Recommendations

  1. Missing Test Scenarios:
    • Logo rendering with valid logoUrl
    • Logo rendering with valid fullLogoUrl
    • Fallback behavior when logos are undefined/null
    • Email rendering with both logo types simultaneously
    • Logo URL validation edge cases

Code Locations

  • Logo props added to theme props: apps/backend/src/app/api/latest/emails/render-email/route.tsx:58-62
  • Email sending with logo props: apps/backend/src/app/api/latest/emails/send-email/route.tsx:158-163
  • Theme template updates: packages/stack-shared/src/helpers/emails.ts:49-63, 87-101
  • TypeScript definitions: apps/dashboard/src/components/vibe-coding/code-editor.tsx:97-98

Recommended Actions

  1. Fix duplicate import in emails.ts:45-46
  2. Add basic URL validation for logo URLs
  3. Add tests for logo rendering scenarios
  4. Consider image optimization attributes for better performance
  5. Document logo sizing decisions in code comments

Overall, this is a solid implementation that follows established patterns. The main concern is the duplicate import that should be fixed before merging.


Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
@claude
Copy link
Contributor

claude bot commented Aug 19, 2025

Claude finished @fomalhautb's task —— View job


Pull Request Review

Reviewing the "Email templates with logo" changes for code quality, security, performance, and potential issues.

Todo List

  • Read and analyze all 5 changed files
  • Review code quality and best practices
  • Check for potential bugs and security issues
  • Assess performance considerations
  • Evaluate test coverage implications
  • Provide comprehensive feedback with specific references

Review Summary

This PR adds logo support to email templates by extending the theme props to include logoUrl, fullLogoUrl, and projectDisplayName properties. The implementation is well-structured and follows existing patterns in the codebase.

Positive Aspects

  1. Consistent Implementation: Logo support is properly threaded through all relevant layers:

    • API endpoints (render-email/route.tsx:58-62, send-email/route.tsx:158-163)
    • Email rendering logic (email-rendering.tsx:52-57, 96)
    • TypeScript definitions (code-editor.tsx:93-99, emails.ts)
  2. Good Fallback Logic: The theme templates handle logo display gracefully:

    • fullLogoUrl takes precedence over logoUrl (emails.ts:56-63, 94-101)
    • Falls back to project display name when no logo is available
    • Proper conditional rendering prevents empty logo elements
  3. Type Safety: TypeScript definitions are properly updated in both the @stackframe/emails module declaration and ThemeProps interface.

⚠️ Issues & Recommendations

Critical Code Quality Issue

  1. Duplicate Import Statement in packages/stack-shared/src/helpers/emails.ts:45-47:
    export const LightEmailTheme = `import { Html, Head, Tailwind, Body, Container, Link, Img } from '@react-email/components';
    import { ThemeProps } from "@stackframe/emails"
    import { ThemeProps } from "@stackframe/emails"
    Issue: Line 47 duplicates the import from line 46. This will cause a compilation error.
    Fix: Remove the duplicate import on line 47.

Security Considerations

  1. URL Validation Missing: The logo URLs are passed directly to <Img> components without validation:
    • Consider adding basic URL format validation
    • While React provides some XSS protection, consider sanitizing URLs
    • Evaluate if there should be domain allowlisting for logo URLs

Performance Considerations

  1. Image Loading: No optimization for image loading is implemented:

    • No lazy loading attributes
    • No width/height hints for layout stability
    • Consider adding image optimization hints for better performance
  2. Double Rendering: In send-email/route.tsx:107-115, when using template_id, the email is rendered twice - once to get the notification category and again for the final send. This is existing behavior but worth noting for efficiency.

Minor Issues

  1. Preview Props Inconsistency: The PreviewProps in both themes (emails.ts:78-80, 116-118) only include unsubscribeLink but don't include the new logo properties. Consider adding preview logo URLs for better development experience.

  2. Alt Text: Hard-coded alt text "Logo" and "Full Logo" could be more descriptive. Consider using dynamic alt text like alt={\${projectDisplayName} logo`}` for better accessibility.

Test Coverage Recommendations

Missing Test Scenarios:

  • Logo rendering with valid logoUrl and fullLogoUrl
  • Fallback behavior when logos are undefined/null
  • Email rendering with both logo types simultaneously
  • Theme preview with logo props
  • Logo URL edge cases (invalid URLs, malformed data)

Specific Code References

  • Logo props added to render endpoint: apps/backend/src/app/api/latest/emails/render-email/route.tsx:58-62
  • Logo props added to send endpoint: apps/backend/src/app/api/latest/emails/send-email/route.tsx:158-163
  • Theme props extended in email rendering: apps/backend/src/lib/email-rendering.tsx:52-57, 96
  • TypeScript definitions updated: apps/dashboard/src/components/vibe-coding/code-editor.tsx:93-99
  • Template implementations: packages/stack-shared/src/helpers/emails.ts:49-63, 87-101

Recommendation

Approve with minor fixes required

The main blocker is the duplicate import that will cause compilation errors. Once fixed, this is a solid implementation that follows established patterns and maintains type safety throughout the application.

Priority Actions:

  1. Fix the duplicate import in emails.ts:47 (critical)
  2. Add logo URL validation (recommended)
  3. Consider adding preview logo props (nice to have)
  4. Add tests for logo scenarios (recommended)

Copy link
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: 0

♻️ Duplicate comments (2)
packages/stack-shared/src/helpers/emails.ts (1)

45-46: Duplicate imports inside LightEmailTheme string cause syntax error

There are two import lines for @react-email/components inside the template string. This will break bundling.

Apply this dedupe:

-export const LightEmailTheme = `import { Html, Head, Tailwind, Body, Container, Link } from '@react-email/components';
-import { Html, Head, Tailwind, Body, Container, Link, Img } from '@react-email/components';
+export const LightEmailTheme = `import { Html, Head, Tailwind, Body, Container, Link, Img } from '@react-email/components';
apps/backend/src/lib/email-rendering.tsx (1)

95-101: Fix: spreading possibly undefined themeProps throws at runtime; also set correct merge precedence

If options.themeProps is undefined, JSON.stringify(options.themeProps) becomes undefined and {...undefined} throws. Also, PreviewProps should provide defaults that call-site props can override, not the other way around.

Apply:

-        const themeProps = {
-          ...${JSON.stringify(options.themeProps)},
-          ...${previewMode ? "EmailTheme.PreviewProps" : "{}"},
-        }
+        const themeProps = {
+          ...${previewMode ? "EmailTheme.PreviewProps" : "{}"},
+          ...${JSON.stringify(options.themeProps || {})},
+        }

This unblocks first-pass renders (which don’t pass themeProps) in send-email and prevents PreviewProps from clobbering explicit props.

🧹 Nitpick comments (5)
packages/stack-shared/src/helpers/emails.ts (5)

47-47: Prefer type-only import for ThemeProps to avoid runtime import

Using a value import for a type may keep an unnecessary import after transpilation depending on settings.

-import { ThemeProps } from "@stackframe/emails"
+import type { ThemeProps } from "@stackframe/emails"

56-63: Add text fallback and better alt text for accessibility

If neither logo is set, nothing renders; also alt text should be descriptive.

-            {fullLogoUrl ? 
-              <Img src={fullLogoUrl} alt="Full Logo" className="h-16" /> :
-              logoUrl ? 
-                <div className="flex gap-2 items-center">
-                  <Img src={logoUrl} alt="Logo" className="h-8" />
-                  <h2>{projectDisplayName}</h2>
-                </div>
-                : null}
+            {fullLogoUrl ? (
+              <Img src={fullLogoUrl} alt={(projectDisplayName ? projectDisplayName + " " : "") + "full logo"} className="h-16" />
+            ) : logoUrl ? (
+              <div className="flex gap-2 items-center">
+                <Img src={logoUrl} alt={(projectDisplayName ? projectDisplayName + " " : "") + "logo"} className="h-8" />
+                <h2>{projectDisplayName}</h2>
+              </div>
+            ) : (
+              projectDisplayName ? <h2>{projectDisplayName}</h2> : null
+            )}

79-80: Consider enriching PreviewProps to showcase branding in preview

Optional: add sample projectDisplayName/logoUrl to improve preview experience in the editor.

 EmailTheme.PreviewProps = {
-  unsubscribeLink: "https://example.com",
+  unsubscribeLink: "https://example.com",
+  projectDisplayName: "Acme Inc.",
+  // logoUrl: "https://example.com/logo.png",
+  // fullLogoUrl: "https://example.com/full-logo.png",
 } satisfies Partial<ThemeProps>

84-85: Prefer type-only import for ThemeProps in Dark theme

Same rationale as Light theme.

-const DarkEmailTheme = `import { Html, Head, Tailwind, Body, Container, Link, Img } from '@react-email/components';
-import { ThemeProps } from "@stackframe/emails"
+const DarkEmailTheme = `import { Html, Head, Tailwind, Body, Container, Link, Img } from '@react-email/components';
+import type { ThemeProps } from "@stackframe/emails"

94-101: Dark theme: add text fallback and descriptive alt text

Mirror Light theme’s accessibility improvements.

-            {fullLogoUrl ? 
-              <Img src={fullLogoUrl} alt="Full Logo" className="h-16" /> :
-              logoUrl ? 
-                <div className="flex gap-2 items-center">
-                  <Img src={logoUrl} alt="Logo" className="h-8" />
-                  <h2 className="text-white">{projectDisplayName}</h2>
-                </div>
-                : null}
+            {fullLogoUrl ? (
+              <Img src={fullLogoUrl} alt={(projectDisplayName ? projectDisplayName + " " : "") + "full logo"} className="h-16" />
+            ) : logoUrl ? (
+              <div className="flex gap-2 items-center">
+                <Img src={logoUrl} alt={(projectDisplayName ? projectDisplayName + " " : "") + "logo"} className="h-8" />
+                <h2 className="text-white">{projectDisplayName}</h2>
+              </div>
+            ) : (
+              projectDisplayName ? <h2 className="text-white">{projectDisplayName}</h2> : null
+            )}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 933c60d and 46e8c98.

📒 Files selected for processing (5)
  • apps/backend/src/app/api/latest/emails/render-email/route.tsx (1 hunks)
  • apps/backend/src/app/api/latest/emails/send-email/route.tsx (2 hunks)
  • apps/backend/src/lib/email-rendering.tsx (3 hunks)
  • apps/dashboard/src/components/vibe-coding/code-editor.tsx (1 hunks)
  • packages/stack-shared/src/helpers/emails.ts (3 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
apps/backend/src/app/api/latest/**/*

📄 CodeRabbit Inference Engine (CLAUDE.md)

apps/backend/src/app/api/latest/**/*: Main API routes are located in /apps/backend/src/app/api/latest
The project uses a custom route handler system in the backend for consistent API responses

Files:

  • apps/backend/src/app/api/latest/emails/render-email/route.tsx
  • apps/backend/src/app/api/latest/emails/send-email/route.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: docker
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: setup-tests
  • GitHub Check: build (22.x)
  • GitHub Check: build (22.x)
  • GitHub Check: restart-dev-and-test
  • GitHub Check: all-good
  • GitHub Check: Security Check
🔇 Additional comments (7)
apps/backend/src/app/api/latest/emails/render-email/route.tsx (2)

58-62: Branding props wiring looks good

Passing projectDisplayName, logoUrl, and fullLogoUrl via themeProps aligns with the new renderer signature and EmailTheme expectations. No issues here.


58-62: Sanity-check logo URLs are absolute and reachable

If tenancy.project.logo_url/full_logo_url can be relative or point to private hosts, many email clients will fail to display them. Ensure these values are absolute, public URLs, or normalize/validate before passing through.

Would you like a guard that validates these URLs and drops invalid ones to avoid broken images?

apps/dashboard/src/components/vibe-coding/code-editor.tsx (1)

96-99: Type augmentation matches backend themeProps

Adding projectDisplayName, logoUrl, and fullLogoUrl to ThemeProps keeps Monaco typings in sync with the renderer and themes. Looks good.

apps/backend/src/app/api/latest/emails/send-email/route.tsx (3)

158-163: Good: themeProps passed with unsubscribe and branding

This aligns with the updated renderEmailWithTemplate signature and new EmailTheme props.


107-115: First-pass render doesn’t pass themeProps; relies on renderer to handle undefined

The first pass to compute notification category omits themeProps. With the current renderer implementation, object-spreading an undefined themeProps causes a runtime error. Ensure the renderer defaults themeProps to {} when absent (see email-rendering.tsx review).

Do you want me to open a follow-up to harden renderEmailWithTemplate and/or pass branding themeProps in the first pass too?


6-6: KnownErrors correctly re-exported at package root

The packages/stack-shared/src/index.ts file includes:

export {
  KnownError,
  KnownErrors
} from "./known-errors";

so importing KnownErrors from "@stackframe/stack-shared" will resolve successfully at runtime.

apps/backend/src/lib/email-rendering.tsx (1)

52-57: Public API extension is coherent

Adding themeProps with unsubscribeLink, projectDisplayName, logoUrl, and fullLogoUrl matches consuming routes and theme templates.

@fomalhautb fomalhautb assigned N2D4 and unassigned fomalhautb Sep 10, 2025
Copy link
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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f0cfc76 and 926a7e4.

📒 Files selected for processing (1)
  • apps/backend/src/lib/email-rendering.tsx (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Prefer ES6 Map over Record when representing key–value collections

Files:

  • apps/backend/src/lib/email-rendering.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: docker
  • GitHub Check: restart-dev-and-test
  • GitHub Check: build (22.x)
  • GitHub Check: build (22.x)
  • GitHub Check: all-good
  • GitHub Check: docker
  • GitHub Check: setup-tests
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: Security Check
🔇 Additional comments (2)
apps/backend/src/lib/email-rendering.tsx (2)

98-101: Children fallback logic looks good.

EmailTheme.PreviewProps?.children ?? EmailTemplateWithProps correctly prioritizes preview children without breaking default rendering.


51-56: Verify or create a shared EmailThemeProps type in stack-shared.

No matching exported EmailThemeProps (or similar) was found in packages/stack-shared. Confirm whether a shared props type exists (e.g. under helpers/emails), or define and export one, before replacing the inline definition with Partial<EmailThemeProps>.

@fomalhautb fomalhautb changed the title Email templates with logo Email templates and project logos Sep 10, 2025
Copy link
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: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/backend/src/lib/email-rendering.tsx (1)

135-135: Fix type assertion: subject and notificationCategory are optional.

Current cast contradicts the function’s return type and can hide missing fields.

-  return Result.ok(output.data.result as { html: string, text: string, subject: string, notificationCategory: string });
+  return Result.ok(output.data.result as { html: string, text: string, subject?: string, notificationCategory?: string });
apps/e2e/tests/backend/endpoints/api/v1/team-permissions.test.ts (1)

258-262: Update remaining legacy full_logo_url references in projects.tsx

  • In apps/backend/src/lib/projects.tsx, replace in logoFields array:
    'full_logo_url''logo_full_url'
  • In the same file (around lines 115 and 149), update property access:
    logoUrls.full_logo_urllogoUrls.logo_full_url
♻️ Duplicate comments (1)
apps/backend/src/lib/email-rendering.tsx (1)

95-98: Guard spread of PreviewProps to prevent runtime TypeError in preview mode.

Spreading EmailTheme.PreviewProps throws if it's undefined. Use nullish coalescing.

-        const themeProps = {
-          ...${JSON.stringify(options.themeProps || {})},
-          ...${previewMode ? "EmailTheme.PreviewProps" : "{}"},
-        }
+        const themeProps = {
+          ...${JSON.stringify(options.themeProps || {})},
+          ...${previewMode ? "(EmailTheme.PreviewProps ?? {})" : "{}"},
+        }
🧹 Nitpick comments (11)
apps/e2e/tests/backend/endpoints/api/v1/integrations/custom/projects/provision.test.ts (1)

80-86: Consistent field rename in provisioning snapshot.

Changed to logo_full_url as expected. Consider adding coverage for dark-mode logo fields when those are exposed by this endpoint.

packages/stack-shared/src/helpers/emails.ts (4)

45-50: Remove redundant ThemeProps import outside the template string.

There’s an extra import { ThemeProps } from "@stackframe/emails" outside the TSX string that isn’t used.

Apply:

 export const LightEmailTheme = `import { Html, Head, Tailwind, Body, Container, Link, Img } from '@react-email/components';
 import { ThemeProps } from "@stackframe/emails"
-import { ThemeProps } from "@stackframe/emails"

56-64: Add header fallback when no logos are provided (show project name).

Currently nothing renders if both logoFullUrl and logoUrl are absent. Show projectDisplayName alone to keep consistent branding.

Apply:

-            {logoFullUrl ? 
-              <Img src={logoFullUrl} alt="Full Logo" className="h-16" /> :
-              logoUrl ? 
-                <div className="flex gap-2 items-center">
-                  <Img src={logoUrl} alt="Logo" className="h-8" />
-                  <h2>{projectDisplayName}</h2>
-                </div>
-                : null}
+            {logoFullUrl ? (
+              <Img
+                src={logoFullUrl}
+                alt={projectDisplayName ? `${projectDisplayName} full logo` : "Project full logo"}
+                className="h-16"
+                height={64}
+              />
+            ) : logoUrl ? (
+              <div className="flex gap-2 items-center">
+                <Img
+                  src={logoUrl}
+                  alt={projectDisplayName ? `${projectDisplayName} logo` : "Project logo"}
+                  className="h-8"
+                  height={32}
+                />
+                {projectDisplayName ? <h2>{projectDisplayName}</h2> : null}
+              </div>
+            ) : (
+              projectDisplayName ? <h2>{projectDisplayName}</h2> : null
+            )}

94-101: Mirror the no-logo fallback in Dark theme.

Same rationale as Light theme; also keep contrast for heading.

Apply:

-            {logoFullDarkModeUrl ? 
-              <Img src={logoFullDarkModeUrl} alt="Full Logo" className="h-16" /> :
-              logoDarkModeUrl ? 
-                <div className="flex gap-2 items-center">
-                  <Img src={logoDarkModeUrl} alt="Logo" className="h-8" />
-                  <h2 className="text-white">{projectDisplayName}</h2>
-                </div>
-                : null}
+            {logoFullDarkModeUrl ? (
+              <Img
+                src={logoFullDarkModeUrl}
+                alt={projectDisplayName ? `${projectDisplayName} full logo` : "Project full logo"}
+                className="h-16"
+                height={64}
+              />
+            ) : logoDarkModeUrl ? (
+              <div className="flex gap-2 items-center">
+                <Img
+                  src={logoDarkModeUrl}
+                  alt={projectDisplayName ? `${projectDisplayName} logo` : "Project logo"}
+                  className="h-8"
+                  height={32}
+                />
+                {projectDisplayName ? <h2 className="text-white">{projectDisplayName}</h2> : null}
+              </div>
+            ) : (
+              projectDisplayName ? <h2 className="text-white">{projectDisplayName}</h2> : null
+            )}

106-108: Minor a11y: clarify unsubscribe link text.

“Click here to unsubscribe” is fine; consider a clearer label for screen readers.

Apply:

-              <Link href={unsubscribeLink} className="text-gray-300">Click here{" "}</Link>
-              to unsubscribe from these emails
+              <Link href={unsubscribeLink} className="text-gray-300" aria-label="Unsubscribe from these emails">
+                Unsubscribe
+              </Link>
apps/backend/src/app/api/latest/emails/send-email/route.tsx (1)

159-166: ThemeProps wiring LGTM

Passing branding + unsubscribe via themeProps is consistent with the renderer change.

For consistency, consider passing the same themeProps (minus unsubscribeLink) in the first render pass used to detect the notification category to avoid divergence if templates start referencing branding in that branch.

packages/template/src/lib/stack-app/projects/index.ts (1)

58-111: Avoid sending an all-undefined config payload

adminProjectUpdateOptionsToCrud always includes a config object, mostly with undefined values when options.config is not provided. Prefer omitting config entirely to reduce downstream branching and accidental overrides.

 export function adminProjectUpdateOptionsToCrud(options: AdminProjectUpdateOptions): ProjectsCrud["Admin"]["Update"] {
-  return {
-    display_name: options.displayName,
-    description: options.description,
-    is_production_mode: options.isProductionMode,
-    logo_url: options.logoUrl,
-    logo_full_url: options.logoFullUrl,
-    logo_dark_mode_url: options.logoDarkModeUrl,
-    logo_full_dark_mode_url: options.logoFullDarkModeUrl,
-    config: {
+  const config = options.config ? {
       domains: options.config?.domains?.map((d) => ({
         domain: d.domain,
         handler_path: d.handlerPath
       })),
       oauth_providers: options.config?.oauthProviders?.map((p) => ({
         id: p.id as any,
         type: p.type,
         ...(p.type === 'standard' && {
           client_id: p.clientId,
           client_secret: p.clientSecret,
           facebook_config_id: p.facebookConfigId,
           microsoft_tenant_id: p.microsoftTenantId,
         }),
       })),
       email_config: options.config?.emailConfig && (
         options.config.emailConfig.type === 'shared' ? {
           type: 'shared',
         } : {
           type: 'standard',
           host: options.config.emailConfig.host,
           port: options.config.emailConfig.port,
           username: options.config.emailConfig.username,
           password: options.config.emailConfig.password,
           sender_name: options.config.emailConfig.senderName,
           sender_email: options.config.emailConfig.senderEmail,
         }
       ),
       email_theme: options.config?.emailTheme,
       sign_up_enabled: options.config?.signUpEnabled,
       credential_enabled: options.config?.credentialEnabled,
       magic_link_enabled: options.config?.magicLinkEnabled,
       passkey_enabled: options.config?.passkeyEnabled,
       allow_localhost: options.config?.allowLocalhost,
       create_team_on_sign_up: options.config?.createTeamOnSignUp,
       client_team_creation_enabled: options.config?.clientTeamCreationEnabled,
       client_user_deletion_enabled: options.config?.clientUserDeletionEnabled,
       team_creator_default_permissions: options.config?.teamCreatorDefaultPermissions,
       team_member_default_permissions: options.config?.teamMemberDefaultPermissions,
       user_default_permissions: options.config?.userDefaultPermissions,
       oauth_account_merge_strategy: options.config?.oauthAccountMergeStrategy,
       allow_user_api_keys: options.config?.allowUserApiKeys,
       allow_team_api_keys: options.config?.allowTeamApiKeys,
-    },
-  };
+    }
+  } : undefined;
+
+  return {
+    display_name: options.displayName,
+    description: options.description,
+    is_production_mode: options.isProductionMode,
+    logo_url: options.logoUrl,
+    logo_full_url: options.logoFullUrl,
+    logo_dark_mode_url: options.logoDarkModeUrl,
+    logo_full_dark_mode_url: options.logoFullDarkModeUrl,
+    ...(config ? { config } : {}),
+  };
 }
apps/backend/src/lib/projects.tsx (2)

93-100: Upload logo variants in parallel and improve typing

Parallelize uploads and use a precise type for the four keys (or a Map, per guidelines) instead of Record<string,...>.

-  const logoFields = ['logo_url', 'full_logo_url', 'logo_dark_mode_url', 'logo_full_dark_mode_url'] as const;
-  const logoUrls: Record<string, string | null | undefined> = {};
-
-  for (const field of logoFields) {
-    if (options.data[field] !== undefined) {
-      logoUrls[field] = await uploadAndGetUrl(options.data[field], "project-logos");
-    }
-  }
+  const logoFields = ['logo_url', 'full_logo_url', 'logo_dark_mode_url', 'logo_full_dark_mode_url'] as const;
+  type LogoField = typeof logoFields[number];
+  const entries = await Promise.all(
+    logoFields.map<Promise<[LogoField, string | null | undefined]>>(async (field) => {
+      return [field, options.data[field] !== undefined ? await uploadAndGetUrl(options.data[field], "project-logos") : undefined];
+    })
+  );
+  const logoUrls = Object.fromEntries(entries) as Partial<Record<LogoField, string | null | undefined>>;

114-118: Map access to typed keys

Switching to the typed object (or a Map) avoids stringly-typed key access.

-          logoUrl: logoUrls.logo_url,
-          logoFullUrl: logoUrls.full_logo_url,
-          logoDarkModeUrl: logoUrls.logo_dark_mode_url,
-          logoFullDarkModeUrl: logoUrls.logo_full_dark_mode_url,
+          logoUrl: logoUrls.logo_url,
+          logoFullUrl: logoUrls.full_logo_url,
+          logoDarkModeUrl: logoUrls.logo_dark_mode_url,
+          logoFullDarkModeUrl: logoUrls.logo_full_dark_mode_url,

(kept identical keys; relies on the typed change above)

Also applies to: 148-152

apps/backend/src/lib/email-rendering.tsx (1)

100-102: Avoid passing 'children' via spread; rely on explicit children composition.

If PreviewProps.children exists, it’s redundantly passed in props and as explicit children. Strip it in the merge to reduce ambiguity.

-        const themeProps = {
-          ...${JSON.stringify(options.themeProps || {})},
-          ...${previewMode ? "(EmailTheme.PreviewProps ?? {})" : "{}"},
-        }
+        const __previewProps = ${previewMode ? "(EmailTheme.PreviewProps ?? {})" : "{}"};
+        const { children: __ignoredChildren, ...__previewPropsSansChildren } = __previewProps;
+        const themeProps = {
+          ...${JSON.stringify(options.themeProps || {})},
+          ...__previewPropsSansChildren,
+        }
apps/e2e/tests/backend/endpoints/api/v1/team-permissions.test.ts (1)

230-267: Optionally assert the legacy field is absent to guard against regressions.

Keeps the contract strict while retaining the inline snapshot.

   expect(response2).toMatchInlineSnapshot(`
     NiceResponse {
       "status": 200,
       "body": {
         "config": {
@@
         "owner_team_id": "<stripped UUID>",
       },
       "headers": Headers { <some fields may have been hidden> },
     }
   `);
+  // Ensure deprecated field isn't accidentally returned
+  expect(response2.body).not.toHaveProperty("full_logo_url");
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 926a7e4 and 9e412ae.

📒 Files selected for processing (22)
  • apps/backend/prisma/migrations/20250801204029_logo_url/migration.sql (1 hunks)
  • apps/backend/prisma/schema.prisma (1 hunks)
  • apps/backend/src/app/api/latest/emails/render-email/route.tsx (1 hunks)
  • apps/backend/src/app/api/latest/emails/send-email/route.tsx (1 hunks)
  • apps/backend/src/lib/email-rendering.tsx (2 hunks)
  • apps/backend/src/lib/projects.tsx (5 hunks)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx (1 hunks)
  • apps/dashboard/src/components/vibe-coding/code-editor.tsx (1 hunks)
  • apps/e2e/tests/backend/endpoints/api/v1/email-themes.test.ts (2 hunks)
  • apps/e2e/tests/backend/endpoints/api/v1/integrations/custom/projects/provision.test.ts (1 hunks)
  • apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/oauth-providers.test.ts (1 hunks)
  • apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/current.test.ts (3 hunks)
  • apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/provision.test.ts (2 hunks)
  • apps/e2e/tests/backend/endpoints/api/v1/internal/projects.test.ts (11 hunks)
  • apps/e2e/tests/backend/endpoints/api/v1/project-permissions.test.ts (1 hunks)
  • apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts (16 hunks)
  • apps/e2e/tests/backend/endpoints/api/v1/team-permissions.test.ts (1 hunks)
  • packages/stack-shared/src/helpers/emails.ts (3 hunks)
  • packages/stack-shared/src/interface/crud/projects.ts (2 hunks)
  • packages/stack-shared/src/schema-fields.ts (1 hunks)
  • packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts (1 hunks)
  • packages/template/src/lib/stack-app/projects/index.ts (3 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Prefer ES6 Map over Record when representing key–value collections

Files:

  • packages/stack-shared/src/interface/crud/projects.ts
  • apps/e2e/tests/backend/endpoints/api/v1/integrations/custom/projects/provision.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/team-permissions.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/oauth-providers.test.ts
  • packages/stack-shared/src/schema-fields.ts
  • apps/e2e/tests/backend/endpoints/api/v1/internal/projects.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx
  • apps/dashboard/src/components/vibe-coding/code-editor.tsx
  • apps/backend/src/app/api/latest/emails/send-email/route.tsx
  • packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
  • apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/current.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/project-permissions.test.ts
  • apps/backend/src/app/api/latest/emails/render-email/route.tsx
  • packages/template/src/lib/stack-app/projects/index.ts
  • apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/provision.test.ts
  • packages/stack-shared/src/helpers/emails.ts
  • apps/backend/src/lib/projects.tsx
  • apps/backend/src/lib/email-rendering.tsx
  • apps/e2e/tests/backend/endpoints/api/v1/email-themes.test.ts
**/*.test.{ts,tsx,js}

📄 CodeRabbit inference engine (AGENTS.md)

In tests, prefer .toMatchInlineSnapshot where possible; refer to snapshot-serializer.ts for snapshot formatting and handling of non-deterministic values

Files:

  • apps/e2e/tests/backend/endpoints/api/v1/integrations/custom/projects/provision.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/team-permissions.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/oauth-providers.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/internal/projects.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/current.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/project-permissions.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/provision.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/email-themes.test.ts
{apps/dashboard,apps/dev-launchpad,packages/stack-ui,packages/react}/**/*.{tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

For blocking alerts and errors in UI, do not use toast notifications; use alerts instead

Files:

  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx
  • apps/dashboard/src/components/vibe-coding/code-editor.tsx
{apps/dashboard,apps/dev-launchpad,packages/stack-ui,packages/react}/**/*.{tsx,jsx,css}

📄 CodeRabbit inference engine (AGENTS.md)

Keep hover/click animations snappy; avoid pre-transition delays on hover and apply transitions after the action (e.g., fade-out on hover end)

Files:

  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx
  • apps/dashboard/src/components/vibe-coding/code-editor.tsx
apps/backend/src/app/api/latest/**

📄 CodeRabbit inference engine (AGENTS.md)

apps/backend/src/app/api/latest/**: Organize backend API routes by resource under /api/latest (e.g., auth at /api/latest/auth/, users at /api/latest/users/, teams at /api/latest/teams/, oauth providers at /api/latest/oauth-providers/)
Use the custom route handler system in the backend to ensure consistent API responses

Files:

  • apps/backend/src/app/api/latest/emails/send-email/route.tsx
  • apps/backend/src/app/api/latest/emails/render-email/route.tsx
packages/template/**

📄 CodeRabbit inference engine (AGENTS.md)

When modifying the SDK copies, make changes in packages/template (source of truth)

Files:

  • packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
  • packages/template/src/lib/stack-app/projects/index.ts
🧬 Code graph analysis (2)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx (1)
apps/dashboard/src/components/logo-upload.tsx (1)
  • LogoUpload (20-135)
apps/backend/src/lib/projects.tsx (1)
apps/backend/src/s3.tsx (1)
  • uploadAndGetUrl (86-105)
🪛 GitHub Actions: Run setup tests
packages/template/src/lib/stack-app/projects/index.ts

[error] 65-65: TypeScript error TS2353: Object literal may only specify known properties; 'logo_dark_mode_url' does not exist on the Project type.

🪛 GitHub Actions: Runs E2E API Tests with external source of truth
packages/template/src/lib/stack-app/projects/index.ts

[error] 65-65: TS2353: Object literal may only specify known properties; 'logo_dark_mode_url' does not exist in the project type. (src/lib/stack-app/projects/index.ts:65,5).

🪛 GitHub Actions: Lint & build
packages/template/src/lib/stack-app/projects/index.ts

[error] 65-65: TS2353: Object literal may only specify known properties; 'logo_dark_mode_url' does not exist in the type for the project config.

🪛 GitHub Actions: Runs E2E API Tests
packages/template/src/lib/stack-app/projects/index.ts

[error] 65-65: TS2353: Object literal may only specify known properties, and 'logo_dark_mode_url' does not exist in the type for a stack-app project's configuration.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: all-good
  • GitHub Check: restart-dev-and-test
  • GitHub Check: docker
  • GitHub Check: docker
  • GitHub Check: Security Check
🔇 Additional comments (12)
apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/oauth-providers.test.ts (1)

124-133: Verify removal of all full_logo_url/fullLogoUrl references
Run

grep -RIn --exclude-dir={node_modules,dist,build} -E '(full_logo_url|fullLogoUrl)' .

to confirm no stale uses remain in code, docs or OpenAPI.

apps/dashboard/src/components/vibe-coding/code-editor.tsx (1)

96-101: Type surface matches new branding props.

Extension of ThemeProps for editor typings looks good and aligned with renderer changes.

apps/backend/prisma/migrations/20250801204029_logo_url/migration.sql (1)

5-11: Migration properly adds the new logo fields.

The migration correctly adds the four new logo-related columns to the Project table:

  • logoFullUrl (renamed from the original fullLogoUrl)
  • logoDarkModeUrl (new dark mode logo)
  • logoFullDarkModeUrl (new dark mode full logo)

All columns are properly defined as nullable TEXT fields, which is appropriate for optional logo URLs.

apps/e2e/tests/backend/endpoints/api/v1/internal/projects.test.ts (1)

95-95: Test snapshots consistently updated for API field rename.

All inline snapshots have been properly updated to reflect the API field rename from full_logo_url to logo_full_url. The changes are consistent across all test cases and maintain the same null values, indicating this is purely a field name update without any functional changes.

Also applies to: 148-148, 204-204, 251-251, 312-312, 375-375, 421-421, 473-473, 514-514, 565-565, 619-619

apps/e2e/tests/backend/endpoints/api/v1/project-permissions.test.ts (1)

240-240: Test snapshot properly reflects API field rename.

The snapshot correctly updates the field name from full_logo_url to logo_full_url, maintaining consistency with the broader API changes across the codebase.

packages/stack-shared/src/schema-fields.ts (1)

490-490: Schema constant renamed to match API consistency.

The export name change from projectFullLogoUrlSchema to projectLogoFullUrlSchema properly aligns with the API field naming convention (logo_full_url). The schema validation logic and metadata remain unchanged, ensuring no functional impact while improving naming consistency.

apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/current.test.ts (1)

42-42: Neon integration test snapshots consistently updated.

All three test snapshots have been properly updated to reflect the API field rename from full_logo_url to logo_full_url. The changes maintain consistency across the Neon integration endpoints.

Also applies to: 97-97, 168-168

apps/backend/src/app/api/latest/emails/render-email/route.tsx (1)

58-64: Email theming properly extended with project branding data.

The addition of the themeProps parameter to renderEmailWithTemplate correctly provides email themes with access to:

  • Project display name
  • Light and dark mode logo variants (both regular and full logos)

The implementation properly handles nullable logo URLs by using the nullish coalescing operator (??) to provide undefined as fallback values. This ensures email themes can conditionally render logos based on availability.

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx (1)

115-117: Project settings UI properly updated for new logo fields.

The changes correctly:

  1. Update the Full Logo field to use project.logoFullUrl instead of the previous project.fullLogoUrl
  2. Add dark mode logo variants with proper field mappings:
    • "Logo (Dark Mode)" → project.logoDarkModeUrl
    • "Full Logo (Dark Mode)" → project.logoFullDarkModeUrl

The implementation maintains consistency with the existing logo upload patterns and provides appropriate descriptions for each logo type. The LogoUpload component properly handles the type prop for different logo formats.

Also applies to: 123-141

apps/e2e/tests/backend/endpoints/api/v1/projects.test.ts (1)

100-101: Snapshots updated to logo_full_url — OK

The rename in snapshots matches the public field change.

Also applies to: 150-151, 205-206, 266-267, 321-322, 419-420, 479-480, 525-526, 571-572, 631-632, 804-805, 1100-1101

apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/provision.test.ts (1)

3-3: Import cleanup + snapshot rename are correct

Removing unused Project import and updating to logo_full_url look good.

Also applies to: 84-85

apps/backend/src/lib/email-rendering.tsx (1)

51-58: Remove the ThemeProps import suggestion—no such export exists in the shared package. The file packages/stack-shared/src/helpers/emails.ts (and other email-related modules) do not export a ThemeProps or EmailThemeProps type, so importing Partial<EmailThemeProps> isn’t valid.

Likely an incorrect or invalid review comment.

Copy link
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: 0

♻️ Duplicate comments (1)
apps/backend/src/lib/projects.tsx (1)

65-66: Fix created_at_millis: avoid coercing Date to string

Using row.createdAt + "Z" can yield Invalid Date. Use the Date directly or parse only when string.

Apply this diff:

-        created_at_millis: new Date(row.createdAt + "Z").getTime(),
+        created_at_millis: row.createdAt instanceof Date
+          ? row.createdAt.getTime()
+          : new Date(row.createdAt).getTime(),
🧹 Nitpick comments (3)
apps/backend/src/lib/projects.tsx (3)

93-101: Use Map over Record and parallelize uploads

Follows repo guideline (prefer ES6 Map). Also removes sequential awaits.

Apply this diff:

   const logoFields = ['logo_url', 'logo_full_url', 'logo_dark_mode_url', 'logo_full_dark_mode_url'] as const;
-  const logoUrls: Record<string, string | null | undefined> = {};
-
-  for (const field of logoFields) {
-    if (options.data[field] !== undefined) {
-      logoUrls[field] = await uploadAndGetUrl(options.data[field], "project-logos");
-    }
-  }
+  const logoUrls = new Map<typeof logoFields[number], string | null>();
+  await Promise.all(
+    logoFields.map(async (field) => {
+      const input = options.data[field];
+      if (input !== undefined) {
+        const url = await uploadAndGetUrl(input, "project-logos");
+        logoUrls.set(field, url ?? null);
+      }
+    })
+  );

(Optional) To avoid passing undefineds to Prisma, build a filtered object:

const logosData = filterUndefined({
  logoUrl: logoUrls.get('logo_url') ?? undefined,
  logoFullUrl: logoUrls.get('logo_full_url') ?? undefined,
  logoDarkModeUrl: logoUrls.get('logo_dark_mode_url') ?? undefined,
  logoFullDarkModeUrl: logoUrls.get('logo_full_dark_mode_url') ?? undefined,
});

114-118: Read from Map and avoid undefined writes

Use Map#get; consider wrapping data with filterUndefined to skip untouched fields.

Apply this diff:

-          logoUrl: logoUrls['logo_url'],
-          logoFullUrl: logoUrls['logo_full_url'],
-          logoDarkModeUrl: logoUrls['logo_dark_mode_url'],
-          logoFullDarkModeUrl: logoUrls['logo_full_dark_mode_url'],
+          logoUrl: logoUrls.get('logo_url'),
+          logoFullUrl: logoUrls.get('logo_full_url'),
+          logoDarkModeUrl: logoUrls.get('logo_dark_mode_url'),
+          logoFullDarkModeUrl: logoUrls.get('logo_full_dark_mode_url'),

(Optional) Wrap the whole data object:

data: filterUndefined({
  // existing fields...
  logoUrl: logoUrls.get('logo_url'),
  logoFullUrl: logoUrls.get('logo_full_url'),
  logoDarkModeUrl: logoUrls.get('logo_dark_mode_url'),
  logoFullDarkModeUrl: logoUrls.get('logo_full_dark_mode_url'),
})

148-152: Same Map usage for update path

Mirror the create-path change; also consider filterUndefined to prevent accidental field nulling.

Apply this diff:

-          logoUrl: logoUrls['logo_url'],
-          logoFullUrl: logoUrls['logo_full_url'],
-          logoDarkModeUrl: logoUrls['logo_dark_mode_url'],
-          logoFullDarkModeUrl: logoUrls['logo_full_dark_mode_url'],
+          logoUrl: logoUrls.get('logo_url'),
+          logoFullUrl: logoUrls.get('logo_full_url'),
+          logoDarkModeUrl: logoUrls.get('logo_dark_mode_url'),
+          logoFullDarkModeUrl: logoUrls.get('logo_full_dark_mode_url'),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e412ae and 20c6eee.

📒 Files selected for processing (3)
  • apps/backend/src/lib/projects.tsx (5 hunks)
  • packages/stack-shared/src/interface/crud/projects.ts (2 hunks)
  • packages/stack-shared/src/schema-fields.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/stack-shared/src/schema-fields.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Prefer ES6 Map over Record when representing key–value collections

Files:

  • apps/backend/src/lib/projects.tsx
  • packages/stack-shared/src/interface/crud/projects.ts
🧬 Code graph analysis (1)
apps/backend/src/lib/projects.tsx (1)
apps/backend/src/s3.tsx (1)
  • uploadAndGetUrl (86-105)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: all-good
  • GitHub Check: docker
  • GitHub Check: build (22.x)
  • GitHub Check: setup-tests
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: build (22.x)
  • GitHub Check: docker
  • GitHub Check: restart-dev-and-test
  • GitHub Check: Security Check
🔇 Additional comments (4)
apps/backend/src/lib/projects.tsx (2)

11-11: Import addition looks good

Importing RawQuery aligns with getProjectQuery’s return type.


62-64: Dark-mode logo fields exposed — LGTM

Field names match Prisma columns and shared types.

packages/stack-shared/src/interface/crud/projects.ts (2)

69-71: Admin Read: dark-mode logo fields — LGTM

Types align with new schemaFields; matches backend response shape.


121-123: Admin Update: dark-mode logo fields — LGTM

Symmetric with read schema; nullable+optional choice is appropriate.

Copy link
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: 2

♻️ Duplicate comments (1)
packages/stack-shared/src/helpers/emails.ts (1)

45-46: Duplicate import warning from earlier bots is resolved — keep as-is.

Only one @react-email/components import remains; the second import is for ThemeProps and is required.

🧹 Nitpick comments (4)
packages/stack-shared/src/helpers/emails.ts (4)

67-69: Use descriptive unsubscribe link text (accessibility + clarity).

“Click here” is non-descriptive. Prefer “Unsubscribe”.

-              <Link href={unsubscribeLink}>Click here{" "}</Link>
-              to unsubscribe from these emails
+              <Link href={unsubscribeLink}>Unsubscribe</Link>{" "}
+              from these emails
-              <Link href={unsubscribeLink} className="text-gray-300">Click here{" "}</Link>
-              to unsubscribe from these emails
+              <Link href={unsubscribeLink} className="text-gray-300">Unsubscribe</Link>{" "}
+              from these emails

Also applies to: 119-121


83-83: Export DarkEmailTheme for parity and reuse.

Keeps API symmetrical with LightEmailTheme.

-const DarkEmailTheme = `import { Html, Head, Tailwind, Body, Container, Link, Img } from '@react-email/components';
+export const DarkEmailTheme = `import { Html, Head, Tailwind, Body, Container, Link, Img } from '@react-email/components';

95-97: Flatten nested ternaries and make FullLogo’s alt descriptive.

Improves readability and brand-specific alt text.

-function FullLogo(props: { logoFullUrl: string }) {
-  return <Img src={props.logoFullUrl} alt="Full Logo" className="h-16" />;
+function FullLogo(props: { logoFullUrl: string; projectDisplayName?: string }) {
+  return (
+    <Img
+      src={props.logoFullUrl}
+      alt={props.projectDisplayName ? `${props.projectDisplayName} full logo` : "Full logo"}
+      className="h-16"
+    />
+  );
 }
-            {logoFullDarkModeUrl ?
-              <FullLogo logoFullUrl={logoFullDarkModeUrl} /> :
-              logoDarkModeUrl ?
-                <Logo logoUrl={logoDarkModeUrl} projectDisplayName={projectDisplayName} /> :
-                  logoFullUrl ?
-                    <FullLogo logoFullUrl={logoFullUrl} /> :
-                      logoUrl ?
-                        <Logo logoUrl={logoUrl} projectDisplayName={projectDisplayName} /> :
-                          null}
+            {(() => {
+              if (logoFullDarkModeUrl) return <FullLogo logoFullUrl={logoFullDarkModeUrl} projectDisplayName={projectDisplayName} />;
+              if (logoDarkModeUrl) return <Logo logoUrl={logoDarkModeUrl} projectDisplayName={projectDisplayName} />;
+              if (logoFullUrl) return <FullLogo logoFullUrl={logoFullUrl} projectDisplayName={projectDisplayName} />;
+              if (logoUrl) return <Logo logoUrl={logoUrl} projectDisplayName={projectDisplayName} />;
+              return null;
+            })()}

Also applies to: 106-114


78-79: Enhance PreviewProps to showcase branding in the preview.

Helps designers see header logos without wiring backend.

 EmailTheme.PreviewProps = {
-  unsubscribeLink: "https://example.com",
+  unsubscribeLink: "https://example.com",
+  projectDisplayName: "Acme Inc.",
+  logoUrl: "https://example.com/logo.png",
+  logoFullUrl: "https://example.com/logo-full.png",
 } satisfies Partial<ThemeProps>
 EmailTheme.PreviewProps = {
-  unsubscribeLink: "https://example.com",
+  unsubscribeLink: "https://example.com",
+  projectDisplayName: "Acme Inc.",
+  logoDarkModeUrl: "https://example.com/logo-dark.png",
+  logoFullDarkModeUrl: "https://example.com/logo-full-dark.png",
 } satisfies Partial<ThemeProps>

Also applies to: 130-131

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 20c6eee and 0fbb79d.

📒 Files selected for processing (1)
  • packages/stack-shared/src/helpers/emails.ts (3 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Prefer ES6 Map over Record when representing key–value collections

Files:

  • packages/stack-shared/src/helpers/emails.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Security Check
🔇 Additional comments (2)
packages/stack-shared/src/helpers/emails.ts (2)

48-48: Light theme props signature looks correct and aligned with new branding fields.

No issues spotted with added props.


99-99: Dark theme props signature matches new fields; LGTM.

Comment on lines +55 to +62
{logoFullUrl ?
<Img src={logoFullUrl} alt="Full Logo" className="h-16" /> :
logoUrl ?
<div className="flex gap-2 items-center">
<Img src={logoUrl} alt="Logo" className="h-8" />
<h2>{projectDisplayName}</h2>
</div>
: null}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Prevent “undefined” in header and improve alt text for accessibility.

If projectDisplayName is missing, <h2> will render “undefined”. Also, alt text should reflect the brand when available.

Apply:

-            {logoFullUrl ? 
-              <Img src={logoFullUrl} alt="Full Logo" className="h-16" /> :
-              logoUrl ? 
-                <div className="flex gap-2 items-center">
-                  <Img src={logoUrl} alt="Logo" className="h-8" />
-                  <h2>{projectDisplayName}</h2>
-                </div>
-                : null}
+            {logoFullUrl ? (
+              <Img
+                src={logoFullUrl}
+                alt={projectDisplayName ? `${projectDisplayName} full logo` : "Full logo"}
+                className="h-16"
+              />
+            ) : logoUrl ? (
+              <div className="flex gap-2 items-center">
+                <Img
+                  src={logoUrl}
+                  alt={projectDisplayName ? `${projectDisplayName} logo` : "Logo"}
+                  className="h-8"
+                />
+                {projectDisplayName && <h2>{projectDisplayName}</h2>}
+              </div>
+            ) : null}
📝 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
{logoFullUrl ?
<Img src={logoFullUrl} alt="Full Logo" className="h-16" /> :
logoUrl ?
<div className="flex gap-2 items-center">
<Img src={logoUrl} alt="Logo" className="h-8" />
<h2>{projectDisplayName}</h2>
</div>
: null}
{logoFullUrl ? (
<Img
src={logoFullUrl}
alt={projectDisplayName ? `${projectDisplayName} full logo` : "Full logo"}
className="h-16"
/>
) : logoUrl ? (
<div className="flex gap-2 items-center">
<Img
src={logoUrl}
alt={projectDisplayName ? `${projectDisplayName} logo` : "Logo"}
className="h-8"
/>
{projectDisplayName && <h2>{projectDisplayName}</h2>}
</div>
) : null}
🤖 Prompt for AI Agents
In packages/stack-shared/src/helpers/emails.ts around lines 55 to 62, the JSX
can render the literal "undefined" when projectDisplayName is missing and the
image alt attributes aren't using the brand name; change the conditional
rendering so the <h2> is only rendered when projectDisplayName is truthy
(otherwise omit it or render a safe fallback like an empty string), and update
the Img alt props to use projectDisplayName when available (e.g.
`${projectDisplayName} logo`) or a generic alt like "Project logo" to improve
accessibility.

Comment on lines +86 to +93
function Logo(props: { logoUrl: string; projectDisplayName: string | undefined }) {
return (
<div className="flex gap-2 items-center">
<Img src={props.logoUrl} alt="Logo" className="h-8" />
<h2 className="text-white">{props.projectDisplayName}</h2>
</div>
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid “undefined” in Dark theme header + better alt text.

Same issue as Light theme.

 function Logo(props: { logoUrl: string; projectDisplayName: string | undefined }) {
   return (
     <div className="flex gap-2 items-center">
-      <Img src={props.logoUrl} alt="Logo" className="h-8" />
-      <h2 className="text-white">{props.projectDisplayName}</h2>
+      <Img
+        src={props.logoUrl}
+        alt={props.projectDisplayName ? `${props.projectDisplayName} logo` : "Logo"}
+        className="h-8"
+      />
+      {props.projectDisplayName && <h2 className="text-white">{props.projectDisplayName}</h2>}
     </div>
   );  
 }
📝 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
function Logo(props: { logoUrl: string; projectDisplayName: string | undefined }) {
return (
<div className="flex gap-2 items-center">
<Img src={props.logoUrl} alt="Logo" className="h-8" />
<h2 className="text-white">{props.projectDisplayName}</h2>
</div>
);
}
function Logo(props: { logoUrl: string; projectDisplayName: string | undefined }) {
return (
<div className="flex gap-2 items-center">
<Img
src={props.logoUrl}
alt={props.projectDisplayName ? `${props.projectDisplayName} logo` : "Logo"}
className="h-8"
/>
{props.projectDisplayName && <h2 className="text-white">{props.projectDisplayName}</h2>}
</div>
);
}
🤖 Prompt for AI Agents
In packages/stack-shared/src/helpers/emails.ts around lines 86 to 93, the dark
theme Logo component currently renders the literal "undefined" when
projectDisplayName is missing and uses a generic alt; change the render to avoid
showing "undefined" by using a safe fallback or conditional (e.g.,
projectDisplayName ?? '' or only render the h2 when truthy) and update the Img
alt to a descriptive value (e.g., `${projectDisplayName ?? 'Project'} logo` or
'Project logo') so the header never shows "undefined" and screen readers get
meaningful alt text.

Copy link
Contributor

@N2D4 N2D4 left a comment

Choose a reason for hiding this comment

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

almost done

@N2D4 N2D4 assigned fomalhautb and unassigned N2D4 Sep 11, 2025
Copy link
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: 0

♻️ Duplicate comments (2)
apps/backend/src/lib/projects.tsx (1)

65-65: Address the Date conversion issue flagged in previous review.

The created_at_millis conversion issue remains unresolved. As noted in the previous review, appending "Z" to a Date object coerces it to a string and can yield Invalid Date.

apps/backend/src/lib/email-rendering.tsx (1)

97-100: Address the PreviewProps guard issue flagged in previous review.

The unguarded spread of EmailTheme.PreviewProps can cause a runtime TypeError if PreviewProps is undefined. The previous review recommended using (EmailTheme.PreviewProps ?? {}) to prevent this.

🧹 Nitpick comments (3)
apps/dashboard/src/components/vibe-coding/code-editor.tsx (3)

33-51: Reconsider disabling strictFunctionTypes in the editor TS config

Turning off strictFunctionTypes globally weakens function type checking (especially for callbacks) in the code editor, which can hide real mistakes users make in templates.

If this was added only to work around specific library type incompatibilities, consider a narrower fix instead (e.g., tweaking the affected declaration(s) only) so the rest of the editor can keep full strict behavior. At minimum, a short comment here explaining the rationale would help future maintainers understand why this strictness flag is relaxed.


55-67: Transform hook for remote d.ts is useful; minor robustness/clarity tweaks

The transform hook on fetchAndAddTypeDefinition plus transformTailwindTypeFile is a nice, targeted way to patch the @react-email/tailwind types without forking them; the control flow and usage look sound.

A couple of small suggestions:

  • Consider adding a brief comment above transformTailwindTypeFile explaining which upstream signature you’re patching and why (e.g., mismatch with JSX.Element | null), so future updates to @react-email/tailwind know what to check.
  • If @react-email/tailwind ever adds multiple overloaded signatures matching this pattern, the single replace may miss some. If you expect more than one, making the regex global (/.../g) would keep the patch scalable.

These are non-blocking, but would make this “stringly typed” patch easier to maintain.

Also applies to: 124-130, 134-139


83-107: Confirm ThemeProps['projectLogos'] shape and optionality matches the real API

The projectLogos object and ProjectLogo component typing generally look aligned with the PR’s logo fields (four optional URLs on a logos object).

Two details to double‑check against @stackframe/emails’ actual ThemeProps:

  • projectLogos is required on ThemeProps (the properties inside are optional). If the real theme props allow projectLogos to be absent entirely, this stub should likely be projectLogos?: { ... } to avoid mismatch and confusing editor errors.
  • ProjectLogo’s data prop is typed as ThemeProps['projectLogos'], which is therefore non-nullable. If ProjectLogo is intended to handle the “no logos configured” case (e.g., undefined or null), it might be better typed as ThemeProps['projectLogos'] | undefined (or similar).

Adjust only if they diverge from the actual runtime typings; otherwise this looks good.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0fbb79d and 25cc150.

📒 Files selected for processing (11)
  • apps/backend/prisma/schema.prisma (1 hunks)
  • apps/backend/src/app/api/latest/emails/render-email/route.tsx (1 hunks)
  • apps/backend/src/app/api/latest/emails/send-email/route.tsx (1 hunks)
  • apps/backend/src/app/api/latest/internal/email-templates/[templateId]/route.tsx (1 hunks)
  • apps/backend/src/lib/email-rendering.tsx (3 hunks)
  • apps/backend/src/lib/emails.tsx (1 hunks)
  • apps/backend/src/lib/projects.tsx (4 hunks)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx (1 hunks)
  • apps/dashboard/src/components/vibe-coding/code-editor.tsx (4 hunks)
  • apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/current.test.ts (3 hunks)
  • apps/e2e/tests/backend/endpoints/api/v1/internal/projects.test.ts (11 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • apps/e2e/tests/backend/endpoints/api/v1/integrations/neon/projects/current.test.ts
  • apps/backend/src/app/api/latest/emails/render-email/route.tsx
  • apps/backend/src/app/api/latest/emails/send-email/route.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx
  • apps/e2e/tests/backend/endpoints/api/v1/internal/projects.test.ts
  • apps/backend/prisma/schema.prisma
🧰 Additional context used
🧬 Code graph analysis (1)
apps/backend/src/lib/projects.tsx (1)
apps/backend/src/s3.tsx (1)
  • uploadAndGetUrl (86-105)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Vercel Agent Review
  • GitHub Check: all-good
  • GitHub Check: build (22.x)
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: docker
  • GitHub Check: check_prisma_migrations (22.x)
  • GitHub Check: setup-tests
  • GitHub Check: restart-dev-and-test
  • GitHub Check: restart-dev-and-test-with-custom-base-port
  • GitHub Check: build (22.x)
  • GitHub Check: build (22.x)
🔇 Additional comments (6)
apps/dashboard/src/components/vibe-coding/code-editor.tsx (1)

114-118: react-email package list change looks correct

Swapping in tailwind and dropping render in the reactEmailPackages array matches the new usage pattern described in the PR (theme composition vs. direct rendering). No issues here.

apps/backend/src/lib/projects.tsx (1)

62-64: LGTM! Logo field additions are well-integrated.

The expansion from two to four logo fields (adding logo_dark_mode_url and logo_full_dark_mode_url) is correctly implemented across the read and write paths:

  • Read path correctly maps from Prisma camelCase to API snake_case
  • Write path properly gathers URLs via uploadAndGetUrl for all four fields
  • Prisma create/update operations correctly write all four fields using bracket notation

Also applies to: 93-117, 148-151

apps/backend/src/lib/email-rendering.tsx (2)

52-60: LGTM! Theme props restructuring is well-designed.

The update to accept a nested themeProps object with projectLogos provides a cleaner API surface for passing branding data to email themes. The spread operator usage on line 102 correctly propagates all theme props to the EmailTheme component.

Also applies to: 102-103


290-354: LGTM! Logo component implementations are correct.

The three-component hierarchy works well:

  • Logo renders compact logo with project name
  • FullLogo renders full-width logo image
  • ProjectLogo correctly selects the appropriate variant based on mode (light/dark) and available logo URLs, with sensible fallback logic
apps/backend/src/lib/emails.tsx (1)

401-408: LGTM! Project logos correctly wired into email rendering.

The themeProps addition properly sources all four logo variants from tenancy.project and converts null to undefined for optional fields, matching the updated renderEmailWithTemplate signature.

apps/backend/src/app/api/latest/internal/email-templates/[templateId]/route.tsx (1)

45-52: LGTM! Theme props consistently integrated in preview mode.

The themeProps addition follows the same pattern as other call sites, correctly providing all four logo variants for email template preview rendering.

Copy link
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

♻️ Duplicate comments (1)
apps/backend/src/lib/email-rendering.tsx (1)

97-100: Guard the spread of EmailTheme.PreviewProps to prevent runtime TypeError.

When previewMode is true and EmailTheme.PreviewProps is undefined, the generated code will attempt to spread undefined, causing a runtime error.

Apply this diff to add nullish coalescing:

         const themeProps = {
           ...${JSON.stringify(options.themeProps || {})},
-          ...${previewMode ? "EmailTheme.PreviewProps" : "{}"},
+          ...${previewMode ? "(EmailTheme.PreviewProps ?? {})" : "{}"},
         }
🧹 Nitpick comments (1)
apps/backend/src/lib/email-rendering.tsx (1)

335-365: Consider adding validation for required logo URLs.

The ProjectLogo component returns components that depend on specific URL props (logoUrl, logoFullUrl, etc.) but doesn't validate these URLs exist before passing them. While the Img component may handle undefined src gracefully, explicit checks would make the logic clearer.

Example validation before returning Logo:

if (mode === "dark" && logoDarkModeUrl) {
  return React.createElement(Logo, {
    logoUrl: logoDarkModeUrl,
    projectDisplayName,
  });
}

Could be:

if (mode === "dark" && logoDarkModeUrl && projectDisplayName) {
  return React.createElement(Logo, {
    logoUrl: logoDarkModeUrl,
    projectDisplayName,
  });
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 91602ea and 95ef068.

📒 Files selected for processing (2)
  • apps/backend/src/app/api/latest/emails/send-email/route.tsx (1 hunks)
  • apps/backend/src/lib/email-rendering.tsx (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/backend/src/app/api/latest/emails/send-email/route.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: restart-dev-and-test-with-custom-base-port
  • GitHub Check: Vercel Agent Review
  • GitHub Check: build (22.x)
  • GitHub Check: build (22.x)
  • GitHub Check: all-good
  • GitHub Check: check_prisma_migrations (22.x)
  • GitHub Check: docker
  • GitHub Check: setup-tests
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: build (22.x)
  • GitHub Check: restart-dev-and-test
🔇 Additional comments (5)
apps/backend/src/lib/email-rendering.tsx (5)

52-60: LGTM! ThemeProps structure is well-defined.

The themeProps parameter properly encapsulates unsubscribeLink and projectLogos with appropriate optional fields for light/dark mode logo variants.


102-104: LGTM! ThemeProps correctly passed to EmailTheme.

The spread operator properly passes themeProps to EmailTheme, and the children fallback logic is sound.


159-166: LGTM! Batch themeProps structure is appropriate.

The themeProps structure for batch rendering appropriately includes projectLogos, while unsubscribeLink is handled per-input at the sibling level.


197-200: LGTM! Safe handling of optional themeProps in batch mode.

The code properly uses optional chaining and nullish coalescing to safely extract projectLogos, preventing runtime errors when themeProps is undefined.


202-204: LGTM! ThemeProps correctly applied in batch rendering.

The spread operator properly passes themeProps to EmailTheme in batch mode, consistent with the single rendering approach.

Copy link
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: 0

♻️ Duplicate comments (1)
apps/backend/src/lib/email-rendering.tsx (1)

97-100: Guard against undefined EmailTheme.PreviewProps to prevent runtime error.

The spread ...EmailTheme.PreviewProps on line 99 will throw a TypeError if PreviewProps is undefined. Although this was marked as addressed in a previous review, the code still lacks the guard.

Apply this diff to safely handle undefined PreviewProps:

         const themeProps = {
           ...${JSON.stringify(options.themeProps || {})},
-          ...${previewMode ? "EmailTheme.PreviewProps" : "{}"},
+          ...${previewMode ? "(EmailTheme.PreviewProps ?? {})" : "{}"},
         }
🧹 Nitpick comments (2)
apps/backend/src/lib/email-rendering.tsx (2)

197-200: Simplify themeProps construction by removing unnecessary spread.

Line 198 spreads an object literal containing a single key, which can be written more directly.

Apply this diff for cleaner code:

           const themeProps = {
-            ...{ projectLogos: input.themeProps?.projectLogos ?? {} },
+            projectLogos: input.themeProps?.projectLogos ?? {},
             unsubscribeLink: input.unsubscribeLink,
           }

311-321: Remove unused gap-2 className from Logo component.

The Logo component applies gap-2 to a flex container, but it only has a single child image. This className has no effect and may be leftover from a previous version that rendered text alongside the logo.

Apply this diff to clean up the unused style:

   export function Logo(props) {
     return React.createElement(
       "div",
-      { className: "flex gap-2 items-center" },
+      { className: "flex items-center" },
       React.createElement(Img, {
         logoUrl: props.logoUrl,
         alt: "Logo",
         className: "h-8",
       }),
     );
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c2a7071 and 6abb9ee.

📒 Files selected for processing (1)
  • apps/backend/src/lib/email-rendering.tsx (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/backend/src/lib/email-rendering.tsx (1)
packages/stack-shared/src/utils/strings.tsx (1)
  • deindent (235-238)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: build (22.x)
  • GitHub Check: build (22.x)
  • GitHub Check: build (22.x)
  • GitHub Check: setup-tests
  • GitHub Check: restart-dev-and-test
  • GitHub Check: restart-dev-and-test-with-custom-base-port
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: all-good
  • GitHub Check: Vercel Agent Review
  • GitHub Check: check_prisma_migrations (22.x)
  • GitHub Check: docker
🔇 Additional comments (1)
apps/backend/src/lib/email-rendering.tsx (1)

331-358: Verify the intended fallback behavior for dark mode logos.

The ProjectLogo component returns null when in dark mode but no dark-mode-specific logo URLs are provided, even if light mode logos exist. This could be intentional to avoid contrast issues (e.g., a light logo designed for dark backgrounds might not work on a light background), but it could also result in no logo being displayed when one is available.

Confirm whether the intended behavior is:

  1. Show nothing in dark mode if no dark logos exist (current behavior), or
  2. Fall back to light mode logos in dark mode

If fallback is desired, you can add this after line 347:

// Add fallback to light mode logos in dark mode
if (mode === "dark" && logoFullUrl) {
  return React.createElement(FullLogo, { logoFullUrl });
}
if (mode === "dark" && logoUrl) {
  return React.createElement(Logo, { logoUrl });
}

@BilalG1 BilalG1 requested a review from N2D4 November 17, 2025 23:19
@BilalG1 BilalG1 merged commit 4b955ce into dev Nov 20, 2025
22 checks passed
@BilalG1 BilalG1 deleted the email-templates branch November 20, 2025 03:12
@coderabbitai coderabbitai bot mentioned this pull request Jan 24, 2026
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.

3 participants