---
title: "Build an email newsletter with Claude Code"
author: "@chris"
author_url: https://elements.dev/u/@chris
url: https://elements.dev/blog/build-an-email-newsletter-with-claude-code-f68ur21e
kind: post
format: video
video: https://elements.dev/blog/build-an-email-newsletter-with-claude-code-f68ur21e/video.mp4
poster: https://elements.dev/blog/build-an-email-newsletter-with-claude-code-f68ur21e/poster.jpg
duration: "20:10"
transcript_vtt: https://elements.dev/blog/build-an-email-newsletter-with-claude-code-f68ur21e/transcript.vtt
---
# Build an email newsletter with Claude Code
by [@chris](https://elements.dev/u/@chris)
## Description
Claude Code builds a public blog with accounts and a newsletter that goes out every Friday with only the posts published since the last issue, finishing with 29 passing tests. Then we work out what the app costs to run on AWS, about $26 a month with email under $1, and tour the code the agent wrote.
## Notes
In this video we build a newsletter blog with Claude Code on Elements. The blog is public, readers sign up with an email and password, and every Friday each subscriber gets one email with the posts published since the last issue. The email goes out through Amazon SES, which costs under $1 a month at 2,000 subscribers. Everything else is built in Elements, the way we want it.
The newsletter is one part of a business application. On Elements, the same application can hold the CRM, invoicing, payments and products, with every customer record in one Postgres database, in place of a separate SaaS subscription for each. If a newsletter is all you send, a SaaS product made for newsletters probably suits you better. If you want one application for your business that you control, and that looks and works the way you want, this is how we build it.
## Setup
We create a new app called `blog` in VS Code, with Claude Code in the lower left and a terminal in the upper right. Elements does not depend on the editor or the agent: the terminal, VS Code and the Claude Code desktop app all work, and so does any AI agent. Once Elements is installed, the editor and the agent know about it.
In the terminal we start Elements, a project server that runs while you work:
```bash Terminal
elements start
```
The browser opens on the starter app. The state in the terminal reads green when the app builds and red when there is an error, whether the agent made it or we did. The agent here is Claude Code with Opus 5.5.
## The prompt
```text Agent
I want to build a blog that has a weekly newsletter that goes out to users. The blog is available to anyone to see, but the newsletter only goes to subscribed users, users that have registered with an email and password. The newsletter should go out every Friday, and I only want to show users the new articles that have been created since the last newsletter went out. Don't send them a repeat set of articles, and if nothing was written, don't send them a blank email.
```
## Claude Code builds the app
While the agent works, the state turns red now and then. The project server answers the agent in microseconds with the error and how to fix it, the agent fixes it, and the state goes back to green.
The agent finished in about 5 to 10 minutes, with 29 passing tests, and clicked through the flow in headless Chrome to check it. Its summary lists what it built: the public blog, accounts, a newsletter every Friday at 9am with no repeats and no blank emails, and an admin page where only administrators write posts. It also gives us demo accounts to sign in with and a page that previews the newsletter.
## Trying the app
The preview shows the newsletter as a subscriber gets it: the two demo posts, then an unsubscribe link at the bottom. We open a post, go back, and create an account with an email and password. Accounts are part of the app and live in its Postgres database, with no outside service for authentication.
## Themes
The app uses the editorial theme from Elements UI, and a theme is one import in the app's stylesheet:
```css app/shared/styles/page.css
@import "@elements/ui";
@import "@elements/ui/themes/editorial";
@import "./vars.css";
```
We ask Claude Code to show the brutalist theme, then luxury, then playful, and go back to editorial. A designer can also make a theme for you, or you can ask the agent for something different. The themes are at [elements.dev/man/ui/themes](https://elements.dev/man/ui/themes).
## What it costs to run
Locally, the newsletter only renders. To send it for real, we ask Claude Code what it would cost to deploy the app and send the email with Amazon SES, for a couple of hundred to a couple of thousand subscribers. Claude Code looks up the prices on the day and comes back with about $26 a month in total:
- About $25 a month to host the app on AWS in Ohio: a small server with a 20 GB disk and a public IP address, and managed Postgres with 20 GB of storage.
- Under $1 a month for Amazon SES at 200 to 2,000 subscribers. SES charges per email sent ([pricing](https://aws.amazon.com/ses/pricing/)).
The server and database prices are on AWS's [EC2](https://aws.amazon.com/ec2/pricing/) and [RDS for PostgreSQL](https://aws.amazon.com/rds/postgresql/pricing/) pages, and they change, so ask your agent for the current figures. Is $26 a month worth paying? For an application that runs your business, with the newsletter as one part of it, we think so, and that one application can replace several subscriptions.
## Sending real email
When you are ready, ask your agent to set up SES. If you have an AWS account, the agent configures SES with the AWS command line. If you do not, it walks you through creating one. The manual's [email setup](https://elements.dev/man/deploy/email) covers the steps.
Deliverability starts with sending through a service like SES rather than a mail server on your laptop. Then the agent adds a few records to your domain, the DKIM signing records and a DMARC record, so receiving servers trust the mail. After that, deliverability holds as long as you are not spamming people, and that includes not letting bots put addresses into your signup form. Ask the agent to design forms that bots cannot fill. No separate deliverability service is needed.
## Code tour: database migrations
The agent changes the database through migrations, plain SQL files in `app/migrations`. Agents write SQL well, so there is no ORM to learn. Save a migration and Elements applies it to the database right away. Edit one and Elements synchronizes the change. Each migration runs in a transaction, so a failure rolls back in full. On a deploy, migrations run against the test database first, and a migration that breaks a test never reaches the app database.
The second migration is seed data. The `@migration env=development` build tag applies it to the development databases only, which is where the demo accounts come from:
```sql app/migrations/20261110121948-demo-rows.migration.sql
/** @migration env=development */
insert into users (
email,
passwordHash,
role,
subscribed
) values (
'admin@example.com',
crypt('password123', genSalt('bf', 12)),
'admin',
true
);
```
## Templates
Each page lives in `app/pages`, and its HTML is an `.ehtml` file: Elements HTML, regular HTML with TypeScript in it. The extension to HTML is small. *Constructor attributes* in parentheses after the tag name are the values a caller provides, typed like function parameters. The account page takes the account and whether the user is an admin:
```ehtml app/pages/account/template.ehtml
Your account
Signed in as {account.email}
```
`SiteLayout` is a shared template the agent factored out for the header, the main column and the footer, so every page reuses it. Templates are symbols, like classes and functions, so go to definition in VS Code jumps from the page to the layout in `app/shared/templates/site-layout`:
```ehtml app/shared/templates/site-layout/index.ehtml
```
Code this close to plain HTML reads easily for the next agent or the next person on the team, and the build tooling, the framework and the HTML language keep the agent from turning it into a tangle as the app grows. The language is covered at [elements.dev/man/html](https://elements.dev/man/html).
## RPC functions and the security check
TypeScript at the top of an `.ehtml` file stays on the server. A function tagged `@rpc` is one the browser can call on the server. Here the subscribe switch on the account page calls `setSubscribed`, which updates the signed-in user and throws if nobody is signed in:
```ehtml app/pages/account/template.ehtml
/** @rpc */
export function setSubscribed(subscribed: boolean) {
session.isLoggedInOrThrow();
sql(`
update users
set subscribed = ${subscribed}
where id = ${session.getOrThrow("userId")}
`);
}
```
Remove the `@rpc` tag, so the browser would run the function itself, and save. The build reports a security error at once: browser code cannot call server code without going through an RPC function. The error shows where the problem is, the call stack and how to fix it, and the agent sees the same error. Put the tag back and the state returns to green. `sql()` and other server code never reach the browser, so every path from the browser to the database goes through an RPC function you can check with the session. The manual has the details at [elements.dev/man/rpc](https://elements.dev/man/rpc).
## Jobs
Jobs are classes that put work on a queue for workers to run outside the request, with retries when they fail. Elements comes with jobs and cron, and the queue and workers are part of every deploy, with no extra subscription. The agent schedules this job every Friday at 9am:
```ts app/jobs/send-newsletter.ts
export class SendNewsletterJob extends Job {
run() {
let result = createIssue();
if (result.sent) {
console.log(`newsletter: ${result.postCount} posts to ${result.recipientCount} subscribers`);
return;
}
console.log(`newsletter: nothing sent (${result.reason})`);
}
}
```
## Emails
Emails are `.ehtml` templates too, marked with the `@email` build tag. The compiler inlines the email's stylesheets into the message, because many email clients do not load external stylesheets. Otherwise an email is regular HTML with constructor attributes, and it renders in the browser for a preview before anyone receives it:
```ehtml app/emails/newsletter/index.ehtml
/** @email */
```
The stylesheet it imports is regular CSS. Elements UI provides style rules and templates to use when you want them, with no CSS utility library between you and the CSS.
## Tests
Tests sit next to the pages they cover, such as `app/pages/account/test.ts`. They let the agent add a feature and know it did not break one that already worked. In a new terminal we run:
```bash Terminal
elements test
```
The results come back at once, all passing, because the project server holds the test state and answers in microseconds. The agent asks the same question after every change. On a deploy the tests run again on the server, and if any fail, the deploy stops.
The manual covers all of this at [elements.dev/man](https://elements.dev/man), and questions go in the comments.
## Transcript
Hey, in this video, we're going to build a newsletter blog that sends out an email to your users every Friday with a summary of the articles that were written that week. And we're not going to use a SaaS subscription to send the emails. We're just going to use raw Amazon AWS SES, which costs a couple cents per month to send emails. Everything else is going to be built in Elements, completely customized to exactly what we want. And the reason that you might do this is if you're building an application for your business that has many different parts. One of the common use cases for Elements is to combine a bunch of different SaaS subscriptions and various business applications that you have spread out all over the place into one application that you can run and build with something like Claude Code or an AI agent and get it to do exactly what you want to do.
And you can have one database where all of your customer information is, your payments and invoices, your customer relationship management, your blog and newsletter. All of that can be combined into one application. If you're just sending email newsletters and that's all you're doing, there's probably a SaaS subscription that would be better suited for that. If you're building an application where you want to have complete control over it and you want it to be exactly the way, to look exactly the way that you want and to add features that you want, this is a really fantastic way to do that. So without further ado, let's build the application and then we'll take a look through the code and see what the agent actually produced and why it's maintainable and a great way to build applications.
Now, let me talk about my setup. I'm gonna create a Visual Studio Code application here, type new app, which is the first one that pops up, but if it doesn't, you can type new app, click that, and we'll call this blog. Now, my setup is Visual Studio Code, where I've got Claude Code in the lower left and a terminal up in the upper right. If you're using a different setup, it's fine. Elements actually doesn't care what you use. Elements is a command line tool with a project server that runs an application framework code that is used to write the application. If you want to write it in the terminal, you can do that. If you want to use Visual Studio Code, you can use that.
If you're using something like Claude Code Desktop, that will work just as well. As soon as you have Elements installed, the various editors will become aware of it and will be able to help you through. And if you're just using Claude Code in the terminal, it will know that Elements is installed and it will help you figure out how to create applications. So you can always just ask your agent what to do if you're not sure. But in our tutorials, we'll use Visual Studio Code, which is free and open source, made by Microsoft. I think it's a great way to build these applications. So whenever we start a new application in Elements, up in the upper left in the terminal, I'll type elements start, and that's going to boot up the application server, and it should open up the browser in the right, and you can see our basic demo application running there on the right.
And if the state is green, okay, that means the application is good to go, and if it's red, it means there's some kind of error. Either your agent produced an error or you produced an error and you need to correct something. So you always have a visual indication of how things are going. Now in the lower left I've got Claude Code, because Elements, our company, has a subscription with Claude Code, but you can use any model you want. If you want to use the standard Visual Studio Code agent user interface, you can do that. If you want to use Claude Code, the extension, which is what I have here, you can do that as well.
So I've got Claude Code, Opus 5.5. I have it on fast mode so we can go a little bit faster, although that would be a little bit more expensive if you're going to do it that way. But we should end up with the same result either way. Okay, let's prompt our way through this. I'm going to say, I want to build a blog that has a weekly newsletter that goes out to users. The blog is available to anyone to see, but the newsletter only goes to subscribed users, users that have registered with an email and password. The newsletter should go out every Friday, and I only want to show users the new articles that have been created since the last newsletter went out.
Don't send them a repeat or a set of articles, or if nothing was written, don't send them a blank email. Okay, I'll press enter and let Claude get to work. Now occasionally you'll see the state in the upper left turn to error, a red error, and that means that the agent's made a mistake, and it will fix it, because the Elements project server gives it feedback in real time, typically in microseconds, as soon as it asks, and it will tell it how to fix it. So the agent will fix the bug, and then you'll see the build change again, and the state will go back to green.
All right, it looks like Claude finished the job. It took about 5 to 10 minutes, and it has a summary here saying that it's built it all. With 29 tests that pass, it clicked through the flow and made sure that it works using headless Chrome. It's a public blog. It's got accounts, a weekly newsletter that runs every Friday at 9 o'clock. No repeats, no blank emails. There's an admin page that only allows authenticated administrative users to create new posts and see other various administrative information, and all the various checks that it did to make sure that it works. There's a couple example accounts that it gives us that we can use to log in, and it shows us how we can sample the newsletter. So let's go take a look at what that email looks like.
It looks like it's /dev/emails/newsletter, and this is what the email will look like to the users. It looks like there's the two posts that were created, and then an unsubscribe button down at the bottom. Let's click through and just make sure that everything works. I can click a post. That looks great. And go back to the post, and let's make sure that I can sign in. We've got an authentication form here that takes email and password. I can create an account, and all of this is baked in. We don't have any external services for authentication. We don't need a separate SaaS subscription for that. It just goes directly into your Postgres database. Before we move on from here, there's some other things we can do.
I like this editorial theme, but we have many themes that are baked into Elements that you can choose. You can also have a designer create one for you, or ask the agent to mess around and give you something a little different. But let's play with a different theme. Let's try a different theme just so I can see what it looks like. Right here in the actual application, can you show me what the brutalist theme looks like? So if you want your users to feel extremely uncomfortable, you can give them this brutalist theme. Oh wow, yeah, that looks like an old 80s style Nintendo game. Okay, so that's brutalist. Let's see what something like, let me try luxury next.
So luxury might be something like a, oh, that looks kind of cool. That looks like maybe what a doctor's office website would look like. All right, let's just do one more while we're playing. Show me the playful theme. All right, not bad. A little bit more playful, playful rounded buttons, a little bit of a purple background. And finally, take me back to editorial. I think that one looks the best. Next up, let's ask Claude how much this will cost and what we have to do to actually send these emails. Right now we've just been sending them locally on our local server, but they're not actually going out. So let's ask: what would it cost to deploy this application and to use Amazon SES or the other, any other recommended service, to actually send these emails on a monthly basis?
Assume I've got a couple hundred, maybe a couple thousand subscribers. Claude's gonna go look up the actual prices that exist and give me a fairly accurate, as of today, price for what it would cost to host this application myself and to send emails on a monthly basis. Okay, a minute or two later we get back our assessment, and Claude recommends that at this volume, it recommends using AWS, and that would cost $26 a month all in to host the application, plus to send the emails, which would be under $1 per month. Which shows the server size you would need, the disk size, what it would cost to get a public IP address, the managed Postgres, and so you're at about $25, maybe $26.
And then down here in the table it will show the different price for sending email for 200 subscribers versus 2,000 subscribers as an example. So, Amazon SES is by far the cheapest at 100 to even a couple thousand subscribers. You're going to be at under $1 per month. So is $26 a month worth it? I think it is if you want to have total control over the application, what it looks like, and if you're building particularly an integrated application for your business that has newsletter or blogs as a part of that, then it's going to make a lot of sense. You'll likely save a lot of money. Now, when you're ready to set up SES, just ask Claude to do that, or your agent to do it, say I'm ready to go, and it will walk you through the process of setting up an account if you don't have one already. And if you do already have an account on AWS, it will use the command line interface to automatically configure it and get you set up.
So within five to ten minutes, you should be able to send emails live on the internet. Now some people are worried about deliverability. There's a lot of, I think, a bit of fear mongering out there about deliverability. It is important. You don't want to end up in people's spam folders, and you want to be able to deliver your emails. The best way to do that is to use a service like SES. So you're not sending from an email server from your laptop, for example. You're actually using a service to do it. So that gets you about at least halfway there. Then the agent, whether it's Claude or Codex or whatever you're using, will add some records to your, some records to your domain that will help it become more trusted.
It's very easy to do, and any agent can do it. So after that you should be fine as long as you're not spamming people or sending, you know, letting bots send emails to people. You'll be fine. Claude will, or your agent will, help you to design forms so that you can't, so that, so the bots can't harvest your forms or put email addresses into your form, so that you don't spam people. As long as you don't do those things, you'll be fine. You don't need a special service to get you to deliverability. You'll get that essentially for free just by using Amazon. Okay, next up, let's go take a look at what code was produced here.
What did Elements actually do under the hood? Why was the agent able to build this so quickly? And why is it a maintainable app that you can hand to another agent or to a team of humans after this? Let's take a look at some of the source code under the hood so you can see how this was built and why Elements is such an effective platform for agents to build on top of. To start, we'll look in the application folder where all of your application code is, and we'll look in the migrations folder. This is how the agent is going to add and change your database. It's a very structured way to modify a database over time so that the agent isn't accidentally messing up your database or dropping it or things like that.
And for a migration file, they're written in raw SQL. So you can see over here, the raw SQL, this is creating a couple of stored procedures. This is the users table. It has the password hash in it and a couple other things going on. And so, this is our baseline schema. And what will happen is the agent will write SQL, and it writes SQL very well. It doesn't need to learn an ORM in order to write this, some kind of abstraction. It just writes SQL, and it has a lot of SQL in its training data. So it does that very, very well. Elements will take care of automatically applying this to the database, and it does it in real time.
As soon as the file is saved, it applies the migration immediately to the database. And if it changes, Elements takes care of syncing that change as well. And on deployment, database migrations are applied very safely, first to the test database and then to the application database only if that succeeds. These migrations are run in a transaction by default, which means if anything in here fails, it automatically rolls back. Now the second migration that it created is kind of interesting because it's our seed data. And if you notice this up here, there's a build tag here called @migration, and it says that this migration is for the development environment only. And this allows us to insert some seed data into the users table.
That is what we were seeing as the example data in our application. So this is really helpful when you're building. You can put seed data and make it as part of the normal migration process. Next up, let's look in the pages folder and check the accounts folder. And this is going to be one of the pages that maps to the account URL. If I open up the eHTML, eHTML stands for Elements HTML, it's an HTML file that has some TypeScript in it. So you can see that we've got an interface here and we've got some RPC functions, which we'll look at in a second. And if I scroll down, we see the HTML for the page. The first thing I want you to notice is that this HTML, the syntax for it looks very much like regular HTML.
This is not a different dialect. There's a little bit of extension to the language. So we can add these constructor attributes, which give this template these variables that can be provided by a caller. But other than that, this really just looks like HTML. So you can hand this off to another agent. You can hand it off to another human being. It's very beautifully written, and it's easy to read, and it's really just a standard language that everybody should know. The second thing to point out is we have this ability to refactor common user interface code into their own templates. So this is an example of the site layout, which has this isAdmin constructor attribute that we can provide it.
And then inside of that is the content, the default content that will go inside of the site layout. So you can create these user interfaces that can be reused across all of your pages. So if I use Go to Definition in Visual Studio Code, which we can do because all of these templates are treated like symbols in Elements. They're just like classes or functions. And here's the reusable template over in the shared templates site layout folder. And it has this constructor attribute, public isAdmin, that's of type Boolean, and the default value is false. And so you can see the agent's quite good at writing this HTML, factoring it out so that this can be reused across your different pages.
And this is going to make all the difference in the world when you're trying to build an application that's maintainable over time. The guardrails that are provided by the build tooling, and also by the framework and the HTML language, give the agent what it needs to make a lot of ferocious progress without worrying about breaking things and creating something that's a total spaghetti mess. So that's the Elements HTML templating system. If we go back here at the top, if you, you might have seen some of these functions here. We can write TypeScript right into the HTML file. They will be stripped out so that they don't go to the browser automatically. And notice this RPC function right here.
This means that this function is going to be callable from the browser to the server. And it has some SQL in it that, again, the agent is very, very fast at writing SQL. And this particular function is called setSubscribed. So this is if, presumably, if somebody wants to subscribe or unsubscribe from the email. And what it does, it says, update the users table, set subscribed equal to the value that's provided, and where the ID is equal to the user ID of the person who's logged in. And if nobody's logged in, throw an error. And if the user's not logged in, throw an error here as well. So this is a secure function that is only callable over the server.
Now let's say that the agent or the user makes a mistake and they forget to make this an RPC function. And now, oops, this is now a function that's gonna go to the browser. And if I save this, I immediately get a security error over on the left. So this security error, it says I've got one error in the template eHTML, and it says security error, you cannot call server code from the browser without going through an RPC function, and it tells you how to fix that problem. And if you scroll down a little bit, it's so zoomed in, it might be a little hard to see, but it shows you where the problem is and it gives you a call stack.
And your agent is gonna see that as well, and the solution is very simple. You just add the RPC tag, build tag back, and now you're back to an okay state. So this is an example of the Elements tooling making it essentially impossible, or very difficult, to cause security problems. And any kind of SQL code that's called can only be called on the server. We will never ship that to the browser. You always have to go through an RPC function, and then you can authenticate that function using the very basic primitives like session or get or throw the session data that's on the session, like the user ID. Next up, I opened up the jobs folder, and jobs are just classes that allow you to put some work into a queue and have it be processed off the main application thread.
And this is helpful for performance, for retrying jobs if they fail. Generally, any production application is going to have some kind of job system. And in Elements that is baked into the tooling. And so you can just write your jobs, or your agent can write the jobs. And the queue and the ability to work, to have workers pull off of that queue and service those jobs, is automatically part of your deployment. You don't need to pay for an extra SaaS subscription to get that. So here's the send newsletter job. This is the job that gets called in order to create a new newsletter and send that out to customers. Finally, let's open up the emails folder.
Emails are also baked into Elements, and they're just regular eHTML files, but we use this @email build tag to mark it as an email. And what that does is it ensures that the style sheets are all inlined into the email that gets sent, because usually email clients like Gmail will not allow style sheets to be pulled in externally, so we have to do a little bit of extra linking in order to get an email to serve properly into most email clients. But other than that, it's just regular HTML. We've got constructor attributes, just like any other HTML template, and this is your email. We can also render these emails in the browser. That gives us a nice, easy way to see what they're going to look like before the user sees it.
Now, the style sheet that's imported looks like regular CSS, just like any other page. And again, just like the other languages in Elements, we try to stick to the standards as much as possible without introducing abstractions. We don't have a CSS tool library that kind of takes you away from regular CSS. You just write CSS. We provide a framework of style rules and templates that you can use if you want, and if you don't want to, you can just have it get out of your way and write regular styles. So, the agent here has created a style sheet for these emails in regular CSS. And we can just import those into the HTML file and they'll be linked automatically.
So this gives you a quick sense of the organization of the application and how the application framework from Elements is being used by the agent to create code that's really maintainable, that can be passed off to another agent over many, many, many sessions, or to another human. Let me show you one last thing, which is that the test files that the agent writes really help to maintain code that doesn't break over time. Here's an example of a test file that's associated with the accounts page. It looks like this has a couple of tests in it. Tests are really great for agents because they help the agent make sure that if it writes a new feature, it didn't break something that was already developed in the past. So, I'm going to open up a new terminal over here on the left, and I'm going to use a new command that we haven't seen yet called elements test.
That's going to show me the results of all the tests. One of the things that you should notice is that there's a lot of tests, first of all. The second thing you should notice is just how fast I got those test results. I'm looking at this quickly, but I think that's 14 microseconds. We got all of the results of the test, and we can see that they're all passing here. So having very fast tests, where the state of the test is available in real time, that the agent can ask, hey, did I break anything, and it gets an answer immediately, not in 10 minutes or 15 minutes, is critically important. And the other thing to note is that when you deploy this application to production, these tests will be run as part of your deployment, and if any of them are failing, it stops the deployment.
So deployments in Elements are very, very safe. Okay, this was a deep dive into some of the code, but I thought it was important to show you around what the agent actually produced, so that you can feel confident that when you build your newsletter blog application, it's going to work the first time and also continue to work over time. If you have any questions, drop them into the comments, and have fun building.