- 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.