Skip to content

Commit 0f9121e

Browse files
committed
changelog
1 parent 32855cb commit 0f9121e

File tree

2 files changed

+138
-0
lines changed

2 files changed

+138
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.
44

55
## [28.0] - 2026-03-05
66

7+
- **Templates**: Added option to choose between visual email builder or MJML code editor when creating templates
78
- **Templates**: Added ability to translate email templates to languages configured in workspace settings
89
- **Contacts**: Fixed invalid "Blacklisted" status option in change status dropdown, replaced with valid "Bounced" and "Complained" statuses (#285)
910

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Webhook Events on Template CRUD
2+
3+
## Status: Feasibility Investigation (complete)
4+
5+
## Summary
6+
7+
Adding webhook events for template CRUD operations is **highly feasible** and follows an established pattern already used by 5 other entity types (contacts, lists, segments, emails, custom_events).
8+
9+
## Current Webhook Architecture
10+
11+
All outgoing webhook events are generated via **PostgreSQL AFTER triggers** on workspace tables. When a row is inserted/updated/deleted, the trigger function:
12+
13+
1. Determines the event type (e.g. `contact.created`)
14+
2. Builds a JSONB payload
15+
3. Queries `webhook_subscriptions` for matching subscriptions
16+
4. Inserts rows into `webhook_deliveries` with `status='pending'`
17+
18+
The `WebhookDeliveryWorker` polls every 10s, signs the payload with HMAC-SHA256 (Standard Webhooks spec), and delivers via HTTP POST with exponential backoff retries (up to 10 attempts over ~48h).
19+
20+
### Key Files
21+
22+
| Layer | File |
23+
|---|---|
24+
| Event types | `internal/domain/webhook_subscription.go` (line 79: `WebhookEventTypes`) |
25+
| Delivery worker | `internal/service/webhook_delivery_worker.go` |
26+
| Trigger examples | `internal/migrations/v19.go` (contacts, lists, segments, emails, custom_events) |
27+
| Subscription service | `internal/service/webhook_subscription_service.go` |
28+
| Template service | `internal/service/template_service.go` |
29+
| Template repo | `internal/repository/template_postgres.go` |
30+
| Template domain | `internal/domain/template.go` |
31+
32+
## New Event Types
33+
34+
```
35+
template.created
36+
template.updated
37+
template.deleted
38+
```
39+
40+
## Design Consideration: Template Versioning
41+
42+
Templates use an **INSERT-based versioning model** — updates don't `UPDATE` the row, they `INSERT` a new row with an incremented `version`. Deletes are **soft deletes** (set `deleted_at`). This affects trigger logic:
43+
44+
| Operation | DB Operation | Trigger Detection |
45+
|---|---|---|
46+
| Create | `INSERT` | `AFTER INSERT` where `version = 1` and `deleted_at IS NULL` |
47+
| Update | `INSERT` (new version) | `AFTER INSERT` where `version > 1` and `deleted_at IS NULL` |
48+
| Delete | `UPDATE` (set deleted_at) | `AFTER UPDATE` where `deleted_at` transitions from `NULL` to non-`NULL` |
49+
50+
This is different from contacts (which use standard INSERT/UPDATE/DELETE) but straightforward in PL/pgSQL:
51+
52+
```sql
53+
IF TG_OP = 'INSERT' THEN
54+
IF NEW.version = 1 THEN
55+
event_kind := 'template.created';
56+
ELSE
57+
event_kind := 'template.updated';
58+
END IF;
59+
ELSIF TG_OP = 'UPDATE' THEN
60+
IF NEW.deleted_at IS NOT NULL AND OLD.deleted_at IS NULL THEN
61+
event_kind := 'template.deleted';
62+
ELSE
63+
RETURN NEW; -- skip non-delete updates
64+
END IF;
65+
END IF;
66+
```
67+
68+
## Design Decision: Payload Content
69+
70+
Templates can carry large MJML trees and compiled HTML in their `email` JSONB column. Two options:
71+
72+
### Option A: Lightweight payload (recommended)
73+
74+
Send only metadata — consumer calls back for full details if needed:
75+
76+
```json
77+
{
78+
"template": {
79+
"id": "welcome-email",
80+
"name": "Welcome Email",
81+
"version": 3,
82+
"channel": "email",
83+
"category": "transactional",
84+
"created_at": "...",
85+
"updated_at": "..."
86+
}
87+
}
88+
```
89+
90+
**Pros**: Small payload, fast delivery, no risk of exceeding webhook size limits.
91+
**Cons**: Consumer needs an extra API call for full template content.
92+
93+
### Option B: Full payload
94+
95+
Send the entire template object including MJML tree and compiled HTML.
96+
97+
**Pros**: Consumer has everything in one delivery.
98+
**Cons**: Payloads could be very large (MJML trees + compiled HTML), may cause delivery timeouts or exceed consumer limits.
99+
100+
## What Needs to Change
101+
102+
### 1. Domain layer (~3 lines)
103+
104+
Add 3 event types to `WebhookEventTypes` in `internal/domain/webhook_subscription.go`.
105+
106+
### 2. New database migration (~80 lines SQL)
107+
108+
New migration file (e.g. `internal/migrations/v28.go`) with:
109+
- `webhook_templates_trigger()` function
110+
- `AFTER INSERT OR UPDATE` trigger on `templates` table
111+
- Following the exact same pattern as `webhook_contacts_trigger()` in v19
112+
113+
### 3. Tests (~50 lines)
114+
115+
- Update `WebhookEventTypes` test expectations in `internal/domain/webhook_subscription_test.go`
116+
- Add migration tests in `internal/migrations/v28_test.go`
117+
118+
### 4. Frontend: zero changes
119+
120+
The subscription UI dynamically reads available event types from the `GET /api/webhookSubscriptions.eventTypes` endpoint, which returns `WebhookEventTypes`. New types will appear automatically.
121+
122+
### 5. No changes needed to
123+
124+
- Delivery worker (generic, processes any event type)
125+
- Signing/retry logic
126+
- Subscription service or HTTP handlers
127+
- Template service or repository (triggers are at DB level)
128+
129+
## Out of Scope: Template Blocks
130+
131+
Template blocks are stored inside `workspace.Settings` (not a separate table), so adding webhook triggers for block CRUD would require an **application-level approach** — inserting into `webhook_deliveries` from Go code in the service layer. This is a different pattern from the rest and should be a separate initiative if desired.
132+
133+
## Risk Assessment
134+
135+
- **Low risk**: The pattern is battle-tested with 5 other entity types
136+
- **Only nuance**: The version-based INSERT pattern requires careful trigger logic (see above)
137+
- **No breaking changes**: Existing subscriptions are unaffected; new events only fire for subscriptions that opt in

0 commit comments

Comments
 (0)