-
released this
2026-10-26 09:14:37 -04:00 | 0 commits to main since this releasePhase 13: A/B testing, project API keys, API sending providers, and MJML. PR #69.
Upgrading: back up the database first. Two additive migrations run on start (
ab_testing,api_keys). New runtime dependency:mjml5.4.1 (exact, MIT).Added
- A/B testing (#20): variants B–D (subject, preview text, content), a 1–50 % test group, automatic winner by opens or clicks after a wait, or a manual pick; the rest get the winner. Per-variant report section and variant test sends.
- Project API keys (#29): created by owners in Project settings, shown once, stored hashed, revocable, per-key rate limit.
Authorization: Bearer postak_…on the project's contacts, groups, segments, campaigns and read-only senders/templates routes. - API sending providers (#49): Amazon SES (API v2), SendGrid, Mailgun, and Postmark senders over HTTPS, no provider SDKs.
- MJML (#50): MJML campaigns and templates, compiled in a sandboxed worker with size and time limits.
Changed
- Campaign and template routes accept JSON bodies up to 4 MB with a Bearer header; nginx limits writes to them to 5 per second per address.
- Temporary MJML compiler failures answer 503 and are retried during delivery.
- The visual email builder (#19) moved to Phase 14 (v0.16.0).
Tests
Backend unit 366/366, e2e 67/68 (known-flaky sign-in rate-limit test), frontend 159/159. Smoke 55/55.
Not verified: real delivery through SES API, SendGrid, Mailgun or Postmark (no credentials), and MJML rendering in real mail clients.
See CHANGELOG.md for details.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
released this
2026-10-19 12:02:39 -04:00 | 15 commits to main since this releaseUpgrading: back up the database first. Two migrations run on start:
contact_groups(additive: groups, memberships, and form group links) and
campaign_audience(campaign include/exclude targets and the audience
recorded at send time). The second one moves each campaign'ssegmentId
into the new audience (unsent campaigns) or the recorded audience (sent
campaigns) and then drops thesegmentIdcolumn.Added
- Groups (#55): hand-managed lists of contacts per project, with a name
(unique ignoring case) and description, member counts, and a Groups page
and group page with paged, searchable members. Add or remove contacts by
id (up to 1,000) or everyone matching a contacts filter (up to 100,000),
rate-limited byPOSTAK_RATE_LIMIT_BULK. At most 500 groups per project. - Contact profile (#56): the contact page shows the contact's groups as
chips you can remove, adds them to an existing or new group, and lists the
segments they match right now. - Contacts list (#57): a group filter (also on export), a Groups column,
row and page selection, "Select all N contacts that match", and bulk
"Add to group" / "Remove from group". - Import, export, and forms (#58): CSV import reads a
groupscolumn
(names separated by|), can create missing groups, and can add everyone
in the file to chosen groups. Exports gain agroupscolumn. Sign-up forms
can add new subscribers to groups (on confirmation for double opt-in). - Campaign audiences (#59): send to any of several groups and segments,
leave out others, and see a live count. Everyone is emailed once.
Confirmations spell out the audience, and the report shows what a
campaign was sent to. Groups and segments used by unsent campaigns can't
be deleted. - Segment group rule (#60): "Group is in / is not in" conditions. The
builder's rule containers are now called blocks. - Phase 12 review fixes (#61): test emails use sample data (Jane Doe) by
default, or a contact you pick, and say whose data was used. Duplicate
keeps the web view setting (with a new address) and the audience.
Changed
- Contact and segment CSV exports have a
groupscolumn after
subscribed_at. A custom data key namedgroupsis no longer exported as
its own column. - CSV columns named
groupsorgroupare now read as group names, not
custom fields. Rename the column to keep it as a custom field. - For sent campaigns,
segmentIdis historical: it comes from the audience
recorded at send time and may refer to a segment that has since been
deleted. segmentIdon campaigns is deprecated in favour ofgroupIds,
segmentIds,excludeGroupIds, andexcludeSegmentIds. It is still
accepted on create, update, andGET campaigns/audience.
PR #62.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Groups (#55): hand-managed lists of contacts per project, with a name
-
released this
2026-10-15 12:39:27 -04:00 | 21 commits to main since this releaseUpgrading: back up the database first. One additive migration (
templates_and_campaign_editor) adds style, footer, and default settings to templates, and tracking, public link, and campaign data to campaigns. Deleting a template now clears it from sent campaigns instead of failing. Existing campaigns keep tracking on and their public link off. nginx now forwards/c/(the public web view) to the backend.Added
- Styled templates with a live preview (#43): Styled (Postak's responsive layout, Markdown content), HTML, and Plain text types. Style options for the page, content, headings, links, buttons, quotes, and footer, stored as validated settings. A Markdown footer (Postak adds an unsubscribe link if it's missing). Clone a template and mark one as the project default. Preview runs in a sandboxed iframe with no scripts.
- Campaign editor (#45): new and edit pages with sender, recipients (live count), subject, Plain text / Markdown / HTML editor, and template, plus a side-by-side preview for a chosen contact. Send a test email to up to 5 addresses (marked
[Test], not tracked or counted). Duplicate any campaign, delete drafts, and confirm with the recipient count before sending or scheduling. - Preheader, personalisation, and campaign data (#46): the preview text becomes the hidden preheader (up to 150 characters).
{{ contact.data.<key> }},{{ campaign.data.<key> }}, and| default: "..."fallbacks. Unknown placeholders render as nothing and show a warning in the preview. - Tracking options and a public web view (#47): turn open/click tracking off per campaign (no pixel, no rewritten links). An optional public link serves a sent campaign at
/c/<id>with neutral contact data and no tracking, and{{ web_view_url }}links to it. - Detailed campaign report (#48): clicks and unique clickers per link, a paged recipients table filterable by status with CSV export, opens and clicks over time (hourly for 48 hours, then daily) in an accessible chart with a table fallback, and a 14-day trend on the project overview.
Fixed
- Template type is respected (#44): Markdown bodies are converted first and then placed in the template layout, so HTML templates are no longer run through the Markdown parser.
- Deleting a used template (#44) returns 409 when unsent campaigns use it, and clears it from sent campaigns, instead of a 500.
Security
- Contact and campaign data values are HTML-escaped in HTML bodies. The placeholder language is a small subset that can't run code.
- The web view sends
Content-Security-Policy: sandbox; default-src 'none'(images and inline styles only),nosniff,no-referrer, andno-store, and never shows recipient details. - Sent campaigns can't have their content edited.
Verified
- Backend: 101 unit and 55 end-to-end tests; migrations on an adopted legacy database and a fresh one. Frontend: 87 tests.
- Live smoke through nginx and Mailpit: 51 of 51 checks passed (styled test send, personalised delivery, tracking, web view and its headers, report endpoints and CSV, and every new page rendered in headless Edge with no script errors).
PR #54.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
released this
2026-10-15 09:32:11 -04:00 | 27 commits to main since this releaseMerged in PR #53. Closes #39–#42.
Upgrading: back up the database first. One migration gives each form a sender (existing forms get the project's oldest sender, as before). nginx now forwards
/f/to the backend. SetPUBLIC_URLto your public https address. Confirmation links in double opt-in emails now open a page with a Confirm subscription button.Added
- Hosted sign-up page and embed code (#39): each form gets its own page with a shareable link, an iframe snippet and a plain HTML snippet. You can show a thank-you message or redirect to your own page, and choose whether other websites may use the form.
- Form editor (#40) with a live preview:
- heading, intro, button label and fine print
- fields: email, names and your own custom fields, each required or optional, in any order
- colours, font and corner radius
- Confirmation and welcome emails (#41):
- Choose the sender, write the confirmation email, and pick where people go after confirming.
- An optional welcome email includes an unsubscribe link.
- A double opt-in form without a sender now says it can't accept sign-ups, instead of silently sending nothing.
- Spam protection (#42):
- A hidden honeypot field, and a minimum fill time on every hosted form.
- An optional captcha that the visitor's browser solves invisibly, with no third-party service.
- Rejected submissions look the same as real ones to bots.
Security
- Contact details are escaped in emails. A name submitted through a public form could add HTML or links to campaign emails. This is now fixed.
- Confirmation needs a button press, so link scanners can't confirm subscriptions on someone's behalf.
- Form pages use a strict Content Security Policy.
- Confirmation emails are limited to one per person per form every 2 minutes.
Verified
- Tests: backend 72 unit and 51 end-to-end; frontend 67.
- Live through nginx and Mailpit:
- full double opt-in with a custom field
- the welcome email and its unsubscribe link
- spam dropped
- captcha solved and replay refused
- A real browser solved the captcha on the hosted page.
Full details are in CHANGELOG.md.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
released this
2026-10-15 08:51:52 -04:00 | 32 commits to main since this releaseMerged in PR #52. Closes #32–#38.
Upgrading: back up the database first. The two new migrations only add to the schema.
- SMTP senders now verify TLS certificates. A server with a self-signed certificate needs "Allow a self-signed certificate" ticked on its sender.
- The backend port is now bound to
127.0.0.1. Reach the API through nginx on port 3000.
Added
- Contact pages (#32): details, custom data, external ID, campaigns received and activity, plus new, edit and delete.
- Search and paging (#33): search by email, name or external ID, filter by status, pages of 25.
- CSV import and export (#34):
- The file is read in the browser, and you see how each column will be used before importing.
- Choose whether to update contacts that already exist (matched by external ID, then email).
- Rejected rows are reported and can be downloaded.
- A template and instructions for Excel, LibreOffice and Google Sheets are included.
- Import never changes a contact's status. Exports defuse cells that look like formulas.
- Sender setup (#35): edit pages, reply-to, SMTP security (SSL/TLS, STARTTLS, none), username and password, self-signed option, and a test send to yourself. Passwords stay write-only.
- Background sending (#36):
- Sending returns right away and a worker delivers in the background.
- Per-sender limits (per second, minute and hour) are honoured.
- Temporary failures are retried with back-off.
- The campaign list shows progress, and the report shows failures with the server's reason.
- Segment editor (#37): OR groups of AND conditions, a JSON query, a live list of matching contacts, and a CSV download.
Security
- Rate limiting (#38): 10 requests per minute per client on sign-in, password reset, registration, form submissions and test emails; 600 per minute elsewhere. nginx sets the client address, so clients can't fake it.
- TLS certificates are verified by default. From and Reply-To headers can no longer be broken by unusual names.
Verified
- Tests: backend 62 unit and 46 end-to-end; frontend 63.
npm audit: 0 for both. - Live stack through nginx and Mailpit:
- background send (request returned in 26 ms; 4 emails delivered with correct headers)
- import, search and CSV exports
- test email
- login rate limiting, including with a spoofed
X-Forwarded-For - a sender saved before this release still works
Full details are in CHANGELOG.md.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
released this
2026-10-15 07:52:26 -04:00 | 41 commits to main since this releaseMerged in PR #51. Closes #25, #26, #27, #28, #30 and #31. No database migrations.
Added
- Project sidebar (#25): once a project is open, the sidebar lists its sections and marks the current one. A "← All projects" link goes back.
- Project overview (#26): a card per section with a live figure, plus a "Get ready to send" checklist until the project has a sender, contacts and a first campaign. The contacts list is now at
/projects/:id/contacts. - Project settings (#27): rename the project, or delete it after typing its name. Members see a read-only view.
- System info (#28) for the administrator: version, database and migrations, and which features are on, with hints for turning on the ones that are off. It never shows secrets.
Security
- Sender credentials are no longer returned by the API (#30). Previously, any project member could read SMTP passwords and SES secret keys.
- Single opt-in forms can't re-subscribe people (#31) who unsubscribed, bounced or complained.
Verified
- Tests: backend 53 unit and 39 end-to-end; frontend 39.
- Live stack through the web server: rename, summary, system info with no secrets, senders without credentials, and the new pages.
Full details are in CHANGELOG.md.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-
released this
2026-10-15 01:03:41 -04:00 | 48 commits to main since this releaseMerged in PR #24. Closes #16, #23, #18 and #17.
Upgrading: back up the database first. On first start, Postak adopts an existing v0.8.0 (or older) database into the new migration history. It refuses any schema change that would lose data.
Changed
- Versioned database migrations (#16).
prisma db push --accept-data-lossis gone. Schema changes now ship as SQL migrations, applied withprisma migrate deployon start.0_initis the v0.8.0 baseline.
Added
- Account controls for the administrator (#23), on the Users page:
- disable or enable an account, which ends its sessions immediately
- set a temporary password, which the user must change after signing in
- delete an account; projects where that user was the only owner go to the administrator
- Forgot password by email (#23). Links are single-use, expire after 1 hour, and are throttled, and the response never reveals whether an account exists. Needs
POSTAK_SMTP_URL,POSTAK_MAIL_FROMandPOSTAK_APP_URL. - SES bounces and complaints (#17) via SNS at
POST /api/webhooks/ses:- Every message's SNS signature is verified, and only topics listed in
POSTAK_SES_SNS_TOPIC_ARNSare accepted. With none listed, the endpoint is off. - Hard bounces mark the contact BOUNCED, and complaints mark it COMPLAINED. Neither is mailed again.
- Reports and daily snapshots now show real bounce and complaint numbers.
- Every message's SNS signature is verified, and only topics listed in
- New migrations
user_account_controlsandcontact_bounce_statuses. Both are additive.
Testing
- Frontend tests (#18): Vitest and Testing Library, run with
docker compose run --rm --build -T frontend-test. - The backend test run now checks both migration paths and fails if a schema change has no migration.
Security
- Frontend toolchain upgraded: Vite 8, React Router 7.18 (fixes an open-redirect advisory), Node 22.
- The backend also runs on Node 22, since Node 20 is end-of-life.
npm audit: 0 for the backend (production dependencies) and 0 for the frontend.
Verified
- Tests: backend 53 unit and 35 end-to-end; frontend 28.
- Live stack: the existing v0.8.0 database was adopted with its data intact. Forgot password, temporary password, disable, and delete with project hand-over were all tested through the web server, with email going to Mailpit.
- Not yet tested against real AWS SNS/SES. That path is covered by tests that use locally signed messages.
Full details are in CHANGELOG.md.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Versioned database migrations (#16).
-
released this
2026-10-14 23:56:29 -04:00 | 55 commits to main since this releaseRecommended upgrade: clears all known vulnerabilities in production dependencies and fixes missing unsubscribe links (#21).
Merged via PR #22.
Security
- NestJS 10 → 11.2 (Express 5),
@nestjs/config4,@nestjs/schedule6,@nestjs/jwt/@nestjs/passport11, nodemailer 6 → 10, Jest 30.npm audit --omit=dev: 12 vulnerabilities (4 high) → 0 (#14). - Password change ends all other sessions; deleted users' tokens are rejected.
Added
- Account page: change password.
- Users page (administrator): create accounts.
- Members page: owner/member roles, add by email, change role, remove, leave (#15). Projects always keep one owner.
Fixed
- Emails without a template had no unsubscribe link (#21). Every email now has a footer link plus
List-Unsubscribe/ one-clickList-Unsubscribe-Postheaders. - Unsubscribes were counted as clicks.
- Opening the unsubscribe link (GET) unsubscribed immediately, so link scanners could unsubscribe people. GET now shows a confirmation; POST unsubscribes.
- Any member could delete a project; now owners only.
- Emails stored lowercase; sign-in case-insensitive.
Behavior changes
- Integrations must
POST /track/unsubscribe/:idto unsubscribe. - Documentation other than README, CHANGELOG, CONTRIBUTING, and LICENSE moved out of the code repository. The user guide is on the wiki.
Upgrading
- Rebuild:
docker compose up -d --build. The schema change (User.tokenVersion) applies automatically; existing sessions stay valid.
Tests: 45 unit + 29 end-to-end passing.
Milestone: Phase 8 · Not included: password reset and account disable (#23).
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- NestJS 10 → 11.2 (Express 5),
-
released this
2026-10-14 22:55:35 -04:00 | 61 commits to main since this releaseRecommended upgrade: fixes a security issue present in all earlier releases (#8).
Added
- Segment rule engine: whitelisted JSON rules (email, names, dates, custom
data.<key>; all/any groups up to 3 levels) compiled to Prisma. - Segments API (CRUD + preview) and builder UI with live preview.
- Campaign targeting by segment; schedule / unschedule endpoints and UI.
ProjectAccessGuardon every/projects/:projectId/...route.- End-to-end suite against PostgreSQL with a fake mailer (22 tests); 43 unit tests.
docker compose run --rm --build -T backend-test(resets onlypostak_test).POSTAK_SCHEDULER_ENABLED,POSTAK_DISABLE_REGISTRATION.- User guide
docs/user/segments.md.
Fixed
- #8 Project data was readable by any signed-in user.
- #9 A campaign with a missing segment was sent to every contact.
- #10 Campaigns could reference another project's sender, template, or segment.
- #11 The same campaign could be sent twice.
- #12 Sent campaigns were editable; PATCH bodies were not validated.
- #13 Snapshot endpoint ignored the project and accepted invalid dates.
Changed
npm ciwith committed lockfiles;.dockerignorefiles.
Known issues
npm audit --omit=dev: 12 vulnerabilities (4 high) in runtime dependencies. Tracked in #14 (Phase 8).
Milestone: Phase 7 · Summary: #7
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Segment rule engine: whitelisted JSON rules (email, names, dates, custom
-
released this
2026-10-14 22:16:18 -04:00 | 63 commits to main since this releaseAdded
- Templates UI: create, edit, delete.
- Forms UI: single or double opt-in, shows each form's public submit endpoint.
- Template picker in the campaign composer.
- Jest + ts-jest unit tests (renderer, rate math): 14 passing.
Changed
- Rate calculation extracted to a tested
rate()helper. - Production build excludes
*.spec.ts.
Security note: project-scoped routes do not check project membership. Fixed in v0.7.0 (#8).
Milestone: Phase 6 · Summary: #6
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads