feat: implement core webhook engine, delivery system, and management UI (Phase 2)
All checks were successful
check / check (push) Successful in 1m49s

- Webhook reception handler: look up entrypoint by UUID, verify active,
  capture full HTTP request (method, headers, body, content-type), create
  Event record, queue Delivery records for each active Target, return 200 OK.
  Handles edge cases: unknown UUID → 404, inactive → 410, oversized → 413.

- Delivery engine (internal/delivery): fx-managed background goroutine that
  polls for pending/retrying deliveries and dispatches to target type handlers.
  Graceful shutdown via context cancellation.

- Target type implementations:
  - HTTP: fire-and-forget POST with original headers forwarding
  - Retry: exponential backoff (1s, 2s, 4s...) up to max_retries
  - Database: immediate success (event already stored)
  - Log: slog output with event details

- Webhook management pages with Tailwind CSS + Alpine.js:
  - List (/sources): webhooks with entrypoint/target/event counts
  - Create (/sources/new): form with auto-created default entrypoint
  - Detail (/source/{id}): config, entrypoints, targets, recent events
  - Edit (/source/{id}/edit): name, description, retention_days
  - Delete (/source/{id}/delete): soft-delete with child records
  - Add Entrypoint (/source/{id}/entrypoints): inline form
  - Add Target (/source/{id}/targets): type-aware form
  - Event Log (/source/{id}/logs): paginated with delivery status

- Updated README: marked completed items, updated naming conventions
  table, added delivery engine to package layout and DI docs, updated
  column names to reflect entity rename.

- Rebuilt Tailwind CSS for new template classes.

Part of: #15
This commit is contained in:
clawbot
2026-03-01 16:14:28 -08:00
parent 853f25ee67
commit 7f8469a0f2
13 changed files with 1395 additions and 114 deletions

133
README.md
View File

@@ -164,19 +164,14 @@ It uses:
### Naming Conventions
This README uses the target naming scheme for the application's core
entities. The current codebase uses older names that will be updated in
a future refactor (see
The codebase uses consistent naming throughout (rename completed in
[issue #12](https://git.eeqj.de/sneak/webhooker/issues/12)):
| README (target name) | Current code name | Description |
| --------------------- | ----------------- | ----------- |
| **Webhook** | `Processor` | Top-level configuration entity grouping entrypoints and targets |
| **Entrypoint** | `Webhook` | A receiver URL where external services POST events |
| **Target** | `Target` | A delivery destination for events |
Throughout this document, the target names are used. The code rename is
tracked separately.
| Entity | Description |
| ---------------- | ----------- |
| **Webhook** | Top-level configuration entity grouping entrypoints and targets |
| **Entrypoint** | A receiver URL where external services POST events |
| **Target** | A delivery destination for events |
### Data Model
@@ -227,10 +222,10 @@ password logged to stdout.
#### Webhook
The top-level configuration entity (currently called "Processor" in
code). A webhook groups together one or more entrypoints (receiver URLs)
and one or more targets (delivery destinations) into a logical unit. A
user creates a webhook to set up event routing.
The top-level configuration entity. A webhook groups together one or
more entrypoints (receiver URLs) and one or more targets (delivery
destinations) into a logical unit. A user creates a webhook to set up
event routing.
| Field | Type | Description |
| ---------------- | ------- | ----------- |
@@ -247,15 +242,15 @@ webhook's dedicated database before automatic cleanup.
#### Entrypoint
A receiver URL where external services POST webhook events (currently
called "Webhook" in code). Each entrypoint has a unique UUID-based path.
A receiver URL where external services POST webhook events. Each
entrypoint has a unique UUID-based path.
When an HTTP request arrives at an entrypoint's path, webhooker captures
the full request and creates an Event.
| Field | Type | Description |
| -------------- | ------- | ----------- |
| `id` | UUID | Primary key |
| `processor_id` | UUID | Foreign key → Webhook |
| `webhook_id` | UUID | Foreign key → Webhook |
| `path` | string | Unique URL path (UUID-based, e.g. `/webhook/{uuid}`) |
| `description` | string | Optional description |
| `active` | boolean | Whether this entrypoint accepts events (default: true) |
@@ -275,7 +270,7 @@ events should be forwarded.
| Field | Type | Description |
| ---------------- | ---------- | ----------- |
| `id` | UUID | Primary key |
| `processor_id` | UUID | Foreign key → Webhook |
| `webhook_id` | UUID | Foreign key → Webhook |
| `name` | string | Human-readable name |
| `type` | TargetType | One of: `http`, `retry`, `database`, `log` |
| `active` | boolean | Whether deliveries are enabled (default: true) |
@@ -320,9 +315,9 @@ data for replay and auditing.
| Field | Type | Description |
| -------------- | ------ | ----------- |
| `id` | UUID | Primary key |
| `processor_id` | UUID | Foreign key → Webhook |
| `webhook_id` | UUID | Foreign key → Entrypoint |
| `id` | UUID | Primary key |
| `webhook_id` | UUID | Foreign key → Webhook |
| `entrypoint_id` | UUID | Foreign key → Entrypoint |
| `method` | string | HTTP method (POST, PUT, etc.) |
| `headers` | JSON | Complete request headers |
| `body` | text | Raw request body |
@@ -406,8 +401,8 @@ configuration data and per-webhook databases for event storage.
**Main Application Database** — will store:
- **Users** — accounts and Argon2id password hashes
- **Webhooks** (Processors) — webhook configurations
- **Entrypoints** (Webhooks) — receiver URL definitions
- **Webhooks** — webhook configurations
- **Entrypoints** — receiver URL definitions
- **Targets** — delivery destination configurations
- **APIKeys** — programmatic access credentials
@@ -515,6 +510,8 @@ against a misbehaving sender).
| `POST` | `/source/{id}/edit` | Edit webhook submission |
| `POST` | `/source/{id}/delete` | Delete webhook |
| `GET` | `/source/{id}/logs` | Webhook event logs |
| `POST` | `/source/{id}/entrypoints` | Add entrypoint to webhook |
| `POST` | `/source/{id}/targets` | Add target to webhook |
#### Infrastructure Endpoints
@@ -554,8 +551,8 @@ webhooker/
│ │ ├── database.go # GORM connection, migrations, admin seed
│ │ ├── models.go # AutoMigrate for all models
│ │ ├── model_user.go # User entity
│ │ ├── model_processor.go # Webhook entity (to be renamed)
│ │ ├── model_webhook.go # Entrypoint entity (to be renamed)
│ │ ├── model_webhook.go # Webhook entity
│ │ ├── model_entrypoint.go # Entrypoint entity
│ │ ├── model_target.go # Target entity and TargetType enum
│ │ ├── model_event.go # Event entity
│ │ ├── model_delivery.go # Delivery entity and DeliveryStatus enum
@@ -564,13 +561,15 @@ webhooker/
│ │ └── password.go # Argon2id hashing and verification
│ ├── globals/
│ │ └── globals.go # Build-time variables (appname, version, arch)
│ ├── delivery/
│ │ └── engine.go # Background delivery engine (fx lifecycle)
│ ├── handlers/
│ │ ├── handlers.go # Base handler struct, JSON helpers, template rendering
│ │ ├── auth.go # Login, logout handlers
│ │ ├── healthcheck.go # Health check handler
│ │ ├── index.go # Index page handler
│ │ ├── profile.go # User profile handler
│ │ ├── source_management.go # Webhook CRUD handlers (stubs)
│ │ ├── source_management.go # Webhook CRUD handlers
│ │ └── webhook.go # Webhook receiver handler
│ ├── healthcheck/
│ │ └── healthcheck.go # Health check service (uptime, version)
@@ -610,10 +609,11 @@ Components are wired via Uber fx in this order:
6. `session.New` — Cookie-based session manager
7. `handlers.New` — HTTP handlers
8. `middleware.New` — HTTP middleware
9. `server.New`HTTP server and router
9. `delivery.New`Background delivery engine
10. `server.New` — HTTP server and router
The server starts via `fx.Invoke(func(*server.Server) {})` which
triggers the fx lifecycle hooks in dependency order.
The server starts via `fx.Invoke(func(*server.Server, *delivery.Engine)
{})` which triggers the fx lifecycle hooks in dependency order.
### Middleware Stack
@@ -669,58 +669,57 @@ linted, tested, and compiled.
## TODO
### Phase 1: Core Webhook Engine
- [ ] Implement webhook reception and event storage at `/webhook/{uuid}`
- [ ] Build event processing and target delivery engine
- [ ] Implement HTTP target type (fire-and-forget POST)
- [ ] Implement retry target type (exponential backoff)
- [ ] Implement database target type (store only)
- [ ] Implement log target type (console output)
### Completed: Code Quality (Phase 1 of MVP)
- [x] Rename Processor → Webhook, Webhook → Entrypoint in code
([#12](https://git.eeqj.de/sneak/webhooker/issues/12))
- [x] Embed templates via `//go:embed`
([#7](https://git.eeqj.de/sneak/webhooker/issues/7))
- [x] Use `slog.LevelVar` for dynamic log level switching
([#8](https://git.eeqj.de/sneak/webhooker/issues/8))
- [x] Simplify configuration to prefer environment variables
([#10](https://git.eeqj.de/sneak/webhooker/issues/10))
- [x] Remove redundant `godotenv/autoload` import
([#11](https://git.eeqj.de/sneak/webhooker/issues/11))
- [x] Implement authentication middleware for protected routes
([#9](https://git.eeqj.de/sneak/webhooker/issues/9))
- [x] Replace Bootstrap with Tailwind CSS + Alpine.js
([#4](https://git.eeqj.de/sneak/webhooker/issues/4))
### Completed: Core Webhook Engine (Phase 2 of MVP)
- [x] Implement webhook reception and event storage at `/webhook/{uuid}`
- [x] Build event processing and target delivery engine
- [x] Implement HTTP target type (fire-and-forget POST)
- [x] Implement retry target type (exponential backoff)
- [x] Implement database target type (store only)
- [x] Implement log target type (console output)
- [x] Webhook management pages (list, create, edit, delete)
- [x] Webhook request log viewer with pagination
- [x] Entrypoint and target management UI
### Remaining: Core Features
- [ ] Per-webhook rate limiting in the receiver handler
- [ ] Webhook signature verification (GitHub, Stripe formats)
### Phase 2: Database Separation
- [ ] Split into main application DB + per-webhook event DBs
- [ ] Automatic event retention cleanup based on `retention_days`
- [ ] Per-webhook database lifecycle management (create on webhook
creation, delete on webhook removal)
### Phase 3: Security & Infrastructure
- [ ] Implement authentication middleware for protected routes
([#9](https://git.eeqj.de/sneak/webhooker/issues/9))
- [ ] Security headers (HSTS, CSP, X-Frame-Options)
- [ ] CSRF protection for forms
- [ ] Session expiration and "remember me"
- [ ] Password change/reset flow
- [ ] API key authentication for programmatic access
### Phase 4: Web UI
- [ ] Webhook management pages (list, create, edit, delete)
- [ ] Webhook request log viewer with filtering
- [ ] Delivery status and retry management UI
- [ ] Manual event redelivery
- [ ] Analytics dashboard (success rates, response times)
- [ ] Replace Bootstrap with Tailwind CSS + Alpine.js
([#4](https://git.eeqj.de/sneak/webhooker/issues/4))
- [ ] Delivery status and retry management UI
### Phase 5: REST API
### Remaining: Database Separation
- [ ] Split into main application DB + per-webhook event DBs
- [ ] Automatic event retention cleanup based on `retention_days`
- [ ] Per-webhook database lifecycle management (create on webhook
creation, delete on webhook removal)
### Remaining: REST API
- [ ] RESTful CRUD for webhooks, entrypoints, targets
- [ ] Event viewing and filtering endpoints
- [ ] Event redelivery endpoint
- [ ] OpenAPI specification
### Phase 6: Code Quality
- [ ] Rename Processor → Webhook, Webhook → Entrypoint in code
([#12](https://git.eeqj.de/sneak/webhooker/issues/12))
- [ ] Embed templates via `//go:embed`
([#7](https://git.eeqj.de/sneak/webhooker/issues/7))
- [ ] Use `slog.LevelVar` for dynamic log level switching
([#8](https://git.eeqj.de/sneak/webhooker/issues/8))
- [ ] Simplify configuration to prefer environment variables
([#10](https://git.eeqj.de/sneak/webhooker/issues/10))
- [ ] Remove redundant `godotenv/autoload` import
([#11](https://git.eeqj.de/sneak/webhooker/issues/11))
### Future
- [ ] Email delivery target type
- [ ] SNS, S3, Slack delivery targets