Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 

Repository files navigation

MailKite

Supabase + MailKite

Email in and out of your Supabase app.
Inbound mail becomes rows in an emails table. Outbound mail is one Edge Function call.
Two functions, one migration, no servers.

MailKite docs · Supabase Edge Functions · mailkite.dev

What's in the box

File What it does
supabase/functions/mailkite-inbound/index.ts Receives the MailKite webhook: verifies the HMAC signature, inserts the email into emails.
supabase/functions/send-email/index.ts Sends email (e.g. a welcome email) via mk.send(). JWT-protected — only your signed-in users can call it.
supabase/migrations/20260703120000_emails.sql The emails table + RLS (service-role writes, users read their own mail).
supabase/config.toml Per-function JWT settings — verify_jwt = false for the webhook (MailKite can't send a Supabase JWT; the signature check is the auth).

Using an AI builder?

Paste this into Lovable, Bolt, Cursor, Claude Code, or any coding agent:

Add MailKite email to this Supabase app so it receives email as rows in an
`emails` table and can send email from an Edge Function. Use the reference
implementation at https://github.com/mailkite/supabase-mailkite — copy its two
Edge Functions (mailkite-inbound with verify_jwt = false in config.toml,
send-email with JWT on), its emails-table migration with RLS, and its README
steps: verify my domain with MailKite first, set the MAILKITE_API_KEY and
MAILKITE_WEBHOOK_SECRET secrets, deploy the functions, then point my MailKite
domain's webhook at the deployed mailkite-inbound URL. Use the `mailkite` npm
package (npm: specifier) — verify webhooks with MailKite.verifyWebhook(), send
with mk.send(). Do not hand-roll HMAC or raw fetch calls.

The 5-minute path

Prereqs: a Supabase project, the Supabase CLI (supabase link'd to it), and a MailKite account (free).

1. Verify your domain with MailKite — do this first

MailKite gates receiving and sending until your domain proves control via DNS (inbound = MX, outbound = SPF + DKIM), so start the DNS clock before anything else:

npx @mailkite/cli signup                      # or: login
npx @mailkite/cli domains add mail.yourapp.com   # prints the DNS records to add
# …add the records at your DNS provider, then:
npx @mailkite/cli domains verify <dom_id>

(Or do it in the dashboard — Domains → Add domain.)

2. Copy this repo into your project

git clone https://github.com/mailkite/supabase-mailkite

then copy supabase/functions/, supabase/migrations/, and the two [functions.*] blocks from supabase/config.toml into your own supabase/ directory (or work from the clone directly).

3. Create the emails table

supabase db push

4. Set the secrets

npx @mailkite/cli secret get                  # your webhook signing secret (whsec_…)

supabase secrets set MAILKITE_API_KEY=mk_live_…
supabase secrets set MAILKITE_WEBHOOK_SECRET=whsec_…

(API keys live in the MailKite dashboard → API keys.)

5. Deploy the functions

supabase functions deploy mailkite-inbound
supabase functions deploy send-email

config.toml already sets verify_jwt = false for mailkite-inbound. If you deploy without that config, pass --no-verify-jwt — otherwise Supabase rejects every webhook delivery with 401 before your code runs.

6. Point MailKite at the function

npx @mailkite/cli webhook set <dom_id> https://<project-ref>.supabase.co/functions/v1/mailkite-inbound
npx @mailkite/cli webhook test <dom_id>       # sends a signed test event end-to-end

Or from code:

import { MailKite } from "mailkite";

const mk = new MailKite(process.env.MAILKITE_API_KEY);
await mk.setWebhook("<dom_id>", {
  url: "https://<project-ref>.supabase.co/functions/v1/mailkite-inbound",
});

7. Send yourself an email

Email anything@mail.yourapp.com, then:

select from_addr, subject, received_at from emails order by received_at desc;

There's your inbox, in Postgres.

Sending (the welcome-email case)

Edit FROM in send-email/index.ts to an address on your verified domain, then call it from your app — supabase.functions.invoke sends the user's JWT automatically:

const { data, error } = await supabase.functions.invoke("send-email", {
  body: {
    to: user.email,
    subject: "Welcome to YourApp",
    html: "<p>You're in. Reply to this email and it lands in your database.</p>",
  },
});

Server-side (a database webhook, a cron job, another function) works the same way — or call MailKite directly with mk.send().

How the pieces fit

inbound   someone@mail.yourapp.com ──▶ MailKite ──▶ POST /functions/v1/mailkite-inbound
                                        (signed)      verify signature → insert into emails
outbound  your app ── supabase.functions.invoke("send-email") ──▶ mk.send() ──▶ recipient
  • Signature verification — every delivery carries an x-mailkite-signature header (HMAC-SHA256 over the raw body). MailKite.verifyWebhook() checks it locally, timing-safe, with replay protection. Never skip it on a public URL.
  • RLS — the table has no insert policy; only the function's service-role client writes. Signed-in users can select mail where to_addr matches their own email. Adjust the read policy to your app's shape.
  • Retries — return non-2xx and MailKite redelivers; the function treats a duplicate-id insert as already-processed and acknowledges it.

Local development

supabase start
echo "MAILKITE_WEBHOOK_SECRET=whsec_…" >> supabase/functions/.env
echo "MAILKITE_API_KEY=mk_live_…" >> supabase/functions/.env
supabase functions serve --env-file supabase/functions/.env

Expose the local URL (e.g. ngrok http 54321) and point webhook set at https://<tunnel>/functions/v1/mailkite-inbound to receive real mail locally.

Troubleshooting

Symptom Fix
Webhook deliveries show 401 in MailKite verify_jwt is still on — redeploy with the config.toml from this repo (or --no-verify-jwt).
Function logs bad signature Wrong or rotated secret — re-run npx @mailkite/cli secret get and reset MAILKITE_WEBHOOK_SECRET.
Nothing arrives at all Domain not verified yet — npx @mailkite/cli domains verify <dom_id> and check the MX record.
send-email returns 401 Caller has no Supabase JWT — invoke it via supabase.functions.invoke from a signed-in session.
send-email returns a MailKite error Usually "domain not verified" for the FROM address — finish step 1.

License

MIT

About

Supabase + MailKite — Edge Functions that receive inbound email as rows and send transactional email.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages