Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ All notable changes to this project will be documented in this file.
- Dashboard with summary stats and recent campaigns
- Docker Compose setup for local development (Postgres, MinIO, app, worker)
- Production Dockerfile with standalone Next.js output
- Embeddable signup forms with builder UI, hosted form pages at `/f/:id`, and a drop-in JS embed snippet
- Optional double opt-in flow with `pending` contact status, confirmation token, transactional confirmation email job, and a confirmation page at `/confirm/:token`
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Self-hostable broadcast email tool. Manage contact lists, design emails with a v
- **Campaign Sending** - Queue-based sending via pg-boss, with scheduling, cancellation, and per-provider rate limiting
- **Open and Click Tracking** - Tracking pixel for opens, link wrapping for clicks, per-campaign analytics with charts
- **Unsubscribe Handling** - One-click unsubscribe with List-Unsubscribe header support
- **Signup Forms** - Build forms in the dashboard, share a hosted page or embed a JS snippet on any site, with optional double opt-in
- **REST API** - Full API with Bearer token auth for programmatic access to lists, contacts, and campaign data
- **Self-Contained** - Postgres for everything (data, queue, migrations). No Redis, no external queue. Optional MinIO/S3 for file uploads
- **Docker Ready** - Single `docker compose up` for local development, production-ready Dockerfile included
Expand Down Expand Up @@ -169,6 +170,47 @@ Webhooks let Mailpost track bounces and complaints reported by the email provide
3. The endpoint will auto-confirm the subscription
4. In SES, configure a Configuration Set to publish bounce and complaint notifications to the SNS topic

## Signup Forms

Forms turn your lists into something people can self-subscribe to. Each form is tied to a single list. Submissions create a contact in that list, respecting the global suppression list.

### Creating a form

1. Go to **Forms** in the sidebar and click **New Form**.
2. Pick a name and a target list.
3. In the builder, configure fields (email is required, plus any combination of text, checkbox, and select fields), set a success message or a redirect URL, and toggle double opt-in if you want a confirmation step.
4. Save.

### Sharing the form

Each form has two surfaces:

- **Hosted page**: `${APP_URL}/f/<form-id>`. Share the link directly or link from a navigation menu.
- **JS embed snippet**: drop the snippet into any HTML page. The form renders inline and posts back to your Mailpost instance.

```html
<script
src="https://mail.example.com/api/public/forms/FORM_ID/embed.js"
data-form-id="FORM_ID"
async
></script>
```

The embed renders the form via DOM injection so it inherits your site's styling. For maximum CSS isolation, add `data-mode="iframe"` and the snippet renders the hosted page inside an iframe instead.

### Double opt-in

When double opt-in is enabled, a submission creates a contact with status `pending` and queues a confirmation email through the chosen provider. The contact is not eligible for campaign sends until they click the link, which transitions them to `active` via `${APP_URL}/confirm/<token>`.

The confirmation email is authored with the same block editor used for campaigns. Use the merge tag `{{confirm_url}}` in a button block. Tracking pixels and the unsubscribe footer are deliberately omitted from confirmation emails, since the contact has not opted in yet.

### Suppression and abuse protection

- Submitted emails are checked against the global suppression list before any contact is created. Suppressed emails get the same success response so the form can't be used to probe membership.
- Each form is rate-limited per source IP.
- A hidden honeypot field rejects bots silently.
- Submissions use `application/x-www-form-urlencoded` so the embed avoids CORS preflights from third-party origins.

## Using AWS S3 Instead of MinIO

For production, switch from MinIO to AWS S3 by changing three variables:
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ The features below build out the API surface so it's safe, observable, and compl
- [ ] **Tags on contacts** [Effort: S]: Lightweight, multi-valued labels that can be applied to contacts manually or by automations. Used as filters in segments and as triggers in workflows.
- [ ] **Segments** [Effort: M]: Saved filter queries against a list (e.g. "opened anything in the last 30 days, country = US"). Used as a campaign target instead of an entire list. Can be combined with tags, fields, and engagement signals.
- [ ] **Double opt-in** [Effort: M]: Per-list toggle that requires new contacts to confirm via emailed link before being marked active. Confirmation page and token-based confirmation flow.
- [ ] **Embeddable signup forms** [Effort: M]: Form builder, hosted form pages, and a JS embed snippet. Submissions flow into a list and respect double opt-in if enabled.
- [x] **Embeddable signup forms** [Effort: M]: Form builder, hosted form pages, and a JS embed snippet. Submissions flow into a list and respect double opt-in if enabled.
- [ ] **Saved templates library** [Effort: S]: Reusable templates with thumbnails. Save any campaign as a template, start new campaigns from any template.
- [ ] **Asset library** [Effort: M]: Central image and file manager. Upload once, browse and drop into any campaign editor. Replaces per-campaign image uploads.
- [ ] **A/B testing** [Effort: M]: Subject line and content variants on a campaign. Send variants to a test sample, pick the winner by open or click rate, and send the winner to the rest.
Expand Down
Loading
Loading