Skip to content

Commit bdde98c

Browse files
authored
Merge pull request #2 from chipgpt/feature/mailgun-email
Add email sending via Mailgun
2 parents 507ac31 + 24a726d commit bdde98c

File tree

10 files changed

+152
-26
lines changed

10 files changed

+152
-26
lines changed

.env renamed to .env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,7 @@ CUSTOM_MCP_DOMAIN=
1818
NEXT_PUBLIC_POSTHOG_KEY=
1919
# Posthog host. [Optional] Default: https://us.i.posthog.com
2020
NEXT_PUBLIC_POSTHOG_HOST=
21+
# Mailgun API Key. (Required)
22+
MAILGUN_API_KEY=
23+
# Sender domain for email. (Required)
24+
SENDER_DOMAIN=

.github/workflows/prod-workflow.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ jobs:
5858
DATABASE_URL: ${{ secrets.DATABASE_URL }}
5959
DATABASE_SSL: true
6060
AWS_HOSTED_ZONE_ID: ${{ secrets.AWS_HOSTED_ZONE_ID }}
61+
MAILGUN_API_KEY: ${{ secrets.MAILGUN_API_KEY }}
62+
SENDER_DOMAIN: chipgpt.biz
6163
CUSTOM_DOMAIN: chipgpt.biz
6264
CUSTOM_MCP_DOMAIN: mcp.chipgpt.biz
6365
NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ yarn-debug.log*
2626
yarn-error.log*
2727

2828
# local env files
29-
.env*.local
29+
.env.*
3030

3131
# vercel
3232
.vercel

README.md

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ The production cloud deployment without much activity is $1-$2 per day to run on
3030
- You must also bring your own production postgres database or you need to add RDS to the SST stack. I use [Digital Ocean managed databases](https://www.digitalocean.com/products/managed-databases-postgresql).
3131
- Your domain should be set up in Route 53 and you will need the Hosted Zone ID for cloud deployments (not needed for local deployments).
3232
- IAM user access key/secret [Setup Guide](https://guide.sst.dev/chapters/create-an-iam-user.html) and [Permissions Guide](https://sst.dev/docs/iam-credentials/#iam-permissions)
33+
- Mailgun Account [Get API Key](https://help.mailgun.com/hc/en-us/articles/203380100-Where-can-I-find-my-API-keys-and-SMTP-credentials)
3334

3435
You can do a global find for `chipgpt` (case insensitive) and locate most things that need to be updated with your own project name and description.
3536

@@ -41,12 +42,12 @@ Log in to AWS SSO:
4142
npm run sso
4243
```
4344

44-
Copy the `.env` file and populate them. You don't need a development cloud deployment but it supports it if you want a staging server eventually:
45+
Copy the `.env.example` file and populate them. You don't need a development cloud deployment but it supports it if you want a staging server eventually:
4546

4647
```bash
47-
cp .env .env.local
48-
cp .env .env.development.local
49-
cp .env .env.production.local
48+
cp .env .env.{{username}}
49+
cp .env .env.development
50+
cp .env .env.production
5051
```
5152

5253
Install dependencies:
@@ -134,19 +135,20 @@ To get the GitHub action deployment working you will need an IAM user with acces
134135
- AUTH_SECRET
135136
- DATABASE_URL
136137
- AWS_HOSTED_ZONE_ID
138+
- MAILGUN_API_KEY
137139
- NEXT_PUBLIC_POSTHOG_KEY (not really a "secret", it could be a variable instead)
138140
139141
## Things I intend to add as I add them to my own SaaS:
140142
141-
- Add SES email management for production SES access
142-
- Add a paid account tier (most likely using Stripe as the payment gateway)
143-
- Add a propper logging utility that works better with AWS CloudWatch.
144-
- Add Alarms/Alerts for cloud deployments to be proactive about issues.
145-
- Auto-Generate REST API Documentation.
146-
- Support for Amazon RDS + Proxy (had trouble getting it working with Sequelize, didn't feel like finding a new ORM)
147-
- Add proper Sequelize migrations.
148-
- Update to use the new Cognito UI mode.
149-
- Switch to the official `@auth/sequelize-adapter` package after [PR #13120](https://github.com/nextauthjs/next-auth/pull/13120) is merged.
143+
- [x] ~~Add SES email management for production SES access~~ Add Mailgun email for sending emails
144+
- [ ] Add a paid account tier (most likely using Stripe as the payment gateway)
145+
- [ ] Add a propper logging utility that works better with AWS CloudWatch.
146+
- [ ] Add Alarms/Alerts for cloud deployments to be proactive about issues.
147+
- [ ] Auto-Generate REST API Documentation.
148+
- [ ] Support for Amazon RDS + Proxy (had trouble getting it working with Sequelize, didn't feel like finding a new ORM)
149+
- [ ] Add proper Sequelize migrations.
150+
- [ ] Update to use the new Cognito UI mode.
151+
- [ ] Switch to the official `@auth/sequelize-adapter` package after [PR #13120](https://github.com/nextauthjs/next-auth/pull/13120) is merged.
150152
151153
## Feedback & Questions:
152154

package-lock.json

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"express-rate-limit": "^8.0.1",
4545
"lodash": "^4.17.21",
4646
"lucide-react": "^0.525.0",
47+
"mailgun.js": "^12.0.3",
4748
"next-auth": "^5.0.0-beta.29",
4849
"pg": "^8.13.1",
4950
"posthog-js": "^1.256.2",

src/auth.config.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { InitAccountModel } from './server/models/account';
99
import SequelizeAdapter from './lib/@auth/sequelize-adapter';
1010
import { InitSessionModel } from './server/models/session';
1111
import { InitVerificationTokenModel } from './server/models/verification-token';
12+
import { sendMailgunEmail } from './server/utils/mailgun';
1213

1314
export const nextAuthConfig: NextAuthConfig = {
1415
callbacks: {
@@ -34,6 +35,23 @@ export const nextAuthConfig: NextAuthConfig = {
3435
// return args;
3536
// },
3637
},
38+
events: {
39+
// Send welcome email to new users
40+
createUser: async ({ user }) => {
41+
if (user.email) {
42+
try {
43+
await sendMailgunEmail(
44+
user.email,
45+
'Welcome to ChipGPT',
46+
'Welcome to ChipGPT',
47+
'Welcome to ChipGPT'
48+
);
49+
} catch (error) {
50+
console.error('Error sending welcome email', error);
51+
}
52+
}
53+
},
54+
},
3755
providers: [
3856
Cognito({
3957
clientId: Resource.MyUserPoolClient.id,

src/server/utils/mailgun.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { once } from 'lodash';
2+
import Mailgun from 'mailgun.js';
3+
import { Resource } from 'sst';
4+
5+
const mailgun = new Mailgun(FormData);
6+
7+
export const getMailgunClient = once(() => {
8+
// @ts-ignore
9+
return mailgun.client({ username: 'api', key: Resource.MyMailgun.key });
10+
});
11+
12+
export function sendMailgunEmail(email: string, subject: string, html: string, text: string) {
13+
// @ts-ignore
14+
return getMailgunClient().messages.create(Resource.MyMailgun.domain, {
15+
from: `ChipGPT <[email protected]>`,
16+
to: [email],
17+
subject,
18+
html,
19+
text,
20+
});
21+
}

sst-env.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ declare module "sst" {
99
"name": string
1010
"type": "sst.aws.Dynamo"
1111
}
12+
"MyMailgun": {
13+
"domain": string
14+
"key": string
15+
"type": "sst.sst.Linkable"
16+
}
1217
"MyMcpService": {
1318
"service": string
1419
"type": "sst.aws.Service"

sst.config.ts

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,35 +26,81 @@ export default $config({
2626
input.stage === 'production' ? 'chipgpt-production' : 'chipgpt-development',
2727
}),
2828
},
29+
mailgun: '3.5.10',
2930
},
3031
};
3132
},
3233
async run() {
3334
const isProductionStage = $app.stage === 'production';
34-
const isDevelopmentStage = $app.stage === 'development';
35-
36-
// Grab the .env based on stage
37-
if (isProductionStage) {
38-
config({ path: './.env.production.local', override: true });
39-
} else if (isDevelopmentStage) {
40-
config({ path: './.env.development.local', override: true });
41-
} else {
42-
config({ path: './.env.local', override: true });
43-
}
4435

4536
// Validate required environment vars
4637
if (!process.env.AUTH_SECRET) {
4738
throw new Error('process.env.AUTH_SECRET is required');
4839
}
40+
if (!process.env.AWS_HOSTED_ZONE_ID) {
41+
throw new Error('process.env.AWS_HOSTED_ZONE_ID is required');
42+
}
43+
if (!process.env.SENDER_DOMAIN) {
44+
throw new Error('process.env.SENDER_DOMAIN is required');
45+
}
46+
if (!process.env.MAILGUN_API_KEY) {
47+
throw new Error('process.env.MAILGUN_API_KEY is required');
48+
}
4949

5050
// Environment variables we will expose to the functions and nextjs app
5151
const environment = {
5252
NODE_ENV: process.env.NODE_ENV || 'development',
5353
DATABASE_URL: process.env.DATABASE_URL || '',
5454
DATABASE_SSL: process.env.DATABASE_SSL || '',
5555
WEB_URL: process.env.WEB_URL || 'http://localhost:3000',
56+
SENDER_DOMAIN: process.env.SENDER_DOMAIN || '',
57+
MAILGUN_API_KEY: process.env.MAILGUN_API_KEY || '',
5658
};
5759

60+
// Create Mailgun domain and verify DNS sending records
61+
const mailgunEmail = new mailgun.Domain('MyMailgunDomain', {
62+
name: environment.SENDER_DOMAIN,
63+
webScheme: 'https',
64+
});
65+
const sendingRecord1 = mailgunEmail.sendingRecordsSets?.[0];
66+
const sendingRecord2 = mailgunEmail.sendingRecordsSets?.[1];
67+
const sendingRecord3 = mailgunEmail.sendingRecordsSets?.[2];
68+
if (sendingRecord1) {
69+
new aws.route53.Record('MyMailgunEmailSendingRecord1', {
70+
zoneId: process.env.AWS_HOSTED_ZONE_ID,
71+
name: sendingRecord1.name,
72+
type: sendingRecord1.recordType,
73+
ttl: 600,
74+
records: [sendingRecord1.value],
75+
});
76+
}
77+
if (sendingRecord2) {
78+
new aws.route53.Record('MyMailgunEmailSendingRecord2', {
79+
zoneId: process.env.AWS_HOSTED_ZONE_ID,
80+
name: sendingRecord2.name,
81+
type: sendingRecord2.recordType,
82+
ttl: 600,
83+
records: [sendingRecord2.value],
84+
});
85+
}
86+
if (sendingRecord3) {
87+
new aws.route53.Record('MyMailgunEmailSendingRecord3', {
88+
zoneId: process.env.AWS_HOSTED_ZONE_ID,
89+
name: sendingRecord3.name,
90+
type: sendingRecord3.recordType,
91+
ttl: 600,
92+
records: [sendingRecord3.value],
93+
});
94+
}
95+
96+
// Create a linkable for the Mailgun API key to link to the app
97+
const mailgunLinkable = new sst.Linkable('MyMailgun', {
98+
properties: {
99+
key: environment.MAILGUN_API_KEY,
100+
domain: mailgunEmail.name,
101+
},
102+
});
103+
58104
// Create our VPC and add EC2 internet gateway
59105
const vpc = new sst.aws.Vpc('MyVpc', { nat: 'ec2' });
60106
const cluster = new sst.aws.Cluster('MyCluster', {
@@ -102,7 +148,7 @@ export default $config({
102148
AUTH_TRUST_HOST: String(!!environment.WEB_URL),
103149
AUTH_URL: `${environment.WEB_URL}/api/auth`,
104150
},
105-
link: [pool, client].filter(Boolean),
151+
link: [mailgunLinkable, pool, client].filter(Boolean),
106152
domain: process.env.CUSTOM_DOMAIN
107153
? {
108154
name: process.env.CUSTOM_DOMAIN,
@@ -179,7 +225,7 @@ export default $config({
179225
cpu: '0.5 vCPU',
180226
memory: '1 GB',
181227
environment: environment,
182-
link: [dynamoMCPSessionCache].filter(Boolean),
228+
link: [mailgunLinkable, dynamoMCPSessionCache].filter(Boolean),
183229
dev: {
184230
command: 'npm run dev:mcp',
185231
},

0 commit comments

Comments
 (0)