Base44's built-in SendEmail only reaches people who have signed up to your app. To email anyone else, your app needs a paid plan and its own custom domain connected and verified, and even then each email goes to one recipient at a time.
Below: the two domain settings people mix up, the pattern I'd use for a contact form, when to switch to Resend (with its DNS records), and the checklist I run when an email goes missing.
The contact form question
Every portfolio needs a contact form, and every contact form needs a way to tell someone a message arrived. When I planned this site, the obvious move was SendEmail to hello@aatmanjain.com, my own inbox.
Then I read who SendEmail can actually reach. In the end my site doesn't email me at all. Every message is saved to a ContactMessage entity and lands in the inbox in my admin panel. That inbox is the source of truth, and nothing depends on an email arriving.
That's the one idea I'd take from this post: save first, email second. An email is a notification. It should never be the only place your data lives.
Who SendEmail can reach
This is where the confusion usually starts, so here's Base44's rule straight from the docs:
And here's the same rule as a flow, so you can find exactly where your email stops:
Why so strict? My guess is spam. Base44 sends on your app's behalf, and if every free app could email any address, you'd have a spam cannon. The docs back that up in their own way: emailing people who haven't signed up is meant for transactional messages, and your app's sending can be paused if recipients report spam or too many emails fail to deliver.
A few more numbers worth knowing before you build on it:
- integration credit per email from the default sender
- ~1
- per email from a custom email domain
- ~2
- integration credits a month on the Free plan
- 100
- the longest a new email domain can take to verify
- 48 h
There are no mailing lists and no attachments. The call takes to, subject, body and an optional from_name. That's it. No cc, and no reply_to.
"Paid plan" means Starter and up. Starter is also the first plan that lets you connect a domain at all, so it's the lowest plan where the built-in integration can email outside addresses.
The two domains people mix up
This is the bit people trip over, so let's slow down. There are two different domain settings, and they do different jobs.
Your app's custom domain
- The domain your app is served on. For this site, aatmanjain.com.
- With a paid plan, it's what lets SendEmail reach people who haven't signed up.
- You can connect one from the Starter plan.
A custom email domain
- Only changes the sender address.
- From Base44's
no-reply@base44-apps.comto a prefix you pick, like support or info, on your own domain. - Covers every built-in email: one-time passwords, password resets and app invites.
- Won't get you outside recipients on its own.
The docs are blunt about which one matters for outside recipients: "This is your app's own custom domain, the one your app is served on, not a custom email sending domain." So a nice sender address doesn't fix outside delivery on its own.
Setting up a custom email domain
You need the Starter plan or above, and your custom domain has to be connected already. Go to Dashboard, then Domains, then the Email domain section, and click Use Your Custom Domain. Pick a sender name and an address prefix. If you bought the domain from Base44, it handles the DNS for you. If it's from another registrar, add the three CNAME records Base44 shows you:
em CNAME (value from your Base44 dashboard)
s1._domainkey CNAME (value from your Base44 dashboard)
s2._domainkey CNAME (value from your Base44 dashboard)Base44 says you don't need extra TXT records for SPF or DKIM, because those checks run through the CNAMEs. A DMARC record is optional but recommended, and the docs suggest starting relaxed:
_dmarc TXT v=DMARC1; p=none; adkim=r; aspf=r; pct=100Two gotchas. If your domain already has s1._domainkey or s2._domainkey records from another email provider, remove or update them so only Base44's remain. And a custom email domain only sends. To receive mail at that domain, you still need MX records with an email provider, or a business email bought through Base44.
Save first, email second
Here's the pattern I'd use for a contact form that does email you:
Here it is in code. It's an illustrative sketch, not the code on my site, and it runs as a backend function, which needs the Builder plan or above:
import { createClientFromRequest } from "npm:@base44/sdk";
import { secrets } from "base44:runtime";
// Sketch: save the message first, then try to send a notification.
export default async function (req: Request) {
try {
const base44 = createClientFromRequest(req);
const { name, email, message } = await req.json();
// validate name, email and message here, and trim them to sane lengths
// 1. The save is what matters. The service role writes past RLS.
const row = await base44.asServiceRole.entities.ContactMessage.create({
name, email, message, status: "new",
});
// 2. The email is a best-effort extra.
try {
await base44.asServiceRole.integrations.Core.SendEmail({
to: secrets.get("NOTIFY_EMAIL"), // a signed-up user, unless your app meets the rules above
subject: `New message from ${name}`,
body: `${message}\n\nFrom: ${email}`,
from_name: "My site",
});
} catch (err) {
// needs an email_error field on the entity
await base44.asServiceRole.entities.ContactMessage.update(row.id, { email_error: String(err) });
}
return Response.json({ ok: true });
} catch (error) {
return Response.json({ error: error.message }, { status: 500 });
}
}One catch: the docs say an email that isn't sent shows up in your app logs with the reason. So don't rely on that catch block alone. Check the logs too.
Why a backend function and not a call straight from the page? Two reasons. Base44's security scan has a credit protection check that flags credit-using features, email included, that strangers could run directly and spend your integration credits on. And a function can write with the service role while the entity stays admin only for everything, which is a stricter version of the letterbox pattern from my RLS post. New to them? My backend function glossary entry covers the basics.
When to use Resend instead
Resend is an email API you connect to your app. Emails go through your own Resend account instead of Base44's built-in delivery, so Base44's rules about who you can email stop being the question. Resend's rules and your verified domain are what count. I run Resend in production on client apps.
I reach for it when an app needs outside recipients and doesn't meet the rules above, or when it needs proper templates, a reply-to address or delivery tracking in the Resend dashboard. The Base44 Resend integration needs the Builder plan or above, and it works through backend functions.
Resend's DNS records
In Resend, add your domain. Resend strongly recommends sending from a subdomain, like updates.yourdomain.com, rather than your root domain. The Records tab then shows what to add. On a typical setup, that's:
send MX feedback-smtp.us-east-1.amazonses.com (priority 10)
send TXT "v=spf1 include:amazonses.com ~all"
resend._domainkey TXT p=... (your key, copied from Resend)Those are example values from Resend's Cloudflare guide. On a subdomain like updates, the names become send.updates and resend._domainkey.updates. The region in the MX value varies, and Resend says domains created after August 2026 may show CNAME records instead of the MX and TXT pair. Copy exactly what your Records tab shows. After it verifies, Resend recommends adding DMARC too.
Here are both setups side by side, because putting Resend's records where Base44's should go (or the other way round) is the classic mix-up:
One Base44-specific catch from the docs: if you bought your domain through Base44, its MX records can't be edited by hand. Contact Base44 support with the host, value and priority for the send record, and they'll add it for you.
Calling Resend from a backend function
You can connect Resend through Base44's integrations (click Use this integration, then paste your API key), or call its API yourself from a backend function with the key in secrets:
import { secrets } from "base44:runtime";
// Inside your handler. FROM_ADDRESS and REPLY_TO are secrets too, so no addresses live in code.
const res = await fetch("https://api.resend.com/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${secrets.get("RESEND_API_KEY")}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
from: secrets.get("FROM_ADDRESS"),
to: [email],
reply_to: secrets.get("REPLY_TO"),
subject: "Thanks, we got your message",
html: "<p>We'll get back to you soon.</p>",
}),
});
if (!res.ok) console.error("Resend said no", res.status, await res.text());Call secrets.get() inside the handler, not at the top of the file. The docs say secrets resolve per request, so a read at module load comes back undefined.
When an email doesn't arrive: my checklist
Check the app logs
Base44 logs every email it didn't send, with the reason: the plan, the custom domain or a sending limit.
Has the recipient signed up?
If not, you need a paid plan and a verified custom domain on the app itself.
Is that domain verified?
Connected isn't enough. Check it isn't still pending.
One recipient per email?
Built-in emails go to one recipient at a time, with no mailing lists and no attachments.
Has sending been paused?
Spam reports or lots of failed deliveries can do that.
Any integration credits left?
Free has 100 a month. Each email uses about 1, or about 2 from a custom email domain.
Email domain still pending?
Your app keeps sending from
no-reply@base44-apps.comuntil it's Active, and DNS can take up to 48 hours.Old DKIM records?
Remove or update
s1._domainkeyors2._domainkeyrecords left by another provider.On Resend?
Records sit on the
sendsubdomain, not the root. CNAMEs must be DNS only (grey cloud on Cloudflare). If your DNS provider glued your domain onto a value, add a trailing dot.The boring ones
Check the spam folder, and check the address for typos.
Still stuck after all ten? Email me at hello@aatmanjain.com and tell me what you've tried. It's the one address I promise gets read.
FAQ
Why won't Base44 SendEmail send to an external email address?
SendEmail only reaches people who have signed up to your app, unless your app is on a paid plan and has a custom domain connected and verified. With both in place, it can email outside addresses, one recipient per message. Any email that isn't sent shows up in your app logs with the reason.
Do I need a custom email domain to email people outside my Base44 app?
No. The docs say the requirement is a paid plan plus your app's own custom domain, the one your app is served on. A custom email domain only changes the sender address, from Base44's default no-reply@base44-apps.com to an address on your domain.
How many credits does Base44 SendEmail use?
About 1 integration credit per email from the default sender, or about 2 when you send from a custom email domain. The Free plan includes 100 integration credits a month, so check your plan before you add email to a busy flow.
Should I use SendEmail or Resend?
Use SendEmail for simple transactional emails to people who've signed up to your app. Use Resend, on the Builder plan and above, when you need outside recipients your setup can't reach, higher volume, a reply-to address, templates or delivery tracking in the Resend dashboard.




