Skip to content

feat: Add resend and stub as an email provider#852

Merged
kingston merged 6 commits into
mainfrom
kingston/eng-1093-add-resend-as-an-email-provider
Mar 20, 2026
Merged

feat: Add resend and stub as an email provider#852
kingston merged 6 commits into
mainfrom
kingston/eng-1093-add-resend-as-an-email-provider

Conversation

@kingston

@kingston kingston commented Mar 20, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added Resend email provider option alongside existing Postmark support for transactional email delivery.
    • Added Stub/Custom email provider option for testing and custom implementations.
    • Added blog post management capabilities (create, read, update, delete operations) to the example application.
  • Chores

    • Updated example application schema and configuration structures.
    • Reorganized plugin configuration entries.

@changeset-bot

changeset-bot Bot commented Mar 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1b88c2c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 23 packages
Name Type
@baseplate-dev/plugin-email Patch
@baseplate-dev/project-builder-common Patch
@baseplate-dev/plugin-auth Patch
@baseplate-dev/project-builder-cli Patch
@baseplate-dev/create-project Patch
@baseplate-dev/code-morph Patch
@baseplate-dev/core-generators Patch
@baseplate-dev/fastify-generators Patch
@baseplate-dev/project-builder-dev Patch
@baseplate-dev/project-builder-lib Patch
@baseplate-dev/project-builder-server Patch
@baseplate-dev/project-builder-test Patch
@baseplate-dev/project-builder-web Patch
@baseplate-dev/react-generators Patch
@baseplate-dev/sync Patch
@baseplate-dev/tools Patch
@baseplate-dev/ui-components Patch
@baseplate-dev/utils Patch
@baseplate-dev/plugin-observability Patch
@baseplate-dev/plugin-payments Patch
@baseplate-dev/plugin-queue Patch
@baseplate-dev/plugin-rate-limit Patch
@baseplate-dev/plugin-storage Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Mar 20, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@kingston has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 53 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 11be7444-9b49-4db1-a7c5-f6dbe4322585

📥 Commits

Reviewing files that changed from the base of the PR and between f6f2294 and 1b88c2c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (1)
  • plugins/plugin-email/package.json
📝 Walkthrough

Walkthrough

Two new email providers (Resend and Stub/Custom) are added to the plugin-email package, each with configuration schemas, web editors, code generators, and email adapter implementations. GraphQL blog post types and mutations are introduced to the example application. Plugin ordering in the project definition is adjusted.

Changes

Cohort / File(s) Summary
Resend Email Provider
plugins/plugin-email/src/resend/*
Introduces complete Resend provider implementation including schema definition, web UI editor component, plugin module registration (common/node/web), generator with template, and email adapter service using Resend client for transactional email delivery.
Stub/Custom Email Provider
plugins/plugin-email/src/stub/*
Implements stub provider with analogous structure: plugin schema, definition editor, module registration, parameterized generator accepting custom provider names, and stub email adapter that logs messages without sending.
BlogPost GraphQL Schema & Types
examples/blog-with-auth/apps/backend/schema.graphql, examples/blog-with-auth/apps/admin/src/gql/graphql.ts
Adds BlogPost type with CRUD mutations (createBlogPost, updateBlogPost, deleteBlogPost) and queries (blogPost, blogPosts) to GraphQL schema and generates corresponding TypeScript type definitions in the admin app.
Plugin Exports & Infrastructure
plugins/plugin-email/src/index.ts, .changeset/add-resend-email-provider.md
Extends plugin-email package exports to include Resend and Stub provider types; documents patch-level release introducing both new email provider options alongside existing Postmark.
Project Configuration & Template Updates
examples/blog-with-auth/baseplate/project-definition.json, packages/react-generators/src/generators/core/react-tailwind/extractor.json
Reorders postmark plugin position in plugin array; adds light/dark color theme template variables to Tailwind generator configuration.
Plugin Module Cleanup & Docs
plugins/plugin-auth/src/.../seed-initial-user/..., packages/project-builder-common/index.js
Removes seedInitialUser from projectExports in better-auth and local-auth seed modules; removes JSDoc return type annotation from getDefaultPlugins.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/Admin
    participant WebUI as Web UI<br/>(Definition Editor)
    participant Form as Form Handler<br/>(useResettableForm)
    participant Schema as Zod Schema<br/>(Plugin Definition)
    participant Config as projectDefinition<br/>(pluginConfig)
    participant Backend as Backend Compiler

    User->>WebUI: Open Resend/Stub<br/>Provider Config
    activate WebUI
    WebUI->>Schema: Build schema from<br/>createXxxPluginDefinitionSchema
    WebUI->>Form: Initialize form with<br/>default values from config
    activate Form
    User->>Form: Enter provider settings
    User->>Form: Submit form
    Form->>Schema: Validate against schema
    Schema-->>Form: Validation result
    Form->>Config: setPluginConfig with<br/>updated definition
    activate Config
    Config-->>Form: Success response
    deactivate Config
    Form->>WebUI: onSave callback
    deactivate Form
    WebUI->>User: Show success message
    deactivate WebUI
    
    Note over Backend: During Build Phase
    Backend->>Config: Retrieve plugin config<br/>by pluginKey
    Config-->>Backend: Plugin definition
    Backend->>Backend: Invoke generator<br/>(resendGenerator/stubGenerator)<br/>with config.stubOptions.providerName
    Backend->>Backend: Render email adapter service<br/>template with variables
    Backend-->>Backend: Compiled app with<br/>email provider integration
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding two new email providers (Resend and Stub) to the plugin-email package.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch kingston/eng-1093-add-resend-as-an-email-provider
📝 Coding Plan
  • Generate coding plan for human review comments

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/blog-with-auth/apps/backend/schema.graphql`:
- Around line 27-33: Add the missing id field to the GraphQL BlogPost type so
clients can read object identifiers: update the type definition for BlogPost to
include an id: Uuid! field (matching Prisma model) so queries like blogPost(id:
Uuid!) and mutations updateBlogPost/deleteBlogPost and create responses return
the id for Apollo Client cache normalization and subsequent operations.

In `@plugins/plugin-email/src/resend/core/generators/resend/resend.generator.ts`:
- Around line 53-64: The email adapter registration currently calls
emailConfig.emailAdapter.set(...) which overwrites any previously-registered
adapter and makes the selected provider order-dependent; change this to detect
an existing registration and fail-fast: before calling .set(...) (in
resend.generator.ts for resendEmailAdapter and likewise in stub.generator.ts),
check the current value (e.g., emailConfig.emailAdapter.get() or equivalent) and
if a value exists throw a clear error/raise a conflict that names the existing
provider and the attempted provider (paths.resendService/resendEmailAdapter),
instead of silently replacing it; ensure the check runs inside the
createGeneratorTask run() so the generator errors when a second provider
registers.

In `@plugins/plugin-email/src/stub/core/generators/stub/stub.generator.ts`:
- Around line 13-15: The descriptorSchema currently permits arbitrary
providerName strings and those are used to build adapterName and are
interpolated into generated single-quoted string literals in stub.generator.ts,
which can produce invalid TypeScript for values like O'Hare or strings with
newlines; constrain providerName with a safe regex (e.g. allow only
alphanumerics, dashes/underscores) in descriptorSchema and the
editor/plugin-definition schema (plugin-definition.ts) and ensure adapterName is
derived only from the validated/sanitized providerName; additionally, when
emitting providerName into templates in stub.generator.ts (where template
variables are inserted into single-quoted literals), wrap the value with
JSON.stringify() (or otherwise escape it) so the generated code is always valid
TypeScript.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 226693c4-4580-4182-925f-c6ab786e4d50

📥 Commits

Reviewing files that changed from the base of the PR and between 71146cd and f6f2294.

⛔ Files ignored due to path filters (20)
  • examples/blog-with-auth/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • packages/fastify-generators/src/generators/prisma/data-utils/generated/ts-import-providers.ts is excluded by !**/generated/**, !**/generated/**
  • packages/fastify-generators/src/generators/prisma/data-utils/generated/typed-templates.ts is excluded by !**/generated/**, !**/generated/**
  • packages/react-generators/src/generators/core/react-tailwind/generated/typed-templates.ts is excluded by !**/generated/**, !**/generated/**
  • plugins/plugin-auth/src/better-auth/generators/fastify/seed-initial-user/generated/index.ts is excluded by !**/generated/**, !**/generated/**
  • plugins/plugin-auth/src/better-auth/generators/fastify/seed-initial-user/generated/ts-import-providers.ts is excluded by !**/generated/**, !**/generated/**
  • plugins/plugin-auth/src/better-auth/generators/fastify/seed-initial-user/generated/typed-templates.ts is excluded by !**/generated/**, !**/generated/**
  • plugins/plugin-auth/src/local-auth/core/generators/seed-initial-user/generated/index.ts is excluded by !**/generated/**, !**/generated/**
  • plugins/plugin-auth/src/local-auth/core/generators/seed-initial-user/generated/ts-import-providers.ts is excluded by !**/generated/**, !**/generated/**
  • plugins/plugin-auth/src/local-auth/core/generators/seed-initial-user/generated/typed-templates.ts is excluded by !**/generated/**, !**/generated/**
  • plugins/plugin-email/src/resend/core/generators/resend/generated/index.ts is excluded by !**/generated/**, !**/generated/**
  • plugins/plugin-email/src/resend/core/generators/resend/generated/template-paths.ts is excluded by !**/generated/**, !**/generated/**
  • plugins/plugin-email/src/resend/core/generators/resend/generated/template-renderers.ts is excluded by !**/generated/**, !**/generated/**
  • plugins/plugin-email/src/resend/core/generators/resend/generated/typed-templates.ts is excluded by !**/generated/**, !**/generated/**
  • plugins/plugin-email/src/resend/static/icon.svg is excluded by !**/*.svg
  • plugins/plugin-email/src/stub/core/generators/stub/generated/index.ts is excluded by !**/generated/**, !**/generated/**
  • plugins/plugin-email/src/stub/core/generators/stub/generated/template-paths.ts is excluded by !**/generated/**, !**/generated/**
  • plugins/plugin-email/src/stub/core/generators/stub/generated/template-renderers.ts is excluded by !**/generated/**, !**/generated/**
  • plugins/plugin-email/src/stub/core/generators/stub/generated/typed-templates.ts is excluded by !**/generated/**, !**/generated/**
  • plugins/plugin-email/src/stub/static/icon.svg is excluded by !**/*.svg
📒 Files selected for processing (37)
  • .changeset/add-resend-email-provider.md
  • examples/blog-with-auth/apps/admin/src/gql/graphql.ts
  • examples/blog-with-auth/apps/backend/schema.graphql
  • examples/blog-with-auth/baseplate/project-definition.json
  • packages/project-builder-common/index.js
  • packages/react-generators/src/generators/core/react-tailwind/extractor.json
  • plugins/plugin-auth/src/better-auth/generators/fastify/seed-initial-user/extractor.json
  • plugins/plugin-auth/src/better-auth/generators/fastify/seed-initial-user/index.ts
  • plugins/plugin-auth/src/local-auth/core/generators/seed-initial-user/extractor.json
  • plugins/plugin-auth/src/local-auth/core/generators/seed-initial-user/index.ts
  • plugins/plugin-email/src/index.ts
  • plugins/plugin-email/src/resend/core/common.ts
  • plugins/plugin-email/src/resend/core/components/resend-definition-editor.tsx
  • plugins/plugin-email/src/resend/core/generators/index.ts
  • plugins/plugin-email/src/resend/core/generators/resend/extractor.json
  • plugins/plugin-email/src/resend/core/generators/resend/index.ts
  • plugins/plugin-email/src/resend/core/generators/resend/resend.generator.ts
  • plugins/plugin-email/src/resend/core/generators/resend/templates/module/emails/services/resend.service.ts
  • plugins/plugin-email/src/resend/core/index.ts
  • plugins/plugin-email/src/resend/core/node.ts
  • plugins/plugin-email/src/resend/core/schema/plugin-definition.ts
  • plugins/plugin-email/src/resend/core/web.ts
  • plugins/plugin-email/src/resend/index.ts
  • plugins/plugin-email/src/resend/plugin.json
  • plugins/plugin-email/src/stub/core/common.ts
  • plugins/plugin-email/src/stub/core/components/stub-definition-editor.tsx
  • plugins/plugin-email/src/stub/core/generators/index.ts
  • plugins/plugin-email/src/stub/core/generators/stub/extractor.json
  • plugins/plugin-email/src/stub/core/generators/stub/index.ts
  • plugins/plugin-email/src/stub/core/generators/stub/stub.generator.ts
  • plugins/plugin-email/src/stub/core/generators/stub/templates/module/emails/services/stub.service.ts
  • plugins/plugin-email/src/stub/core/index.ts
  • plugins/plugin-email/src/stub/core/node.ts
  • plugins/plugin-email/src/stub/core/schema/plugin-definition.ts
  • plugins/plugin-email/src/stub/core/web.ts
  • plugins/plugin-email/src/stub/index.ts
  • plugins/plugin-email/src/stub/plugin.json
💤 Files with no reviewable changes (5)
  • packages/project-builder-common/index.js
  • plugins/plugin-auth/src/better-auth/generators/fastify/seed-initial-user/extractor.json
  • plugins/plugin-auth/src/local-auth/core/generators/seed-initial-user/extractor.json
  • plugins/plugin-auth/src/better-auth/generators/fastify/seed-initial-user/index.ts
  • plugins/plugin-auth/src/local-auth/core/generators/seed-initial-user/index.ts

Comment on lines +27 to +33
type BlogPost {
blogId: Uuid!
content: String!
metadata: JSON
publisherId: Uuid!
title: String!
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Missing id field in BlogPost type.

The BlogPost type is missing the id field that exists in the Prisma model. This will cause issues because:

  1. Apollo Client requires id for cache normalization
  2. The blogPost(id: Uuid!) query accepts an id but clients cannot retrieve it from the response
  3. Clients cannot obtain the id needed for updateBlogPost and deleteBlogPost mutations after creating a post

Other entity types like Blog and User correctly expose their id field.

🐛 Proposed fix to add id field
 type BlogPost {
+  id: ID!
   blogId: Uuid!
   content: String!
   metadata: JSON
   publisherId: Uuid!
   title: String!
 }
📝 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
type BlogPost {
blogId: Uuid!
content: String!
metadata: JSON
publisherId: Uuid!
title: String!
}
type BlogPost {
id: ID!
blogId: Uuid!
content: String!
metadata: JSON
publisherId: Uuid!
title: String!
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/blog-with-auth/apps/backend/schema.graphql` around lines 27 - 33,
Add the missing id field to the GraphQL BlogPost type so clients can read object
identifiers: update the type definition for BlogPost to include an id: Uuid!
field (matching Prisma model) so queries like blogPost(id: Uuid!) and mutations
updateBlogPost/deleteBlogPost and create responses return the id for Apollo
Client cache normalization and subsequent operations.

Comment on lines +53 to +64
emailConfig: createGeneratorTask({
dependencies: {
paths: GENERATED_TEMPLATES.paths.provider,
emailConfig: emailConfigProvider,
},
run({ paths, emailConfig }) {
emailConfig.emailAdapter.set(
tsCodeFragment(
'resendEmailAdapter',
tsImportBuilder(['resendEmailAdapter']).from(paths.resendService),
),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fail fast when a second email provider registers.

emailAdapter is a scalar config field, so .set() here replaces any adapter that was already registered by another provider. With the same registration pattern also added in plugins/plugin-email/src/stub/core/generators/stub/stub.generator.ts, the effective provider becomes generator-order dependent instead of explicit. Please enforce mutual exclusivity or raise a conflict when a second provider tries to register.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/plugin-email/src/resend/core/generators/resend/resend.generator.ts`
around lines 53 - 64, The email adapter registration currently calls
emailConfig.emailAdapter.set(...) which overwrites any previously-registered
adapter and makes the selected provider order-dependent; change this to detect
an existing registration and fail-fast: before calling .set(...) (in
resend.generator.ts for resendEmailAdapter and likewise in stub.generator.ts),
check the current value (e.g., emailConfig.emailAdapter.get() or equivalent) and
if a value exists throw a clear error/raise a conflict that names the existing
provider and the attempted provider (paths.resendService/resendEmailAdapter),
instead of silently replacing it; ensure the check runs inside the
createGeneratorTask run() so the generator errors when a second provider
registers.

Comment on lines +13 to +15
const descriptorSchema = z.object({
providerName: z.string().min(1),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '13,25p;54,57p' plugins/plugin-email/src/stub/core/generators/stub/stub.generator.ts

python - <<'PY'
samples = ["O'Hare", "line\nbreak"]
for value in samples:
    print("providerName =", repr(value))
    print("current TPL_PROVIDER_NAME =", f"'{value}'")
    print("current TPL_LOG_MESSAGE =", f"'{value} email adapter: email logged (not sent)'")
    print()
PY

Repository: halfdomelabs/baseplate

Length of output: 939


🏁 Script executed:

cat -n plugins/plugin-email/src/stub/core/schema/plugin-definition.ts | head -40

Repository: halfdomelabs/baseplate

Length of output: 992


🏁 Script executed:

rg 'TPL_PROVIDER_NAME|TPL_LOG_MESSAGE' plugins/plugin-email/src --type ts

Repository: halfdomelabs/baseplate

Length of output: 822


🏁 Script executed:

fd '\.ts$' plugins/plugin-email/src --exec grep -l 'buildTasks.*providerName' {} \;

Repository: halfdomelabs/baseplate

Length of output: 134


🏁 Script executed:

cat -n plugins/plugin-email/src/stub/core/generators/stub/templates/module/emails/services/stub.service.ts

Repository: halfdomelabs/baseplate

Length of output: 1759


Escape and constrain providerName before generating code.

providerName is validated only with .min(1) in the descriptor schema (lines 13–15) and the editor schema, but it is interpolated directly into single-quoted string literals on lines 56–57. Inputs like O'Hare or newlines will emit invalid TypeScript source code. The same free-form value also drives adapterName on line 25. Add a regex constraint to both schemas and use JSON.stringify() for template variable values to ensure safe code generation.

♻️ Proposed fix
 const descriptorSchema = z.object({
-  providerName: z.string().min(1),
+  providerName: z
+    .string()
+    .regex(
+      /^[A-Za-z][A-Za-z0-9 _-]*$/,
+      'Use letters, numbers, spaces, "_" or "-", and start with a letter.',
+    ),
 });
 
 /**
  * Generator for stub/core/stub
  */
 export const stubGenerator = createGenerator({
@@
   descriptorSchema,
   buildTasks: ({ providerName }) => {
     const adapterName = `${camelCase(providerName)}EmailAdapter`;
+    const providerNameLiteral = JSON.stringify(providerName);
+    const logMessageLiteral = JSON.stringify(
+      `${providerName} email adapter: email logged (not sent)`,
+    );
 
     return {
@@
                 renderers.stubService.render({
                   variables: {
                     TPL_ADAPTER_NAME: adapterName,
-                    TPL_PROVIDER_NAME: `'${providerName}'`,
-                    TPL_LOG_MESSAGE: `'${providerName} email adapter: email logged (not sent)'`,
+                    TPL_PROVIDER_NAME: providerNameLiteral,
+                    TPL_LOG_MESSAGE: logMessageLiteral,
                   },
                 }),
               );

Also update plugins/plugin-email/src/stub/core/schema/plugin-definition.ts with the same regex constraint on the providerName field to prevent invalid input at the editor level.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/plugin-email/src/stub/core/generators/stub/stub.generator.ts` around
lines 13 - 15, The descriptorSchema currently permits arbitrary providerName
strings and those are used to build adapterName and are interpolated into
generated single-quoted string literals in stub.generator.ts, which can produce
invalid TypeScript for values like O'Hare or strings with newlines; constrain
providerName with a safe regex (e.g. allow only alphanumerics,
dashes/underscores) in descriptorSchema and the editor/plugin-definition schema
(plugin-definition.ts) and ensure adapterName is derived only from the
validated/sanitized providerName; additionally, when emitting providerName into
templates in stub.generator.ts (where template variables are inserted into
single-quoted literals), wrap the value with JSON.stringify() (or otherwise
escape it) so the generated code is always valid TypeScript.

@kingston kingston merged commit 90ef6d5 into main Mar 20, 2026
12 checks passed
@kingston kingston deleted the kingston/eng-1093-add-resend-as-an-email-provider branch March 20, 2026 11:46
@github-actions github-actions Bot mentioned this pull request Mar 20, 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.

1 participant