- TypeScript 90%
- CSS 5.6%
- Dockerfile 3.6%
- JavaScript 0.8%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .github/workflows | ||
| app | ||
| components | ||
| demo | ||
| drizzle | ||
| hooks | ||
| lib | ||
| public | ||
| .dockerignore | ||
| .gitignore | ||
| .prettierignore | ||
| .prettierrc | ||
| bun.lock | ||
| components.json | ||
| compose.test.yml | ||
| compose.yml | ||
| Dockerfile | ||
| drizzle.config.ts | ||
| eslint.config.mjs | ||
| instrumentation.ts | ||
| LICENSE | ||
| next.config.ts | ||
| package.json | ||
| postcss.config.mjs | ||
| README.md | ||
| tsconfig.json | ||
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:
[email protected]allows one exact address.@example.comallows any sender at that domain.*[email protected]allows matching addresses such as[email protected].*@example.comis also valid, but@example.comis the simpler domain form.
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:
latestfor the default branch.sha-<commit>for pushed commits.- Git tag names for
v*releases.