# Posta: a self-hosted email platform with an HTTP API in front of SMTP

> Posta is a Go and AGPL-3.0 email platform that sends, receives and relays mail through one HTTP API, with templates, campaigns, form intake, bounce handling, analytics and a dashboard. It suits a team that wants to own its email infrastructure rather than rent it, and it is a self-hosted service with a Postgres, a Redis queue and an SMTP dependency, not a library.

**goposta/posta** — Self-hosted email delivery platform that enables applications to send emails via HTTP APIs while handling SMTP delivery, templates, storage, security, and analytics. Fully self-hostable alternative to services like SendGrid or Mailgun.

- Repository: https://github.com/goposta/posta
- Website: https://goposta.dev
- Stars: 620 · Forks: 73
- Language: Go
- License: AGPL-3.0
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/goposta-posta

## Outbound, inbound and relayed mail behind one API

Posta is a self-hosted email platform for sending, receiving and relaying email through a unified API and SMTP, and the README describes it as a self-hosted, developer-first email platform that handles outbound, inbound and relayed email, driven by a single HTTP API, with SMTP available for the paths that need it.

Those three directions are the product, and the third is the interesting one.

Outbound is send transactional and marketing email over your own SMTP, with templates, localization, campaigns, subscriber lists, bounce and complaint handling, and delivery analytics. Inbound is receive email at your domains, parse messages and attachments, and forward the structured payloads to your application via webhooks. Relay is point an existing SMTP client at Posta so its mail flows through the same outbound pipeline as the API, which means you can migrate to the HTTP API at your own pace. That third mode is what makes the project a drop-in option for a team that already has SMTP calls in its code and does not want to rewrite them.

There is a fourth capability that the feature list calls Forms, and it is unusual for an email platform. A website contact form can post straight to Posta, each submission is scanned for spam, and a person replies to the sender from the dashboard through the same domains and suppression list as everything else.

The project positions itself as a fully self-hostable alternative to services like SendGrid, Mailgun and Postmark, with the stated benefit of complete ownership of your email infrastructure, data and deliverability.

The implementation is Go, the licence is AGPL-3.0, the default branch is main, and the releases are recent and fast moving, with v0.17.0 on 2026-10-17, a v0.17.0-rc.1 two days earlier, and v0.16.0 on 2026-10-11. The last push was on 2026-10-22.

## The send API, and the shape of a request

The quick example is a single curl call, and it shows the whole request shape.

```bash
curl -X POST http://localhost:9000/api/v1/emails/send \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme <hello@example.com>",
    "to": ["Jonas Kaninda <jonas@example.com>","bob@example.com"],
    "subject": "Hello from Posta",
    "html": "<h1>Hello!</h1>"
  }'
```

The address fields are the first thing to notice, because it is a decision rather than an accident. The README states that the from, to and other address fields accept a plain address such as hello@example.com or the RFC 5322 display-name format such as Acme <hello@example.com>.

The response is deliberately thin, because the work is queued rather than done.

```json
{
  "id": "0ae4b04e-5c64-4b2f-bad6-460f8d5d98b3",
  "status": "queued"
}
```

That status is the visible edge of the architecture. The delivery features list names async processing with Redis and Asynq, automatic retries and priority queues, and the dependency list confirms both, with hibiken/asynq and redis/go-redis in go.mod alongside the SMTP client emersion/go-smtp. A send returns an id and a queued status, the worker picks the job up, and the result of the send lands in the analytics and the bounce and complaint handling rather than in the HTTP response.

The port in the example, 9000, is the service's own default, and the .env.example sets POSTA_PORT=9000. The endpoint path prefixes are /api/v1, and the form intake endpoint has a different shape, POST /api/v1/f/{key}, with no API key on the page.

The address verification in the delivery feature list is also worth noting, because it is the sort of thing that silently breaks deliverability if it is skipped. It covers syntax, MX, and disposable and role-account checks.

## Form intake without JavaScript, and no auto-responder

The form feature is the part of Posta that goes furthest beyond sending mail, and the README is careful about how it behaves.

A form posts directly to Posta with no backend of your own and no JavaScript, using a public ingest endpoint per form.

```html
<form action="http://localhost:9000/api/v1/f/YOUR_FORM_KEY" method="POST">
  <input type="email" name="email" required>
  <textarea name="message" required></textarea>
  <button type="submit">Send</button>
</form>
```

The submission is scanned, stored, and shown in the dashboard, where a person replies to the sender, and the README is explicit that there is no auto-responder.

That single sentence shapes the whole feature. A form intake system that auto-replies is a spam magnet, and a human-in-the-loop reply is also what makes the feature usable for a contact form on a small site where the owner wants to answer personally. The supporting list is built around that: spam scoring with a score, the reasons behind it and an action ladder of allow, flag, quarantine and reject; workspace spam filters by keyword, phrase, regex, email, domain or IP with a dry run before you commit; inbox states, assignment, attachments and a live event stream; and digest notifications to the addresses each form nominates, immediate, hourly or daily.

The abuse controls are the other half. There is an origin allowlist, a honeypot field, single-use nonces, and rate limits per IP, per form, per sender and per workspace. The endpoint takes plain HTML form posts, JSON and multipart uploads.

And the whole thing is off by default behind POSTA_MESSAGES_ENABLED, which is the same pattern the relay mode uses. Both features that widen the attack surface are opt in.

## Running it, and the two services it needs

The compose file shows the deployment shape, and it is a two-service-plus-worker arrangement rather than a single binary.

The posta service builds from a Dockerfile in the docker directory, publishes port 9000, and points at a Postgres and a Redis. The worker service is the same image with a different command, and it carries its own concurrency and retry settings.

```yaml
  worker:
    build:
      context: .
      dockerfile: docker/Dockerfile
    command: ["worker"]
    environment:
      POSTA_DB_URL: "host=posta-db user=posta password=posta dbname=posta port=5432 sslmode=disable"
      POSTA_REDIS_ADDR: "posta-redis:6379"
      POSTA_WORKER_CONCURRENCY: "10"
      POSTA_WORKER_MAX_RETRIES: "5"
```

The data services are postgres:17-alpine and redis:7-alpine, with a health check on each, and the application waits for both to be healthy before starting. The application itself has a health check on /healthz, and the whole thing restarts unless stopped.

The .env.example is where the security posture is decided, and it is unusually explicit about it. The JWT signing secret is required in production and 32 or more characters is recommended, because it signs dashboard sessions, tracking links and the email stamper, so anyone who knows it can forge a session. The file says to generate one with openssl rand -hex 32, and it documents that production refuses to start if the secret is empty or left at the placeholder, and warns if it is shorter than 32 characters.

The first admin account is guarded the same way. The default credentials are used only to create the first account on an empty database, and in production the password must be at least 12 characters and not a known placeholder, or the initial seed is refused. The compose file in the repository says so in a comment, noting that the placeholder credentials below are for a local quick start and that production rejects them, with a pointer to the examples directory for a deployable template that requires real secrets.

Other switches are worth knowing about. POSTA_DEV_MODE stores emails without sending, which is what you want in development. POSTA_OPENAPI_DOCS turns on Swagger UI and ReDoc, and POSTA_METRICS_ENABLED turns on Prometheus metrics, with the Prometheus client in the dependency list to match. Rate limits are separately configurable for login and register, and there are hourly and daily per-user email limits.

## What Posta is not, and where the boundary sits

Three limits are visible in the documentation, and they are the ones to weigh before committing.

The first is that self-hosting email means owning deliverability. There is no shared IP pool and no established sending reputation, and the README's feature list shows what the project does about it, with domain verification for SPF, DKIM and DMARC, verified sender enforcement, and shared SMTP pools for teams. Those are the tools, but the reputation itself is a property of your own infrastructure and your own sending behaviour, and it is the part no feature list can give you.

The second is the AGPL-3.0 licence, which is the most restrictive of the common open source licences and is the thing to read carefully if Posta is a service you expose over HTTP. The README does not make any claims about the licence, so the terms are the ones in the LICENSE file at the top of the repository, and a legal reader should look at them before the code.

The third is the operational surface. A single binary is not what this is. The Go module pulls in the AWS SDK v2 with the S3 service client, a Postgres driver plus GORM, Redis, Asynq, a cron library, Prometheus client, an OIDC library for the OAuth and SSO login, a TOTP library for the two-factor authentication, a premailer for stylesheet inlining, goquery for HTML parsing, and godotenv for the environment file. The AWS SDK is there for the S3 source in the import path, and the rest is the platform.

Against the hosted alternatives, the difference in approach is ownership. SendGrid, Mailgun and Postmark run your sending through shared infrastructure and charge per message, and you get deliverability done for you. Posta replaces that with your own servers, your own domain reputation and your own queue, and the trade is the operational burden plus the AGPL terms against the per-message cost and the loss of control over your data and your sending infrastructure.

For a team with a low message volume and a real aversion to handing email data to a third party, that is a reasonable trade. For a team that needs to send at volume with predictable deliverability and no operations staff, it usually is not.

## Conclusion

Posta fits a team that has decided to run its own email infrastructure, needs an HTTP send API rather than raw SMTP, and wants to own the deliverability data, and it does not fit a team that wants a hosted service with a support contract, since the operator takes on the IP reputation, the queue and the deliverability work. Before deploying, read the .env.example carefully, because with POSTA_ENV set to production the server refuses to start on a missing, placeholder or too-short POSTA_JWT_SECRET and refuses to seed the first admin account with a placeholder POSTA_ADMIN_PASSWORD.

## FAQ

### What are the three ways Posta handles email?

Outbound sends transactional and marketing email over your own SMTP, with templates, campaigns, subscriber lists, bounce handling and analytics. Inbound receives email at your domains and forwards structured payloads to your application via webhooks. Relay points an existing SMTP client at Posta so mail flows through the same outbound pipeline as the API.

### How does a website contact form send email through Posta?

The form posts directly to a public ingest endpoint per form at POST /api/v1/f/{key}, with no API key on the page and no JavaScript needed. The submission is scanned and stored and shown in the dashboard, where a person replies to the sender, and there is no auto-responder.

### What does Posta need to run in production?

The compose file brings up Postgres and Redis alongside the application and a worker service. With POSTA_ENV set to production the server refuses to start on a missing, placeholder or too-short POSTA_JWT_SECRET, which signs dashboard sessions, tracking links and the email stamper, and it refuses to seed the first admin account with a placeholder POSTA_ADMIN_PASSWORD.

### How does the Posta send API behave, and is the email sent immediately?

A send is a POST to http://localhost:9000/api/v1/emails/send with a bearer token and a JSON body whose from and to fields accept either a plain address or the RFC 5322 display-name format. The response returns an id and a status of queued, because delivery is processed asynchronously with Redis and Asynq, with automatic retries and priority queues.

## Sources

- [goposta/posta on GitHub](https://github.com/goposta/posta)
- [License: AGPL-3.0](https://github.com/goposta/posta/blob/main/LICENSE)
- [Project website](https://goposta.dev)
- [README](https://github.com/goposta/posta/blob/main/README.md)
- [Releases](https://github.com/goposta/posta/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/goposta-posta
