the easy way to selfhost the Resend SDK
  • TypeScript 90%
  • CSS 5.6%
  • Dockerfile 3.6%
  • JavaScript 0.8%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-05-28 09:22:50 +02:00
.github/workflows feat: selfsend 2026-05-27 15:43:26 +02:00
app fix: dont prerender signin 2026-05-28 09:22:50 +02:00
components feat: selfsend 2026-05-27 15:43:26 +02:00
demo feat: selfsend 2026-05-27 15:43:26 +02:00
drizzle feat: selfsend 2026-05-27 15:43:26 +02:00
hooks feat: initial commit 2026-05-27 08:23:03 +02:00
lib feat: selfsend 2026-05-27 15:43:26 +02:00
public feat: initial commit 2026-05-27 08:23:03 +02:00
.dockerignore fix: exclude demo from build 2026-05-27 20:55:28 +02:00
.gitignore feat: base 2026-05-27 10:09:12 +02:00
.prettierignore feat: initial commit 2026-05-27 08:23:03 +02:00
.prettierrc feat: initial commit 2026-05-27 08:23:03 +02:00
bun.lock feat: selfsend 2026-05-27 15:43:26 +02:00
components.json feat: initial commit 2026-05-27 08:23:03 +02:00
compose.test.yml feat: selfsend 2026-05-27 15:43:26 +02:00
compose.yml feat: selfsend 2026-05-27 15:43:26 +02:00
Dockerfile feat: selfsend 2026-05-27 15:43:26 +02:00
drizzle.config.ts feat: base 2026-05-27 10:09:12 +02:00
eslint.config.mjs feat: initial commit 2026-05-27 08:23:03 +02:00
instrumentation.ts feat: selfsend 2026-05-27 15:43:26 +02:00
LICENSE feat: selfsend 2026-05-27 15:43:26 +02:00
next.config.ts fix: config 2026-05-27 15:46:59 +02:00
package.json feat: selfsend 2026-05-27 15:43:26 +02:00
postcss.config.mjs feat: initial commit 2026-05-27 08:23:03 +02:00
README.md feat: selfsend 2026-05-27 15:43:26 +02:00
tsconfig.json fix: exclude demo from build 2026-05-27 20:55:28 +02:00

Selfsend

Selfsend is a self-hosted, Resend-compatible email API that sends through your own SMTP server. It is meant for hobbyists that want to keep using Resend-style SDK calls while routing delivery through infrastructure they control.

Note

THIS DOES NOT IMPLEMENT ALL OF THE RESEND API

The most common features, namely the sending of emails is implemented tho

The app includes:

  • A Resend-compatible send API for single and batch email sends.
  • Project-scoped ss_ API keys.
  • A small web UI for projects, default senders, API keys, and send history.
  • SMTP delivery through Nodemailer.
  • SQLite persistence with Drizzle.
  • email/password, OIDC sign-in, and optional sender allowlists.

API Usage

Point Resend-compatible clients at your Selfsend base URL and use a project API key from the dashboard.

curl http://localhost:3000/emails \
  -H "Authorization: Bearer ss_your_project_key" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Selfsend <[email protected]>",
    "to": "[email protected]",
    "subject": "Hello",
    "html": "<p>Sent through Selfsend.</p>"
  }'

With the official Resend Node SDK, set the SDK base URL before initializing the client:

RESEND_BASE_URL=http://localhost:3000
import { Resend } from "resend";

const resend = new Resend("re_your_project_key");

await resend.emails.send({
  from: "[email protected]",
  to: "[email protected]",
  subject: "Hello",
  html: "<p>Sent through Selfsend.</p>",
});

Implemented Resend-compatible routes include POST /emails, POST /emails/batch, GET /emails, GET /emails/:id, attachment retrieval, and basic API key listing/creation/deletion. Features that SMTP cannot provide, such as hosted domains, broadcasts, inbound routing, and webhooks, are not implemented.

Configuration

Environment variables:

Variable Description
DB_FILE_NAME libSQL/SQLite URL. Docker examples use file:/data/selfsend.db.
BETTER_AUTH_URL Public base URL of the app, for auth redirects and callbacks.
BETTER_AUTH_SECRET Random production secret, at least 32 characters.
DISABLE_PASSWORD_SIGNIN Set to true to disable email/password sign-in.
SMTP_HOST SMTP host used for delivery.
SMTP_PORT SMTP port, usually 587, 465, or 1025 for MailCatcher.
SMTP_SECURE true for implicit TLS, otherwise false.
SMTP_USER / SMTP_PASSWORD Optional SMTP credentials.
SMTP_FROM Optional fallback sender when a project/request omits from.
ALLOWED_SENDERS Optional comma-separated sender allowlist.
OIDC_* Optional generic OIDC provider settings.
API_KEY_RATE_LIMIT_ENABLED Wheter to rate limit the API

OIDC callback URL:

https://your-selfsend-host.example.com/api/auth/oauth2/callback/oidc

Set OIDC_PROVIDER_ID to something other than oidc if you want a different callback suffix.

Sender Restrictions

Set ALLOWED_SENDERS to restrict which from addresses Selfsend can use. Leave it empty to allow any sender.

Patterns are comma-separated and matched case-insensitively:

Docker

The published image is:

ghcr.io/gametec-live/selfsend:latest

Run the production-style example:

mkdir -p data
chmod 777 data
docker compose up

The default compose.yml uses ./data:/data and stores SQLite at /data/selfsend.db. Replace the placeholder auth, SMTP, and OIDC values before production use. It is also recommended to use a volume to store the database instead of a bind mound with broad permissions.

Test Stack

The test stack runs Selfsend with MailCatcher:

mkdir -p test-data
chmod 777 test-data
docker compose -f compose.test.yml up --build

MailCatcher endpoints:

  • SMTP: localhost:1025
  • UI: http://localhost:1080

Selfsend runs at http://localhost:3000.

If startup fails with Unable to open connection to local database /data/selfsend.db: 14, the bind-mounted directory is not writable by the container user. Fix the host directory permissions and recreate the stack:

Development

bun install
bun run dev

Useful scripts:

bun run lint
bun run typecheck
bun run build
bun run db:generate
bun run db:migrate
bun run db:push

The app uses SQLite through Drizzle/libSQL. Generated migrations live in drizzle/, and startup migrations run automatically.

Publishing

The GitHub Actions workflow at .github/workflows/docker.yml builds Docker images for pull requests and publishes to GHCR on pushes to main or v* tags.

Published tags:

  • latest for the default branch.
  • sha-<commit> for pushed commits.
  • Git tag names for v* releases.