Skip to content

Releases: Notifuse/notifuse

v35.0

Choose a tag to compare

@pierre-b pierre-b released this 08 Jul 11:11
  • 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_links JSONB 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 /visit links remain aggregate-only (#339, #311).
  • 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).
Capture d’écran 2026-07-08 à 11 40 58
  • 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).
  • 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).
  • 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 sets disabled on 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 explicit utm_content was 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_content is now computed per recipient from the variant actually chosen for them, and utm_term is applied consistently across both send paths.
  • Fix: The automation Add to List node no longer fails silently. Its "Subscribed" option stored an invalid subscribed status that the backend rejected (only active/pending are valid), so contact journeys stalled at that node with no visible error. The editor now stores active, 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 carry subscribed (#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 echarts dependency 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 the postgres admin 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).

v34.1

Choose a tag to compare

@pierre-b pierre-b released this 26 Jun 09:03
  • Fix: Workspace SMTP integrations now connect to servers that advertise only AUTH LOGIN (such as Azure Communication Services) — the raw SMTP sender hardcoded AUTH PLAIN and 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 /unsubscribe endpoint, while /unsubscribe-oneclick is reserved for the RFC 8058 mail-client one-click carried in the List-Unsubscribe header (it still accepts the legacy JSON body as a backward-compatible shim). When v34.0 made /unsubscribe-oneclick strictly RFC 8058 for the Gmail/Yahoo one-click fix, the notification center's JSON request was rejected with 400 "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.me returned "workspaces": null instead of [] for the ROOT_EMAIL user — the root path returns the workspace list straight from the database, which is a nil slice when empty — and the console crashed with Cannot 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 null workspaces to an empty array as a safeguard (#367).
  • Fix: An mj-button (or mj-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).

v34.0

Choose a tag to compare

@pierre-b pierre-b released this 22 Jun 10:09
  • 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 or ROOT_EMAIL for access. Uses Authorization Code + PKCE and works with any compliant provider (Google Workspace, Keycloak, Okta, …); adds the federated_identities system table (migration v34).
  • Fix: One-click unsubscribe (RFC 8058) now works end-to-end. The /unsubscribe-oneclick endpoint takes its parameters from the List-Unsubscribe URL query string — it previously tried to JSON-decode the POST body and rejected every mail-client request with 400 "Invalid request body". The emitted URL now also carries the email_hmac the endpoint verifies. And the endpoint no longer applies User-Agent bot detection, which silently dropped the automated POSTs that Gmail/Yahoo/Apple (and tools like curl) actually send (returning 200 while leaving the contact subscribed); it instead requires the RFC 8058 List-Unsubscribe=One-Click body 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.list API now accepts multiple statuses and a name search (#335).

v33.0

Choose a tag to compare

@pierre-b pierre-b released this 19 Jun 09:20

Features

  • 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 (no stop(), 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 by In-Reply-To/References → the Message-ID stored 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 as email.replied and — 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_EMAIL now 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).

Fixes

  • 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-preview block embedded in the email tree, but the metadata-sync that stamps it from the subject_preview field 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's subject_preview at compile time, which also corrects already-saved translations without a re-save (#359).

Database Schema Changes

  • 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 on inbound_webhook_events to dedup replayed replies, and redefines the track_inbound_webhook_event_changes() trigger so inbound events of type reply/auto_reply surface on the contact timeline as email.replied/email.auto_reply. Column adds are nullable/constant-default (instant, no table rewrite).

v32.3

Choose a tag to compare

@pierre-b pierre-b released this 15 Jun 13:43
  • Security: Broadcast data-feed endpoints (broadcasts.refreshGlobalFeed, broadcasts.testRecipientFeed) are no longer a server-side request forgery (SSRF) vector and now require broadcasts: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 with BROADCAST_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:write and listing/test-results require broadcasts:read; unauthorized requests receive 403 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.execute endpoint. 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 with connection refused and left send_broadcast and other tasks stuck pending; 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 controlled value, 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.testTemplate path built the message from the modal's options only and never fell back to the template's reply_to, so test emails arrived without a Reply-To header (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 workspace write 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 endpoint POST /api/workspaces.setCustomFieldLabels (granular workspace:write instead of owner role), mirroring the template-blocks pattern. As a side effect, workspaces.update no 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 blog write 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" granted blog:write could publish posts and themes but could not enable the blog or change its settings. Blog settings are now managed via a dedicated, permission-checked endpoint POST /api/workspaces.setBlogSettings (granular blog:write instead of owner role), mirroring the custom-field-labels pattern. As a side effect, workspaces.update no 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_list status is pending are 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 TrailingNode was enabled in the button's paragraph-less inline schema, where it falls back to hardBreak and 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.compile endpoint 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 renders website_url as empty (#342).
  • Refactor: Extracted shared WorkspaceSettings.ResolveEndpoint and BuildWorkspaceTemplateVars helpers, replacing ~8 duplicated copies of the tracking-endpoint resolution and workspace template-object construction across the send and preview paths.

v32.2

Choose a tag to compare

@pierre-b pierre-b released this 31 May 19:09
  • 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/xxx instead of pointing at the tracking domain (#342).

v32.1

Choose a tag to compare

@pierre-b pierre-b released this 30 May 08:05
  • 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 liquidjs to 10.27.0 in console to clear 6 Dependabot alerts (critical RCE, ReDoS in strip_html, date filter padding DoS, {% render %} ownPropertyOnly bypass, empty {% for %} renderLimit bypass, and strip_html newline XSS); npm audit fix also cleared transitive brace-expansion and ws advisories.
  • Fix: Mailgun webhook registration no longer fails with 400 on domains shared with other services — Notifuse now merges its callback URL into each event's existing URL set via PUT (up to Mailgun's limit of 3 per event) instead of always POSTing, and unregistering removes only its own URL while preserving other consumers' (#340).

v32.0

Choose a tag to compare

@pierre-b pierre-b released this 22 May 16:08

Database Schema Changes

  • Migration v32.0 adds a language column (VARCHAR(10) NOT NULL DEFAULT 'en') to the system users table. Existing users default to English.

Features

  • Feature: System emails and the console UI are now localized per user. Each user has a language preference — one of en, 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 new POST /api/user.updateLanguage endpoint. Magic-code emails use the recipient's language, circuit-breaker alerts use each owner's language, and workspace invitations use the inviter's language.

v31.0

Choose a tag to compare

@pierre-b pierre-b released this 19 May 18:21

Database Schema Changes

  • Migration v31.0 updates the queue_contact_for_segment_recomputation trigger function on every workspace database to short-circuit when the inserted contact_timeline row is itself a segment membership event (kind IN ('segment.joined', 'segment.left')).

Fixes

  • Fix: queue_contact_for_segment_recomputation trigger no longer re-enqueues contacts when the inserted contact_timeline event 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_after in UTC. The column is TIMESTAMP WITHOUT TIME ZONE and the scheduler compares it against time.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-pause next_run_after.
  • Fix: GetWorkspaceConnection's pool health check now uses an isolated context for pool.PingContext instead of the caller's. A caller-context cancellation no longer triggers pool eviction.

v30.3

Choose a tag to compare

@pierre-b pierre-b released this 14 May 14:28
  • 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.