All notable changes to this project will be documented in this file.
- Fix: The contacts multi-select no longer gets out of sync when deleting via the row's ⋯ menu — clicking a dropdown item invisibly toggled the row's selection (the click bubbles through the React portal to the row's select-on-click handler), and a deleted contact was never removed from the selection, leaving a phantom "N contacts selected" bar whose stale emails a later bulk Add to list would even re-create via upsert (#374).
- Feature: Broadcast reports now show a per-link click breakdown — total clicks and unique clickers per destination URL (UTM parameters kept, since they are commonly used to tell links apart). Clicks are recorded per URL in a new
message_history.clicked_linksJSONB column added by workspace migration v35; recording starts immediately after the upgrade, including for emails already sent (the destination URL is embedded in their tracking tokens), so the breakdown only covers post-upgrade clicks and legacy/visitlinks remain aggregate-only (#339, #311). - Fix: One-time authentication emails (the built-in Supabase notifications: magic link, signup confirmation, invite, recovery, email change) are no longer rewritten through the click-tracking redirect — their links previously depended on the API being up and could be consumed by mailbox link scanners before the user clicked. Transactional notifications gain a tri-state
tracking_mode(inherit the workspace setting, or disabled — which also suppresses UTM rewriting); an absent field on update keeps the stored value so partial edits cannot wipe an opt-out, and migration v35 setsdisabledon existing Supabase notifications. - Fix: A/B-test broadcasts no longer attribute most recipients' link clicks and
{{ utm_content }}values to the wrong variant. When no explicitutm_contentwas set, the first processed recipient's template ID was written back onto the shared broadcast object and frozen for every later recipient — and in batch sends the template data even lagged one recipient behind the rewritten links.utm_contentis now computed per recipient from the variant actually chosen for them, andutm_termis applied consistently across both send paths. - Fix: The automation Add to List node no longer fails silently. Its "Subscribed" option stored an invalid
subscribedstatus that the backend rejected (onlyactive/pendingare valid), so contact journeys stalled at that node with no visible error. The editor now storesactive, node configurations are validated when the automation is saved so an invalid status is caught immediately, and a data migration repairs existing automations, contact lists, and timeline entries that still carrysubscribed(#376). - Improvement: Concurrent template edits no longer silently overwrite each other. Template saves were last-writer-wins, so a save based on a stale revision clobbered newer changes. The editor now sends the revision it was based on and the server rejects a stale-base save with
409 Conflict, prompting to reload the latest or overwrite. Covers the email template, blog post, and transactional-notification editors (#378). - Security: Bumped the console's
echartsdependency to 6.1.0 to resolve a cross-site scripting (XSS) advisory (GHSA-fgmj-fm8m-jvvx) flagged by Dependabot. - Fix: Resolved intermittent
pq: password authentication failed/ "failed to get workspace connection" errors under load. The workspace connection manager health-check-pinged the cached pool on every query and evicted, closed, and rebuilt it (re-hitting thepostgresadmin database) on any slow ping or transient blip; it now reuses cached pools and creates them without a global lock (#380). - Fix: Adding a contact (or adding a contact to a list from the details drawer) now refreshes the contacts list immediately instead of requiring a hard page reload. The "Add" contact drawer never invalidated the React Query cache on success, and the "add to list" action only refreshed the contact details — both now invalidate the contacts list (and total count) so the new contact appears right away (#364).
- Improvement: Faster email rendering — Liquid engines are now reused across blocks via a pool instead of being rebuilt for every block, and constant regexes are compiled once, cutting per-block Liquid processing time by ~3× (~8× under concurrent sends) during broadcasts (#381).
- Improvement: AI Email Designer reliability with reasoning models (e.g. DeepSeek) — thinking now streams into a collapsible panel, the preview refreshes immediately after AI edits, a generated email that doesn't compile surfaces the MJML error instead of silently looking successful, hitting the token limit shows a non-destructive warning instead of wiping the answer, and OpenAI-compatible integrations gain a reasoning-effort selector (#363).
- Feature: Search templates by name on the templates list — a debounced name search beside the category filter, persisted in the URL, mirroring the broadcasts search (the already-loaded list is filtered client-side).
- Fix: Workspace SMTP integrations now connect to servers that advertise only
AUTH LOGIN(such as Azure Communication Services) — the raw SMTP sender hardcodedAUTH PLAINand was rejected with a 504 before credentials were ever checked. It now reads the AUTH mechanisms advertised in EHLO and uses LOGIN when PLAIN isn't offered, preferring PLAIN when both are available (#368). - Fix: Unsubscribing from the notification center works again. The widget's "Unsubscribe" action and per-list toggle (and the console) now post to a dedicated
/unsubscribeendpoint, while/unsubscribe-oneclickis reserved for the RFC 8058 mail-client one-click carried in theList-Unsubscribeheader (it still accepts the legacy JSON body as a backward-compatible shim). When v34.0 made/unsubscribe-oneclickstrictly RFC 8058 for the Gmail/Yahoo one-click fix, the notification center's JSON request was rejected with400 "Invalid request"and contacts stayed subscribed (#371). - Fix: A freshly installed root account no longer crashes the console on first login. Before any workspace existed,
user.mereturned"workspaces": nullinstead of[]for theROOT_EMAILuser — the root path returns the workspace list straight from the database, which is a nil slice when empty — and the console crashed withCannot read properties of null (reading 'length')instead of redirecting to workspace creation. The repository now returns an empty (non-nil) slice so the API always serializes[], and the console normalizes a nullworkspacesto an empty array as a safeguard (#367). - Fix: An
mj-button(ormj-social) whose inner padding was edited in the visual editor no longer vanishes in Gmail — the inner-padding object was compiled into the CSS as a Go map literal (padding:map[bottom:0px top:0px]) that strict clients reject; it now compiles to a valid CSS shorthand, and the editor no longer stores padding as an object (#369).
- Feature: Single Sign-On via OpenID Connect (OIDC) alongside magic-code login — off by default and enabled per deployment with
OIDC_*env vars, the setup wizard, or Settings → SSO (client secret encrypted at rest), so the sign-in page shows an SSO button only when it is turned on. Invited-users-only by default with opt-in just-in-time provisioning gated by a verified-email domain allowlist; identities are keyed on the durable issuer+subject pair (never email alone) and login still requires a workspace invite orROOT_EMAILfor access. Uses Authorization Code + PKCE and works with any compliant provider (Google Workspace, Keycloak, Okta, …); adds thefederated_identitiessystem table (migration v34). - Fix: One-click unsubscribe (RFC 8058) now works end-to-end. The
/unsubscribe-oneclickendpoint takes its parameters from theList-UnsubscribeURL query string — it previously tried to JSON-decode the POST body and rejected every mail-client request with400 "Invalid request body". The emitted URL now also carries theemail_hmacthe endpoint verifies. And the endpoint no longer applies User-Agent bot detection, which silently dropped the automated POSTs that Gmail/Yahoo/Apple (and tools likecurl) actually send (returning200while leaving the contact subscribed); it instead requires the RFC 8058List-Unsubscribe=One-Clickbody token to deflect bare prefetch/scanner POSTs (#362). - Feature: Google Gemini is now a selectable LLM provider for the AI agent (blog & email generation), alongside Anthropic and OpenAI — configure it under Settings → Integrations with a Gemini API key and model (default Gemini 3.1 Pro); when multiple LLM integrations are configured, a provider dropdown in the AI chat selects which to use.
- Feature: Search broadcasts by name and filter by status on the broadcasts list — a grouped status filter (All/Draft/Scheduled/Sending/Sent/Failed) plus a debounced name search beside it, both persisted in the URL; the
broadcasts.listAPI now accepts multiple statuses and a name search (#335).
- Migration v33.0 (workspace): adds
message_history.smtp_message_id(with a partial index) for reply matching,automations.exit_on_reply, a partial unique index oninbound_webhook_eventsto dedup replayed replies, and redefines thetrack_inbound_webhook_event_changes()trigger so inbound events of typereply/auto_replysurface on the contact timeline asemail.replied/email.auto_reply. Column adds are nullable/constant-default (instant, no table rewrite).
- Feature: Stop-on-reply for automations (#346). When a contact replies to a sequence email, the journey stops. Replies are ingested via a new public endpoint
POST /webhooks/email/inbound?workspace_id={id}&integration_id={id}(Mailgun first; provider-agnostic parser registry for the rest). For Mailgun, the inbound Route that forwards replies to this endpoint is now created automatically by the same Register Webhooks action used for delivery/bounce webhooks (only the DNS MX records remain a manual step); the route is non-preemptive (nostop(), lower priority) so it never silently overrides other inbound consumers on a shared Mailgun domain. The public endpoint is rate-limited per source IP and per workspace, and permanent client errors return 4xx (not 5xx) so providers don't retry-loop. Inbound mail is classified so bounces and out-of-office auto-replies never count as a reply. A genuine reply is matched to the send strictly byIn-Reply-To/References→ theMessage-IDstored at send time (persisted before the email is dispatched so even an instant reply matches), which both identifies the contact and scopes the exit to the exact automation that sent the replied-to email; replies that don't match a stored send are ignored, and a replayed/duplicate reply is deduplicated so it can't re-exit a re-enrolled journey. The matched reply is recorded on the contact timeline asemail.repliedand — when the automation has Exit on reply enabled — exits the contact's journey (bounded to journeys entered before the reply). The stop is enforced by a layered guarantee (event-driven interrupt + an active-guarded optimistic lock on the executor's happy and error paths + a just-in-time guard in the email queue worker) so it holds whether the contact is mid-delay or the next email is already queued, and a concurrent exit is never resurrected by a retry. The feature is free for workspaces that don't enable it (no extra per-send queries or index maintenance). - Feature: Stop-on-reply also supports Amazon SES — inbound replies arrive as RSA-signature-verified, topic-bound Amazon SNS notifications; Register Webhooks auto-provisions the SNS topic + a coexistence-safe SES receipt rule (scoped to verified identities, region-validated), and the Message-ID SES returns at send is captured and matched host-independently (#346).
- Feature:
ROOT_EMAILnow accepts multiple comma/semicolon-separated emails, so a shared self-hosted instance can have more than one root administrator without displacing the first. Root-gated actions (workspace creation, system settings, root HMAC sign-in) now check list membership instead of a single equality, and a user row is created on startup for every listed root so each can sign in immediately. Matching is case-sensitive and a single email behaves exactly as before. The console System Settings drawer edits the list as tags; the setup wizard still establishes the primary (first) root. Example:ROOT_EMAIL=alice@example.com,bob@example.com(#361).
- Fix: Translated email templates now send with their own inbox preview text (preheader) instead of the default language's. The inbox preview is rendered from the
mj-previewblock embedded in the email tree, but the metadata-sync that stamps it from thesubject_previewfield only ran for the default template — never for translations — so a translation kept the preview value it was cloned with, even after its preview was edited and saved. The sync now stamps every language variant, and all send paths (broadcast, automation, transactional) additionally inject each resolved variant'ssubject_previewat compile time, which also corrects already-saved translations without a re-save (#359).
- Security: Broadcast data-feed endpoints (
broadcasts.refreshGlobalFeed,broadcasts.testRecipientFeed) are no longer a server-side request forgery (SSRF) vector and now requirebroadcasts:write. The data-feed fetcher used a plain HTTP client with no address validation, so any authenticated workspace member — including a read-only member — could make the server fetch an arbitrary URL (internal services, the private network, or the cloud instance metadata endpoint) and read back the JSON response. The fetcher now uses the SSRF-safe client already used for favicon detection (dial-time rejection of private/loopback/link-local/reserved ranges, redirect re-validation, DNS-rebinding protection), and both service methods enforce the same write permission as broadcast creation. Trusted self-hosted deployments that intentionally fetch feeds from their internal network can opt out withBROADCAST_DATA_FEED_ALLOW_PRIVATE_HOSTS=true. - Security: All broadcast operations now enforce workspace permissions. Previously only create/get/refresh/test were permission-checked, so any workspace member — including a read-only member — could update, delete, schedule, pause, resume, cancel, send, and select A/B winners for broadcasts. Mutating operations now require
broadcasts:writeand listing/test-results requirebroadcasts:read; unauthorized requests receive403 Forbidden. - Fix: The task scheduler now executes due tasks in-process when the internal scheduler is enabled (
TASK_SCHEDULER_ENABLED), instead of dispatching them over HTTP to its own/api/tasks.executeendpoint. In single-instance deployments where the app cannot reach its own public URL (e.g. a pod that is itself the load balancer's backend), the self-call failed withconnection refusedand leftsend_broadcastand other tasks stuckpending; HTTP fan-out is still used when the scheduler is disabled (external cron). - Fix: Selecting a different email node in the automation editor now refreshes the config panel — the shared template selector (
TemplateSelectorInput) cached the first template it resolved and ignored later changes to its controlledvalue, so switching between email nodes kept showing (and appearing to edit) the first node's template (#353). - Fix: Test emails sent from the template editor now honor the template's Reply-To — the
transactional.testTemplatepath built the message from the modal's options only and never fell back to the template'sreply_to, so test emails arrived without aReply-Toheader (real automation/broadcast/transactional sends were already unaffected); an explicit Reply-To from the modal's Advanced options still takes precedence (#355). - Fix: Workspace members with the
workspacewrite permission ("full access") can now manage contact custom field labels. Previously both the Settings → Custom Fields controls and the underlying save were gated to workspace owners only, so full-access members had no way to add or edit field labels (#354). Custom field labels are now managed via a dedicated, permission-checked endpointPOST /api/workspaces.setCustomFieldLabels(granularworkspace:writeinstead of owner role), mirroring the template-blocks pattern. As a side effect,workspaces.updateno longer writes custom field labels — so an owner saving general settings can no longer clobber labels set by a member. - Fix: Workspace members with the
blogwrite permission can now manage blog settings — enabling the blog and editing its title, SEO, pagination, and feed configuration. Previously both the Settings → Blog editor and the underlying save were gated to workspace owners only, so a delegated "blog manager" grantedblog:writecould publish posts and themes but could not enable the blog or change its settings. Blog settings are now managed via a dedicated, permission-checked endpointPOST /api/workspaces.setBlogSettings(granularblog:writeinstead of owner role), mirroring the custom-field-labels pattern. As a side effect,workspaces.updateno longer writes blog settings — so an owner saving general settings can no longer clobber blog config set by a member. - Fix: Broadcasts to a double opt-in list no longer reach contacts who never confirmed — recipients whose
contact_liststatus ispendingare now excluded from both the recipient count and the send (#344). - Fix: Typing into a button's text editor in the email builder no longer puts each character on its own line — StarterKit's
TrailingNodewas enabled in the button's paragraph-less inline schema, where it falls back tohardBreakand appended a<br>after every keystroke; it is now disabled for the inline editor (#352). - Improvement:
{{ workspace.base_url }}/{{ workspace.website_url }}now render in the template preview — the/api/templates.compileendpoint injects the workspace object server-side (filling only missing keys, so historical message snapshots are preserved), so any API consumer gets it, not just the console, and the Preview tab no longer renderswebsite_urlas empty (#342). - Refactor: Extracted shared
WorkspaceSettings.ResolveEndpointandBuildWorkspaceTemplateVarshelpers, replacing ~8 duplicated copies of the tracking-endpoint resolution andworkspacetemplate-object construction across the send and preview paths.
- Feature: Exposed
{{ workspace.website_url }}in email templates — the workspace's public Website URL (trailing slash trimmed), distinct from{{ workspace.base_url }}(the tracking endpoint) — so templates can compose application links like{{ workspace.website_url }}/users/verify/xxxinstead of pointing at the tracking domain (#342).
- Feature: Exposed
{{ workspace.base_url }}in email templates — the resolved Custom Endpoint URL (or the default API endpoint), trailing slash trimmed — so templates can compose links from relative paths like{{ workspace.base_url }}/users/verify/xxx(#342). - Security: Bumped
liquidjsto 10.27.0 in console to clear 6 Dependabot alerts (critical RCE, ReDoS instrip_html,datefilter padding DoS,{% render %}ownPropertyOnlybypass, empty{% for %}renderLimit bypass, andstrip_htmlnewline XSS);npm audit fixalso cleared transitivebrace-expansionandwsadvisories. - Fix: Mailgun webhook registration no longer fails with
400on domains shared with other services — Notifuse now merges its callback URL into each event's existing URL set viaPUT(up to Mailgun's limit of 3 per event) instead of alwaysPOSTing, and unregistering removes only its own URL while preserving other consumers' (#340).
- Migration v32.0 adds a
languagecolumn (VARCHAR(10) NOT NULL DEFAULT 'en') to the systemuserstable. Existing users default to English.
- Feature: System emails and the console UI are now localized per user. Each user has a
languagepreference — one ofen,fr,es,de,ca,pt-BR,ja,it— that drives both their console UI locale and the language of the system emails (authentication code, workspace invitation, broadcast circuit-breaker alert) sent to them. The language is changed from the console language switcher and persisted via the newPOST /api/user.updateLanguageendpoint. Magic-code emails use the recipient's language, circuit-breaker alerts use each owner's language, and workspace invitations use the inviter's language.
- Migration v31.0 updates the
queue_contact_for_segment_recomputationtrigger function on every workspace database to short-circuit when the insertedcontact_timelinerow is itself a segment membership event (kind IN ('segment.joined', 'segment.left')).
- Fix:
queue_contact_for_segment_recomputationtrigger no longer re-enqueues contacts when the insertedcontact_timelineevent is itself a segment membership change (segment.joined/segment.left). Removes a self-loop where every membership write re-queued the same contact. - Fix: Recurring tasks dispatched via HTTP now write
timeout_afterin UTC. The column isTIMESTAMP WITHOUT TIME ZONEand the scheduler compares it againsttime.Now().UTC(); on non-UTC hosts the local-time value caused the task to appear "still running" for the host's UTC offset. Same fix applied to the broadcast-pausenext_run_after. - Fix:
GetWorkspaceConnection's pool health check now uses an isolated context forpool.PingContextinstead of the caller's. A caller-context cancellation no longer triggers pool eviction.
- Fix: UTM parameters (
utm_source,utm_medium,utm_campaign,utm_content,utm_term) were dropped from tracked links when click tracking was enabled — the encrypted/r/redirect token embedded the raw destination URL instead of the UTM-augmented one. The UTM parameters are now preserved in the redirect target.
- Fix: SES
4.4.7 Message expired(retry-exhaustion) now suppresses on the first event, and any recipient that accumulates 5 consecutive soft bounces with no successful delivery in between is also suppressed;MessageTooLarge/ContentRejected/AttachmentRejectednever count (#323). - Fix: Email AI Assistant
setEmailTreetool now declaresitemson itschildrenarray schema, so OpenAI-compatible providers no longer reject the request witharray schema missing items(#324). Anthropic was already lenient about this; only OpenAI-compatible endpoints surfaced the error. - Improvement:
/api/templates.compilenow accepts and returnssubjectandsubject_preview, rendered through the same Liquid engine used at send time. Previously the API only returnedmjml/html, so the console preview drawer rendered the subject in-browser withliquidjs, which could diverge from the Go-sideliquidgooutput used by the send pipeline. Any API consumer can now retrieve the rendered subject directly (#329). - Deps: Bumped
liquidjsto 10.25.7,postcssto 8.5.14,fast-xml-parseroverride to ≥5.8.0 (+ newfast-xml-builder≥1.1.7 override), andgithub.qkg1.top/prometheus/prometheusto v0.311.3.
- Security: Bumped
go.opentelemetry.io/otelto v1.41.0 intelemetry/go.mod(CVE-2026-29181). - Deps: Bumped
gomjmlto v0.12.0.
- SMTP auth with
SMTP_USE_TLS=false: When TLS is explicitly disabled, the SMTP client now usesPLAIN-NOENC(go-mail'sSMTPAuthPlainNoEnc) explicitly instead ofSMTPAuthAutoDiscover. Previously, go-mail's auto-discover refusedPLAIN/LOGINover an unencrypted connection (onlySCRAM-SHA-*andCRAM-MD5were tried), andSMTPAuthPlainitself also refused unencrypted connections at the AUTH step.PLAIN-NOENCbypasses both gates while sending the standardAUTH PLAINcommand on the wire, so any server that advertisesAUTH PLAIN(e.g. local maddy/Mailpit relays) accepts it. Operators who have setSMTP_USE_TLS=falsehave already accepted plaintext credential transit, so forcingPLAINaligns with their stated intent. Action: none if your relay acceptsPLAIN. If your relay only acceptsSCRAM/CRAM-MD5, you must enable TLS (SMTP_USE_TLS=true) — auto-discover continues to apply when TLS is on.
- Webhooks: Signatures now conform to the Standard Webhooks specification and match the published verification code in the docs (#318)
- Stored secrets are now prefixed with
whsec_and the 32 random bytes after the prefix are base64-decoded before use as the HMAC key (previously the 44-char base64 string was used directly as raw bytes, making the published Python/JS/Go/PHP verification snippets always fail) - V30 migration rotates every existing webhook secret to the new format. Subscriptions, URLs, event filters, enabled state, and delivery history are preserved; only the secret value changes
- Consumer action required: copy the new
whsec_…secret from the console into your environment and update your verification code to the spec-compliant form shown in the docs. Deliveries that fire during the gap will retry automatically once the consumer's secret is updated
- Stored secrets are now prefixed with
- Timezone:
Europe/Kievis rewritten to the IANA-canonicalEurope/Kyivacrossworkspaces.settings,contacts.timezone,segments.timezone, andbroadcasts.schedule.timezone. StoredEurope/Kievcontinued to resolve at runtime via Go's tzdata alias, but the console dropdown (which no longer lists the obsolete name) showed an empty selection for affected rows. Thecontactstriggers are briefly disabled around the rename so it does not emitcontact.updatedwebhook events or fillcontact_timelinewith rename entries.
- Task dispatch no longer swallowed by auth proxies (#320, #317): The scheduler's internal
POST /api/tasks.executeclient now refuses to follow redirects (CheckRedirect = http.ErrUseLastResponse). Previously, an auth-walling reverse proxy (Cloudflare Access, Authelia, oauth2-proxy, Traefik Forward Auth, etc.) sitting in front of the API could respond with a 302 to its login page; Go's defaulthttp.Clientfollowed the redirect as a GET, the login page returned 200 OK HTML, and the dispatcher logged "dispatched successfully" while the task never ran. The 302 is now surfaced in the non-200 branch (with theLocationheader logged) so the misconfiguration is loud instead of silent. Operator note: if your ingress performs an HTTP→HTTPS redirect on the API path, set$API_ENDPOINTto the final HTTPS URL — the dispatch client will no longer silently upgrade it. - Task scheduler: Scheduler tick no longer waits on in-flight HTTP dispatches, so one slow recurring task can't delay dispatch of others on the same tick. Stale tasks left in
runningwith an expiredtimeout_afterare now reclaimed byMarkAsRunningTxon a subsequent tick instead of looping on 409 indefinitely (#317). - Task dispatch observability: The scheduler-side "Task execution request dispatched successfully" log now includes the HTTP
status_code, andtasks.executelogs an entry line on the handler side. Diffing the two streams makes any remaining silent-interception failure mode visible. - Feature: Added AWS region
eu-central-2(Europe, Zurich) to the S3 provider and integrations region selectors (#316).
- Feature: Pause, resume, and cancel broadcasts mid-delivery — even after the orchestrator has finished enqueueing — and cancel is now allowed from the Processing state (#303)
- Contacts: Added in-table bulk actions (multi-select delete, add to list, remove from list, unsubscribe) with progress modal and "Skipped" tagging for no-op cases (#299)
- Feature: Added
SMTP_BRIDGE_TLSsetting (off/starttls/implicit) to let operators run the SMTP bridge behind a TLS-terminating reverse proxy or in implicit-TLS (SMTPS) mode (#314) - Feature: Blog RSS 2.0 and JSON Feed 1.1 syndication — automatic
/feed.xmland/feed.jsonendpoints per workspace, per-category feeds, conditional GET with ETag, gzip, XSS-sanitized content, autodiscovery<link>tags, and admin-configurable feed settings - i18n: Notification center confirmation banner (subscribe/unsubscribe result) is now translated in all supported languages instead of always showing English (#315)
- Security: Bumped transitive
github.com/prometheus/prometheusfrom v0.35.0 to v0.311.2 to clear Dependabot alert for CVE-2026-40179 (stored XSS in Prometheus web UI; Notifuse only importsmodel/value, so it was not exploitable)
- Fix: Double opt-in confirmation link now correctly transitions contacts from Pending to Active instead of resending the confirmation email in a loop (#313)
- Feature: Added OpenAI as LLM provider alongside Anthropic — supports any OpenAI-compatible endpoint (OpenRouter, Ollama, vLLM, LiteLLM, Azure, etc.) via custom base URL, with full streaming and tool use support
- Security: Updated liquidjs to 10.25.5 in console and vitest to 3.2.4 in notification center to fix 5 Dependabot vulnerabilities
- Deps: Updated @vitejs/plugin-react to 5.2.0 in console and notification center
- Feature: Added System Settings drawer for root admin to view and edit system configuration from the dashboard
- Workspace: Enforce workspace creation limits via
MAX_WORKSPACESenv var (0 = unlimited), with "upgrade your plan" messaging in the console - Improvement: Email open tracking now works independently of click tracking — added Cache-Control headers to prevent proxy caching, encrypted tracking URLs (
/t/,/r/) to avoid pixel blocker detection, and padded tracking pixel (#307) - i18n: Added Polish language support to the notification center
- Security: Upgraded Vite to 7.3.2 in console and notification center to fix arbitrary file read via WebSocket (CVE-2026-39363)
- Fix: Removed invalid
visibilityattribute from MJML section output that caused template compilation errors (#305) - Fix: Automation now exits when contact is unsubscribed/bounced/complained for marketing emails, while still allowing transactional emails to be sent (#304)
- Fix: Social media buttons now link directly to pages by default instead of wrapping URLs in share prompts; added "Share link" toggle to social element settings (#306)
-
Rename: "SMTP Relay" renamed to "SMTP Bridge" throughout the application
- Environment variables:
SMTP_RELAY_*renamed toSMTP_BRIDGE_*(old names still accepted for backward compatibility) - Database settings keys migrated automatically via V29 migration
- JSON API:
smtp_relay_*fields renamed tosmtp_bridge_*in setup endpoints - Frontend routes:
/settings/smtp-relaychanged to/settings/smtp-bridge - UI labels: "SMTP Relay" changed to "SMTP Bridge"
- Environment variables:
-
Workspace: Enforce team member limits via
MAX_USERSenv var (0 = unlimited), with checks on invite, accept invitation, and direct add — API key users are excluded from the count -
Security: Fixed SSRF vulnerability in
/api/detect-faviconendpoint by adding a safe HTTP client with private IP blocking, DNS rebinding protection, scheme validation, and response size limits -
Security: Upgraded happy-dom to 20.8.9 in notification center and picomatch to 4.0.4 in console
-
Improvement: SMTP EHLO hostname now defaults to the from-email domain instead of the SMTP host, improving compatibility with strict providers (#301)
-
Security: Updated lodash/lodash-es to 4.18.x, brace-expansion to 5.0.5, and yaml to 2.8.3 to fix prototype pollution, code injection, ReDoS, and stack overflow vulnerabilities
- Security: Upgraded picomatch to 4.0.4 in notification center
- Contacts: Fixed dropdown menu becoming unresponsive after deleting contacts, and pagination state now persists in URL across page refreshes (#294)
- Templates: Test emails now load the full contact record, so Liquid variables like
{{ contact.first_name }}render correctly
- Security: Upgraded google.golang.org/grpc to v1.79.3
- Security: Upgraded fast-xml-parser to v5.5.8
- Postmark: Added configurable Message Stream support, allowing Postmark to be used for both transactional (
outbound) and broadcast/marketing emails (#289) - Broadcasts: Fixed MJML code mode templates failing with "template missing content" error when sending broadcasts
- Contacts: Fixed
/api/contacts.listrejecting partial email searches with "invalid email format" error. Theemailfilter now accepts partial strings for substring matching as intended (#292)
- Transactional Emails: Added
subject_previewoverride toemail_options, allowing dynamic email preheader text per API call with Liquid templating support - Templates: Added language selection to "Send Test Email" modal and "Preview Template" drawer, allowing users to preview and test translated email variants
- Demo: Demo workspace now includes French and Spanish translations for all 4 email templates, showcasing the multi-language feature
- Templates: Downloaded template files now use the template's name as filename instead of a generic name (#286)
- Email Builder: Added
mj-liquidblock type for embedding raw MJML+Liquid code in the visual editor, enabling dynamic structural content like for-loops generating columns or conditional sections - Security: Upgraded liquidjs to 10.25.0
- Templates: Added option to choose between visual email builder or MJML code editor when creating templates
- Templates: Added ability to translate email templates to languages configured in workspace settings
- Contacts: Fixed invalid "Blacklisted" status option in change status dropdown, replaced with valid "Bounced" and "Complained" statuses (#285)
- SMTP: Added configurable EHLO hostname for SMTP connections. Some SMTP servers reject
EHLO localhost; users can now set a custom hostname (e.g., their domain) via theSMTP_EHLO_HOSTNAMEenv var, setup wizard, or workspace integration settings. Defaults to the SMTP host value when empty. - Transactional Notifications: Fixed delivery stats (sent, delivered, failed, bounced) always showing 0 by linking messages to their originating notification via a new
transactional_notification_idcolumn - Email Builder: Fixed
<mj-attributes>global styles not applying in preview and sent emails (#282)
- Notification Center: Fixed browser language auto-save overwriting contact's manually chosen language. Now only auto-detects when contact has no language set.
- Email Builder: Fixed missing MJML component defaults (border-radius, borders, background, direction, textAlign) causing styles not to render in the WYSIWYG editor
- Transactional Emails: Added
subjectoverride toemail_options, allowing dynamic email subject lines per API call with Liquid templating support (#281)
- Segments: Added template filter to email activity conditions, allowing segments like "opened template X at least 3 times"
- SMTP: Fixed TLS override option not working for system emails (magic codes, invitations, alerts). The "Use TLS" toggle was ignored, causing certificate errors on local SMTP relays (#275)
- Automations: Fixed automation emails rendering
{{ notification_center_url }}and{{ unsubscribe_url }}as empty strings by using the shared template data builder (#279) - Segments: Fixed race condition where background task execution could pick up unrelated tasks, causing flaky segment recompute behavior
- Security: Upgraded rollup to 4.59.0 and minimatch to 10.2.4
- Contacts: Fixed panic (502) when calling
/api/contacts.listwithout thelimitparameter (#264) - Security: Upgraded fast-xml-parser to 5.3.6
- Email Builder: Fixed block toolbar not appearing on divider blocks and improved toolbar positioning
- Setup: Strip trailing slash from API endpoint to prevent double-slash URLs breaking sign-in (#266)
- Email Builder: Fixed Liquid template variables (e.g.
{{ contact.email }}) not rendering in preview due to entities inserted by Tiptap v3 (#267) - Update Anthropic Sonnet model from
claude-sonnet-4-5-20250929toclaude-sonnet-4-6
- Automations: Per-email integration override — choose which email provider sends each email node, for IP warming and load distribution (#257)
- Update Anthropic Opus model from
claude-opus-4-5-20251101toclaude-opus-4-6 - Segments: Allow
count_value=0in activity conditions to support "never did X" segments (#249) - Security: Upgraded markdown-it to 14.1.1
- Broadcast Data Feeds: Added external data feed integration for broadcasts, allowing dynamic content injection from external APIs
- Global Feed: Fetch data once before broadcast starts, available to all recipients via
{{ global_feed.* }}template variable - Per-Recipient Feed: Fetch personalized data for each recipient via
{{ recipient_feed.* }}template variable - Custom HTTP headers support for API authentication
- Automatic retry with circuit breaker protection
- SSRF protection with URL validation (blocks localhost and private IPs)
- Real-time feed testing from the broadcast editor
- Global Feed: Fetch data once before broadcast starts, available to all recipients via
- Added
data_feedJSONB column tobroadcaststable (workspace migration)
- SMTP M365 OAuth2: Fixed XOAUTH2 authentication to use sender email instead of fixed auth email, resolving SendAs permission errors and incorrect Sent folder placement (#250)
- File Manager: Sanitize uploaded filenames by replacing spaces with dashes and lowercasing extensions (#252)
- Broadcasts: Fixed custom endpoint URL not propagating to click tracking and open tracking URLs in sent emails (#254)
- Email Builder: Fixed text color/background changes not applying when re-selecting already-styled text in the rich text editor
- Contacts: Bulk import now processes contacts in batches of 500, fixing crashes on large imports and improving performance
- Contacts: Emails are now normalized to lowercase on import to prevent case-sensitivity issues (#231)
- Security: Upgraded fast-xml-parser to 5.3.4
- Email Builder: Fixed buttons with HTML content like
<strong>rendering as default "Button" text instead of custom content (#242, gomjml PR#33)
- Segments: Added real-time validation when creating segments to detect duplicate IDs before submission (#243)
- Email Builder: Fixed clicking image blocks with links navigating away instead of allowing block selection (#239)
- Email Builder: Fixed popover buttons overflowing in certain languages by using auto-width with minimum constraint (#240)
- Amazon SES: Fixed Notifuse message ID extraction from webhook
- Transactional Notifications: Added card-based UI with delivery stats (sent, delivered, failed, bounced) and period selector (7D/30D/60D)
- Email Builder: Fixed clicking links in mj-text blocks navigating away instead of allowing text editing
- Console i18n: Added internationalization support with translations for English, French, Spanish, German, Catalan, Brazilian Portuguese, Italian, and Japanese
- File Manager: Increased actions column width and simplified new folder modal
- Broadcasts: Fixed crash on Broadcasts page when
variationsis null instead of empty array (#233) - SMTP Email: Fixed URL corruption in emails where dots were stripped after quoted-printable line breaks by adding proper SMTP dot-stuffing
- Automations: Email node template selector now shows all template categories instead of only marketing templates
- File Manager: Added warning modal when selecting images larger than 200KB from email editor to prevent slow email loading times
- SES Email: Fixed incorrect quoted-printable encoding in raw emails causing Gmail to break rendering for broadcasts with List-Unsubscribe headers or attachments (#230)
- Email Builder: Fixed Raw HTML block content not appearing in preview or sent emails (#229)
- Contacts: Fixed CSV import not trimming non-breaking spaces (NBSP) from email addresses and string fields (#223)
- Email Builder: Fixed MJML export stripping Liquid template syntax like
{{contact.external_id}}from links (#225) - Email Builder: Preview now shows
[undefined: varName]debug message when Liquid variables are missing from template data (#226)
- File Manager: Added explicit "Select" button in actions column when opened from email editor, replacing row-click selection behavior
- File Manager: Fixed files with spaces in filename breaking image delivery by URL-encoding path segments (#209)
- Email Builder: Fixed MJML template import failing when content contains HTML tags like
<br>or entities like (#218) - Email Builder: Fixed link editing in text blocks requiring exact text selection to update URL
- MJML Engine: Switched from
mjml-gotogomjmlv0.10.0 for MJML-to-HTML compilation - File Manager: Fixed drop zone overlay staying active when dragging files out without dropping
- Contacts: Added CSV export with support for current page filters and all contact fields
- Broadcast Progress UX: Added progress bar with ETA, clearer status badges, and tooltips to show sending progress (#185)
- File Manager: Added single file download, bulk delete, and multi-file ZIP download for selected files (#201)
- File Manager: Folder navigation now syncs with browser URL, enabling back/forward navigation and deep linking to folders
- Added SMTP OAuth2 authentication method with support for Microsoft 365 and Gmail SMTP servers (#184)
- Added Twilio SendGrid email provider integration (#178)
- Fix resubcribe to private lists in notification center
- Fix "custom CSS" edit when imported MJML templates have missing default attributes
- Fix email editor switch settings (fullWidth, fluidOnMobile) not persisting after page refresh (#206)
- Supabase Integration: Fixed "Before User Created" webhook URL mismatch causing HTTP 405 errors (#198)
- Email Builder Image Width: Fixed invalid MJML generated when adding images via the editor. The default
width="100%"is not valid for mj-image (only px values allowed). Images now default to filling container width when no explicit width is set (#196)
- Email Builder Column Width: Fixed columns displaying with zero width in WYSIWYG editor, causing text to wrap character-by-character
- Email Reply-To Header: Fixed Reply-To address from email templates not being included in automation and broadcast queue emails (#193)
- Supabase Integration: Fixed incorrect JSON field mapping for new email address in email_change webhooks. Changed
email_newtonew_emailto match Supabase Auth API specification.
- Automation Email Templating: Fixed
{{ contact.first_name }}rendering as&{John false}instead ofJohnin automation emails. Contact data is now properly converted before passing to the Liquid template engine. - List Status Values: Fixed
add_to_listautomation nodes using invalidsubscribedstatus instead ofactive - Automation Stats: Fixed automation stats being reset to 0 when updating automation
- v26: Automatically fixes
subscribed→activein contact_lists, automation nodes, and timeline entries. Recomputes automation stats from actual data.
- Automation Terminal Delay Nodes: Fixed bug where contacts at terminal delay nodes (delay node with no next node) were incorrectly marked as "completed" instead of staying "active" while waiting for the delay to expire
- Contacts now correctly wait at terminal delay nodes until scheduled time, then complete
- Fixed invalid delay node configurations in automation e2e tests (invalid unit "seconds" and duration 0)
- Added
waitForStatsCompleted()helper for polling-based stats verification to fix test flakiness - Improved test stability for automation scheduler timing edge cases
- Automation Enrollment Failure: Fixed "cannot set path in scalar" error when enrolling contacts in automations where the
statsfield contained a JSONB scalar value instead of an object- Migration v24 fixes existing automations by setting
stats = '{}'wherejsonb_typeof(stats) != 'object'
- Migration v24 fixes existing automations by setting
- Automation Triggers Not Firing: Fixed critical bug where automations with "List / Subscribed" trigger never fired for workspaces created after v18 (#190)
- Root cause:
init.gohad outdated trigger functions producing generic event kinds (insert_contact_list) instead of semantic event kinds (list.subscribed) - New workspaces only got
InitializeWorkspaceDatabase()without running migrations, so they never received the v18 trigger updates - V23 migration reinstalls correct trigger functions on ALL existing workspaces
- Updated
init.goto use semantic event kinds for new workspaces - Fixed e2e test that was masking the bug by manually inserting timeline events
- Root cause:
- Migration v23.0 reinstalls three PostgreSQL trigger functions with semantic event naming:
track_contact_list_changes():insert_contact_list→list.subscribed,list.confirmed, etc.track_contact_segment_changes():join_segment→segment.joined,leave_segment→segment.lefttrack_contact_changes():insert_contact→contact.created,update_contact→contact.updated
- SMTP Multi-line Banner: Fixed SMTP connections failing with "EHLO rejected with code: 220" error when server sends multi-line 220 greeting banner (RFC 5321 compliant fix for #183)
- Segment Date Filters: Fixed date picker sending dates in wrong format (
YYYY-MM-DD HH:mm:ssinstead of ISO8601). Frontend now sends proper RFC3339 format for all segment date filters including contact fields, timeline timeframes, and custom events goal timeframes (fixes #182)
- Broadcast Emails: Fixed regression from v21.0 email queue system where system template variables (
{{ unsubscribe_url }},{{ notification_center_url }},{{ broadcast.name }}, etc.) were not rendering in broadcast emails (fixes #180) - Message History: Fixed regression where template data was empty in message history for queue-based sends
- Automation Flow Editor: Fixed stale closure bug causing nodes to disappear when adding children to ListStatusBranch handles (fixes #179)
- Programmatic Root Authentication: New
/api/user.rootSigninendpoint for CI/CD and automation- HMAC-SHA256 signature authentication using existing
SECRET_KEY - 60-second timestamp window to prevent replay attacks
- Rate limited (5 attempts per 5 minutes)
- HMAC-SHA256 signature authentication using existing
- Email AI Assistant: AI-powered design assistant for email templates
- Streaming chat with Anthropic Claude models
- Tool use for modifying email structure, blocks, and content
- Server-side web scraping and search for content inspiration
- Auto-expand tree to selected block for better navigation
- AI Assistant UX: Both Email and Blog AI assistants now show a helpful setup prompt when the Anthropic integration is not configured, guiding users to the integration settings
- Unified AI Assistant codebase: Refactored Email and Blog AI assistants to share common code, reducing duplication by ~60% and ensuring consistent behavior
- Consistent AI Assistant styling: Both assistants now use the same color scheme for a unified experience
-
Blog AI Assistant: AI-powered writing assistant for blog posts
- Streaming chat with Anthropic Claude models (Opus, Sonnet, Haiku)
- Tool use for updating blog content and metadata directly in editor
- Server-side web scraping and search via Firecrawl integration
- Session cost tracking with input/output token breakdown
-
LLM Integration: Anthropic API support with encrypted API key storage
-
Firecrawl Integration: Web scraping (
scrape_url) and search (search_web) tools
- Migration v21.0 introduces the email queue system:
email_queuetable for unified broadcast and automation email delivery- Added
enqueued_countcolumn tobroadcaststable - Migrated broadcast statuses:
sending→processing,sent→processed
-
Email Queue System: Centralized queue for all outbound marketing emails
- Unified delivery for broadcasts and automations
- Priority-based processing with retry logic
- Per-integration rate limiting
- Background worker with graceful shutdown
-
Automation Performance: Single-tick execution optimization
- Process multiple nodes per scheduler tick until delay or completion
- 10-node safety limit per tick prevents runaway loops
- State persisted after each node for crash recovery
-
Automation Timeline Events: Track contact journey lifecycle
automation.startevent on enrollmentautomation.endevent on completion/exit/failure
- Broadcast statuses renamed:
sending→processing,sent→processed
- Migration v20.0 introduces the automations system with 4 new workspace tables:
automations- Workflow definitions with trigger config, nodes, and statisticscontact_automations- Tracks each contact's journey through automationsautomation_node_executions- Audit log of node executions for debuggingautomation_trigger_log- Trigger event logging
-
Marketing Automations: Visual workflow builder for automated contact journeys
- Event-driven triggers from contact timeline (contact, list, segment, email, custom events)
- Trigger frequency control:
once(first occurrence) orevery_time - Conditional triggers using segment filter conditions
- Field-specific triggers for contact updates (e.g., trigger only when
custom_string_1changes)
-
Automation Node Types:
- Trigger: Entry point based on timeline events with configurable conditions
- Delay: Pause workflow for minutes, hours, or days
- Email: Send templated emails using workspace email provider with tracking
- Branch: Conditional branching with multiple paths based on segment conditions
- Filter: Pass/fail routing based on contact attributes
- Add to List: Subscribe contacts to additional lists
- Remove from List: Unsubscribe contacts from lists
- A/B Test: Deterministic variant selection using FNV-32a hashing for consistent splits
- Webhook: POST contact data to external URLs with authorization headers
-
Visual Flow Editor: Drag-and-drop canvas for designing automation workflows
- Node positioning with visual connections
- Type-specific configuration panels
- Real-time validation
-
Automation Lifecycle Management:
- Draft mode for building and testing
- Activate to go live (creates PostgreSQL triggers)
- Pause to stop new enrollments while preserving in-progress journeys
- Soft-delete with recovery capability
-
Contact Journey Tracking:
- Full audit trail of node executions with timestamps and duration
- Contact status tracking (active, completed, exited, failed)
- Exit reasons for debugging
- Node execution output logging
-
Execution Engine:
- Background scheduler polling every 10 seconds
- Batch processing (50 contacts per batch)
- Round-robin workload distribution across workspaces
- Retry logic with exponential backoff (max 5 retries)
- Graceful shutdown handling
-
Statistics Dashboard:
- Enrolled contacts count
- Completed journeys
- Exited contacts (with reasons)
- Failed executions
-
API Endpoints (
/api/automations.*):create,get,list,update,deleteactivate,pausefor lifecycle managementnodeExecutionsfor contact journey audit trail
- Fix: SMTP integration now works with strict SMTP servers (#172)
- Replaced go-mail SMTP client with raw SMTP command implementation
- MAIL FROM command no longer includes BODY=8BITMIME or SMTPUTF8 extensions that caused "501 5.5.4 Syntax error in parameters" errors
- Message composition (MIME, headers, attachments) still handled by go-mail
- Fix: Task completion now saves final state to prevent stale progress display in UI (#157)
- Fix: Prevent concurrent task execution race condition that could cause duplicate broadcast emails
- Fix: Unsubscribes via notification center link are now tracked in broadcast statistics (#165)
- Fix: Email builder now respects column width attributes in section blocks
- Fix: Contact bulk import now handles duplicate emails in a single batch (#167)
- Fix: Image alt text now supports Liquid template variables (#168)
- Fix: Broadcast recipient count mismatch -
CountContactsForBroadcastnow filters soft-deleted lists consistently withGetContactsForBroadcast - Fix: Contact list pagination now uses nanosecond precision timestamps to prevent skipping contacts created within the same second (#159)
- Fix: Broadcast delivery now uses deterministic ordering (
created_at ASC, email ASC) to prevent skipping contacts with identical timestamps during bulk imports (#157) - Enhancement: SES configuration set is now optional for transactional emails
- Fix:
mailto:links are no longer tracked (prevents broken email client links)
- Fix SES non-ASCII characters in email local part (e.g.,
Añejandramendo@gmail.com) now encoded with RFC 2047
- fix update contact form
- fix non-ASCII characters in SES
- fix team table overflow when email is too long (#149)
- add TLS switch to setup wizard SMTP settings
- Enhancement: Added
full_namefield to the Add Contact drawer
- Outgoing Webhooks: Subscribe to workspace events and receive HTTP notifications
- CRUD API for webhook subscriptions (
/api/webhookSubscriptions.*) - Event types:
contact.*,email.*,list.*,segment.*,custom_event.* - HMAC-SHA256 signature verification (Standard Webhooks spec)
- Automatic retries with exponential backoff (up to 10 attempts over 24h)
- Custom event filters for fine-grained subscription control
- Test webhook endpoint for integration debugging
- Delivery logs with 7-day retention
- CRUD API for webhook subscriptions (
- Contact
full_namefield: Native field for systems without separate first/last names
- Fixed timeline timestamps showing incorrect times (now uses
CURRENT_TIMESTAMPin triggers) - Fixed JSON field filters with number/time values failing validation (#140)
- Fixed contact scan error on migrated databases due to column ordering mismatch (replaced
SELECT *with explicit column list)
- Renamed
webhook_eventstable toinbound_webhook_eventsto distinguish from outgoing webhooks
- Fix: SEO settings not being persisted when creating a new blog post
- Enhancement: Featured image thumbnail now displays in the Title column with a popover for full-size preview in the blog posts list
- API Endpoints: Renamed custom events endpoints from singular to plural for consistency
POST /api/customEvent.upsert→POST /api/customEvents.upsertPOST /api/customEvent.import→POST /api/customEvents.importGET /api/customEvent.get→GET /api/customEvents.getGET /api/customEvent.list→GET /api/customEvents.list
- File Manager: Added support for modern image formats (WebP, AVIF, JPEG XL)
.webpfiles now display with proper image previews.aviffiles now display with proper image previews.jxl(JPEG XL) files now display with proper image previews
- Migration v18.0 introduces custom events tracking system
- Removed deprecated contact fields:
lifetime_value,orders_count,last_order_at - Renamed contact timeline event kinds to semantic dotted format:
- Contact:
insert_contact→contact.created,update_contact→contact.updated - Lists:
insert_contact_list→list.subscribed/list.pending, status changes →list.confirmed/list.resubscribed/list.unsubscribed/list.bounced/list.complained - Segments:
join_segment→segment.joined,leave_segment→segment.left
- Contact:
- Added
custom_eventstable for tracking user behavior and goals - Added computed fields in segmentation engine for custom events goal aggregations
-
Custom Events API: Track user behavior and conversion goals
POST /api/customEvents.upsert- Create or update a single eventPOST /api/customEvents.import- Batch import up to 50 eventsGET /api/customEvents.get- Retrieve event by workspace, event name, and external IDGET /api/customEvents.list- List events by email or event name- Goal tracking with types:
purchase,subscription,lead,signup,booking,trial,other - Soft-delete support via
deleted_atfield
-
Segmentation with Custom Events Goals: Build segments based on goal aggregations
{ "source": "custom_events_goals", "custom_events_goal": { "goal_type": "purchase", "aggregate_operator": "sum", "operator": "gte", "value": 500, "timeframe_operator": "in_the_last_days", "timeframe_values": ["30"] } }- Aggregate operators:
sum,count,avg,min,max - Timeframe operators:
anytime,in_the_last_days,in_date_range,before_date,after_date
- Aggregate operators:
- Removed
lifetime_value,orders_count,last_order_atfrom Contact model - Segments using deprecated contact fields will be deleted during migration
- Contact timeline event kinds renamed (existing data migrated automatically)
- Segment tree
TreeNodeLeaf.tablefield renamed tosource(existing segments migrated automatically)
- Enhancement: The File Manager now supports S3-compatible storage providers with path-style bucket endpoint.
- New feature: Blog posts can now be scheduled for publication at past or future dates, allowing you to plan posts in advance or import posts with their original publication dates.
- Fix: retrieve broadcasts list when
pause_reasonorwinning_templateis null to avoid SQL errors - Fix: blog cache is cleared when updating mailing lists to ensure subscription forms work correctly
- Enahncement: blog theme shows a helpful error message when no public lists are configured for newsletter subscription forms
- Fix: Invitation links now correctly point to
/console/accept-invitationinstead of/accept-invitationto match the new console path
- Migration v17.0 introduces blog feature support
-
Blog Feature: Full-featured blogging system with advanced templating capabilities
- Notion-like blog post editor for intuitive content creation
- Full control over templating using Liquid syntax for dynamic content
- The blog is served at the root path
/of the custom domain configured in the workspace settings when it's enabled
-
Segmentation Engine JSON Support: Enhanced contact segmentation with JSON attribute matching
- Support for matching contact
custom_json_xattributes in segmentation rules - Advanced filtering capabilities for complex contact data structures
- Improved targeting precision for email campaigns and contact management
- Support for matching contact
- Template Permission Updates: Custom email blocks now use template write permissions instead of workspace write permissions for better security granularity
- Auto-unsubscribe Enhancement: Notification center now automatically processes unsubscribe actions when the unsubscribe link loads, improving user experience
- The console UI is now serverd at
/consoleinstead of/to avoid conflicts with the new blog feature. When the blog is disabled the/path will redirect to/console. - Permission system updated: saving custom email blocks requires
templates:writepermission instead ofworkspace:write
- Fix: Replace hardcoded SMTP PLAIN auth with auto-discover to support Azure Communication Email and other providers requiring LOGIN authentication
- Fix: Setup wizard redirection loop when API_ENDPOINT differs from actual console host name
- Add
/healthzendpoint for container health checks that pings the database
- SMTP Relay for Transactional Emails: Built-in SMTP relay server for sending transactional emails via standard SMTP clients
- Send transactional emails using SMTP protocol instead of HTTP API calls
- Useful for integrating legacy systems and standard email libraries
- TLS encryption support on port 587 with STARTTLS
- Authentication using workspace API credentials
- Email body contains JSON payload matching Transactional API format
- Configuration via environment variables:
SMTP_RELAY_ENABLED,SMTP_RELAY_PORT,SMTP_RELAY_DOMAIN,SMTP_RELAY_TLS_CERT_BASE64,SMTP_RELAY_TLS_KEY_BASE64
- Added
integration_idcolumn totransactional_notificationsandtemplatestables for integration-managed resources
- Message Data Encryption: Template data variables are now encrypted at rest in the database
- Message history
message_data.datafield is encrypted using AES-256-GCM with workspace secret key - Only new messages are encrypted (no migration of existing data)
- Automatic encryption on message creation and decryption on retrieval
- Backward compatible: unencrypted messages from before this version can still be read
- Metadata remains unencrypted for query performance
- Protects sensitive data like tokens, API keys, and personal information in template variables
- Message history
- Supabase Integration: Connect Supabase Auth with Notifuse
- Auth Email Hook: Send branded authentication emails (signup, magic link, password recovery, email change, reauthentication)
- Before User Created Hook: Automatically sync new Supabase users to Notifuse contacts
- Auto-generated email templates for all Supabase auth events with proper Liquid variables
- HMAC-SHA256 webhook signature verification for security
- Optional automatic list subscription and disposable email rejection
- Field mapping: Supabase
user_metadatato configurable Notifuse custom JSON field
- Integration-managed templates and transactional notifications cannot be deleted (but can be edited)
This is a major security release that migrates the authentication system from PASETO to JWT (HS256) and implements comprehensive security improvements.
Migration Requirements:
- REQUIRED: Set
SECRET_KEYenvironment variable before upgrading- CRITICAL FOR EXISTING DEPLOYMENTS:
- If you already have
SECRET_KEYset: Keep it unchanged (do not generate a new one) - If migrating from PASETO: Use your existing PASETO key:
export SECRET_KEY="$PASETO_PRIVATE_KEY"
- If you already have
- For new installations only: Generate new key:
export SECRET_KEY=$(openssl rand -base64 32)
- CRITICAL FOR EXISTING DEPLOYMENTS:
- Server will automatically restart after migration to reload JWT configuration
🚨 CRITICAL WARNING:
- DO NOT change your existing SECRET_KEY - it encrypts all workspace integration secrets (email provider API keys, SMTP passwords, etc.)
- Changing SECRET_KEY will:
- ❌ Make all encrypted integration secrets unreadable (permanent data loss)
- ❌ Break all email sending (SparkPost, Mailjet, Mailgun, SMTP credentials lost)
- ❌ Invalidate all JWT tokens and user sessions
- Only generate a new SECRET_KEY for fresh installations with no existing data
What Gets Invalidated (During PASETO → JWT Migration):
- ✗ All user sessions (users must log in again - PASETO tokens → JWT tokens)
- ✗ All API keys (must be regenerated - PASETO format → JWT format)
- ✗ All pending workspace invitations (invitation tokens were PASETO-signed)
- ✗ All active magic codes (migrating from plain-text → HMAC-SHA256 hashes)
Why: PASETO tokens are incompatible with JWT verification. Clean migration ensures no security gaps.
Important Notes:
- If you're already using JWT (not migrating from PASETO), and you keep your existing
SECRET_KEY, your existing sessions remain valid. - The
SECRET_KEYis also used to encrypt workspace integration secrets (API keys, SMTP credentials). Never change it on existing deployments or you'll lose access to all encrypted credentials permanently.
- Migrated from PASETO to industry-standard JWT with HMAC-SHA256 signing
- Simplified setup: Uses symmetric key (
SECRET_KEY) instead of PASETO's asymmetric key pair- No need to generate and manage separate public/private keys
- Single
SECRET_KEYenvironment variable for all cryptographic operations - Easier deployment and configuration management
- Algorithm confusion attack prevention (strict HMAC validation)
- Comprehensive token validation (signature, expiration, claims)
- Compatible with standard JWT libraries and tools
- Magic codes now stored as HMAC-SHA256 hashes (no plain text in database)
- Database compromise cannot reveal authentication codes
- Constant-time comparison prevents timing attacks
- Migration clears all existing plain-text codes
- New
/api/user.logoutendpoint (POST, requires authentication) - Deletes ALL sessions for the authenticated user from database
- Tokens become immediately invalid after logout
- Protected endpoints now verify session exists in database
- Returns 401 Unauthorized if session has been deleted
- Frontend integration with graceful error handling
- Protection against brute force attacks and email bombing
- In-memory rate limiter with sliding window algorithm
- Sign-in endpoint: 5 attempts per 5 minutes per email address
- Verify code endpoint: 5 attempts per 5 minutes per email address
- Rate limiter automatically resets on successful authentication
- Thread-safe concurrent access with automatic cleanup
- Independent rate limits per user and per endpoint
- Prevents magic code brute force attacks (blocks 99%+ of attempts)
- Enhanced session verification in
GetCurrentUserendpoint - Added
DeleteAllSessionsByUserIDmethod to user repository - Added
Logoutmethod to user service interface - Frontend
AuthContextnow calls backend logout before clearing local storage - Migration automatically cleans up incompatible authentication artifacts
- New integration tests for logout functionality (
tests/integration/user_logout_test.go) - New integration tests for rate limiter (
tests/integration/rate_limiter_test.go) - Unit tests for rate limiter with race detection
- Fixed race condition in concurrent rate limiter test (atomic operations)
- All tests pass with
-raceflag enabled
- Updated security audit document (
SECURITY_AUDIT_JWT_SESSIONS.md) - Changed "No Server-Side Logout" from 🔴 CRITICAL to ✅ IMPLEMENTED
- Changed "Brute Force Risk" from MEDIUM to LOW
- Added detailed implementation notes and testing coverage
- Comprehensive migration guide in v15 migration file
- Users: Log in again with email/password (or magic code)
- API Key Holders: Regenerate API keys in Settings → API Keys
- Integrations: Update all API integrations with new keys
- Workspace Admins: Resend pending invitations via Settings → Members → Invitations
- Migration v15 is idempotent and safe to run multiple times
- Estimated migration time: < 1 second
- Server automatically restarts after migration
- Migration validates
SECRET_KEYenvironment variable before proceeding - Comprehensive migration summary displayed in console
- Bulk Contact Import: New
/api/contacts.importendpoint for efficiently importing large numbers of contacts- Creates or updates multiple contacts in a single batch operation using PostgreSQL bulk upsert
- Returns individual operation results (created/updated/error) for each contact
- Optional bulk subscription to lists via
subscribe_to_listsparameter - Significantly faster than individual upsert operations for large imports
- Batch size of 100 contacts processed at a time in the UI
- Supports partial success - some contacts can succeed while others fail validation
- Added
channel_optionsJSONB column tomessage_historytable to store email/SMS/push delivery options (CC, BCC, FromName, ReplyTo...)
- Internal Task Scheduler: Tasks now execute automatically every 30 seconds
- No external cron job required
- Configurable via
TASK_SCHEDULER_ENABLED,TASK_SCHEDULER_INTERVAL,TASK_SCHEDULER_MAX_TASKS - Starts automatically with the app, stops gracefully on shutdown
- Faster task processing (30s vs 60s minimum with external cron)
- Privacy Settings: New optional configuration for telemetry and update checks
TELEMETRYenvironment variable (optional) - Send anonymous usage statisticsCHECK_FOR_UPDATESenvironment variable (optional) - Check for new versions- Both can be configured via setup wizard if not set as environment variables
- For existing installations: migration v14 sets both to
trueby default (respects env vars if set) - Environment variables always take precedence over database settings
- Message history now stores email delivery options:
- CC (carbon copy recipients)
- BCC (blind carbon copy recipients)
- FromName (sender display name override)
- ReplyTo (reply-to address override)
- Message preview drawer displays email delivery options when present
- Only stores email options in this version (SMS/push to be added later)
- Modernized Docker Compose to use current standards: renamed
docker-compose.ymltocompose.yaml, removed deprecatedversionfield, updated commands to usedocker composeplugin syntax, and improved.envfile integration
- Removed cron setup instructions from setup wizard
- Removed cron status warning banner from workspace layout
- Simpler onboarding experience - no manual cron configuration needed
- Added preview mode to notification center
- Setup Wizard Improvements:
- Added newsletter subscription option
- PASETO keys configuration moved to collapsible "Advanced Settings" section
- Added "Privacy Settings" section for telemetry and update check configuration
- Improved restart handling: displays setup completion screen immediately while server restarts
- User can review generated keys before manually redirecting to signin
/api/cronHTTP endpoint (internal scheduler is now primary)/api/cron.statusHTTP endpoint (still functional but not advertised)
- Fix: SMTP now supports unauthenticated/anonymous connections (e.g., local mail relays on port 25)
- Fix: Docker images now built with CGO disabled to prevent SIGILL crashes on older CPUs
- Fix: Decode HTML entities in URL attributes to ensure links with query parameters work correctly in MJML-compiled emails
- Fix: Normalize browser timezone names to canonical IANA format to prevent timezone mismatch errors
- Fix: Broadcast pause also pauses the associated task
- Added
ShouldRestartServer()method to migration interface - Migrations can now trigger automatic server restart when config reload is needed
- Existing messages will have
channel_options = NULL(no backfill) - Migration v14 adds default telemetry and update check settings for existing installations (both default to
true) - Migration is idempotent and safe to run multiple times
- Estimated migration time: < 1 second per workspace
- Server will automatically restart after migration to reload all configuration settings
- New feature: transactional email API now supports
from_nameparameter to override the default sender name
- Upgrade github.com/wneessen/go-mail from v0.7.1 to v0.7.2
- Fix: SMTP transport now supports multiple CC and BCC recipients
- Fix: segment filters now support multiple values for contains/not_contains operators
- Multiple values are combined with OR logic as indicated in the UI
- Fix: custom field labels now display consistently in contacts table column headers and JSON viewer popups
- Contacts table columns now use custom field labels from workspace settings instead of generic defaults
- JSON custom fields now show custom labels in their popover titles
- Add new filters to message history: filter by message ID, external ID, and list ID
- List ID filter supports searching messages sent to a specific list
- New feature: customize display names for contact custom fields in workspace settings
- Fix SMTP form default
use_tlsnot being included in form submissions
- New feature: segmentation engine now supports relative dates (e.g., "in the last 30 days")
- Segments containing relative dates are automatically refreshed every day at 5am in the segment timezone
- Fix critical regression introduced in v11 that blocked broadcast sending
- Move rate limit configuration from broadcast audience settings to email integration settings
- Rate limit is now a required field on email integrations (default: 25 emails/minute)
- Simplifies broadcast configuration and centralizes rate limiting at the integration level
- Migration v12 automatically sets default rate limit on all existing email integrations
- New feature: setup wizard for initial configuration
- Many environment variables are now optional and can be configured through the setup wizard:
ROOT_EMAIL,API_ENDPOINT,PASETO_PRIVATE_KEY,PASETO_PUBLIC_KEY,SMTP_HOST,SMTP_PORT,SMTP_USERNAME,SMTP_PASSWORD,SMTP_FROM_EMAIL,SMTP_FROM_NAME - Database environment variables remain required:
DB_HOST,DB_PORT,DB_USER,DB_PASSWORD SECRET_KEYremains required (orPASETO_PRIVATE_KEYas backward compatibility fallback) to encrypt sensitive data in the database- Configuration can be provided through setup wizard on first install and stored securely in database
- PASETO keys can be generated automatically and shown at the end of the setup wizard
- Environment variables always override database settings when present
- New feature: automatic contact list status updates on bounce and complaint events
- Add
list_idsTEXT[] column tomessage_historytable to track which lists a message was sent to - Add database trigger to automatically update
contact_listsstatus to 'bounced' or 'complained' when hard bounces or complaints occur - Distinguish between hard bounces (permanent failures) and soft bounces (temporary failures) - only hard bounces affect contact status
- Status hierarchy: complained > bounced > other statuses
- Backfill historical broadcast messages with list associations
- Render broadcast name in message logs
- escape special characters in MJML import/export
- New feature: email attachments support for transactional emails
- Add
message_attachmentstable for deduplication and storage of attachment content - Add
attachmentsJSONB column tomessage_historytable - Support for attachments across all ESP integrations (SMTP, SES, SparkPost, Postmark, Mailgun, Mailjet)
- Maximum 20 attachments per email, 3MB per file, 10MB total
- New feature: real-time contact segmentation engine
- Add
db_created_atanddb_updated_atfields to contacts table for accurate database tracking - Add
kindfield to contact timeline for granular event types (e.g., open_email, click_email) - Make
created_atandupdated_atoptional with database defaults to support historical data imports - Ensure all timestamps stored in UTC timezone
- Fix panic when broadcast rate limit is set to less than 60 emails per minute
- Improve rate limiting calculation to properly handle low rate limits
- New feature: contact events timeline (messages, webhook events, profile mutations etc...). It's the backbone of the upcoming automations feature.
- Implement per-broadcast rate limiting functionality
- Add support for broadcast-specific rate limits that override system defaults
- Make rate limit field required in broadcast form with default value of 25 emails/minute
- Add comprehensive test coverage for per-broadcast rate limiting
- Upgrade github.com/wneessen/go-mail from v0.6.2 to v0.7.1
- Add cron status monitoring endpoint
/api/cron.status - Add SettingRepository for managing application settings
- Add automatic cron health checking in frontend console
- Add visual banner with setup instructions when cron is not running
- Update TaskService to track last cron execution timestamp
- Fix scheduled broadcast time handling to use string format instead of time.Time
- Remove broadcast service dependency from task service tests
- Update ParseScheduledDateTime tests to match implementation behavior
- Add new workspace dashboard
- Bulk update contacts functionality to console
- Add delete contact functionality to console
- Redact email addresses in message history and webhook events when deleting a contact
- Add test email functionality to broadcast variations
- Fix permissions for test emails to require read template and write contact permissions
- Fix set permissions on root user
- Force all permissions to owners
- Fix circuit breaker error message in broadcast pause reason
- Simplify broadcast circuit breaker notification email
- hide menu items in console when user doesn't have access to the resource
- disabled create/update buttons in console when user doesn't have write permissions
- Add permissions with roles per workspace
- Add pause_reason column to the broadcasts table to store the reason for broadcast pause
- Pause broadcasts when circuit breaker is triggered
- Add system notification service to email circuit breaker events
- Add migrations to the system and workspace databases
- Add permissions column to the user_workspaces table for future permission management
- Add UI previsions about broadcast rate limit per hour/day
- Fix VARCHAR(255) constraint for status_info in message_history table
- Fix z-index for file manager in template editor
- Improve broadcast UI with remaining test time, refresh button, and variations stats
- Improve transactional email API command modal with more examples and better documentation
- Only root user can create new workspaces
- Added server-side validation to restrict workspace creation to the user specified in
ROOT_EMAILenvironment variable - Create workspace UI elements are now hidden for non-root users in the console interface
- Hide deleted list in notification center when user has subscribed
- View a resend member invitations
- Access template test data in "Send test template" transactional email
- Mailgun integration now supports broadcast campaigns and newsletters, in addition to transactional emails
- Fixed issue: accept invitation
- New feature: custom endpoint URL in workspace settings to customize the tracking links and notification center URLs
- MJML raw-block is now editable
- Dates format is only English
- Anonymous users can't signin anymore, they need to be invited to a workspace
- The SECRET_KEY env var is now deprecated, and uses the PASETO_PRIVATE_KEY value to simplify deployments
- Install Notifuse quickly for non-production workload using a Docker compose that embeds Postgres
- Launch of the new Notifuse V3