feat(backend): add webhook delivery system for card view events#187
feat(backend): add webhook delivery system for card view events#187Dipti45sktech wants to merge 1 commit into
Conversation
|
Hi @ShantKhatri, could you please review my PR? |
Could you please add the test proofs in the PR description as well? Since this PR involves schema changes, I have marked it as |
| secret String // encrypted via encryption.ts | ||
| events String[] // e.g. ["card.viewed", "contact.saved"] | ||
| isActive Boolean @default(true) @map("is_active") | ||
| createdAt DateTime @default(now()) @map("created_at") |
There was a problem hiding this comment.
Missing updatedAt needed for secret rotation auditing
| responseCode Int? @map("response_code") | ||
| attempts Int @default(0) | ||
| nextRetryAt DateTime? @map("next_retry_at") | ||
| createdAt DateTime @default(now()) @map("created_at") |
There was a problem hiding this comment.
Missing updatedAt can't tell when last event fired.
| endpointId String @map("endpoint_id") | ||
| eventType String @map("event_type") | ||
| payload Json | ||
| status String @default("pending") // "pending" | "success" | "failed" |
There was a problem hiding this comment.
Missing errorMessage String no way to surface failure reason.
| responseCode Int? @map("response_code") | ||
| attempts Int @default(0) | ||
| nextRetryAt DateTime? @map("next_retry_at") | ||
| createdAt DateTime @default(now()) @map("created_at") |
There was a problem hiding this comment.
Missing deliveredAt DateTime? no success timestamp.
|
|
||
| model WebhookDelivery { | ||
| id String @id @default(uuid()) | ||
| endpointId String @map("endpoint_id") |
There was a problem hiding this comment.
No indexing on endpointId delivery log query will be slow.
| attempts Int @default(0) | ||
| nextRetryAt DateTime? @map("next_retry_at") | ||
| createdAt DateTime @default(now()) @map("created_at") | ||
|
|
There was a problem hiding this comment.
No indexing for status and nextTryAt worker will scan whole thing.
| * Max 5 endpoints per user. Auto-generates and encrypts a secret. | ||
| * Returns the plaintext secret once — user must store it. | ||
| */ | ||
| app.post('/', async (request: FastifyRequest, reply: FastifyReply) => { |
There was a problem hiding this comment.
Request route and Request schema seems is missing here.
| } | ||
|
|
||
| // Enforce max 5 endpoints per user | ||
| const existingCount = await app.prisma.webhookEndpoint.count({ |
There was a problem hiding this comment.
This works,though count check alone may still allow race conditions
| import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; | ||
| import crypto from 'crypto'; | ||
| import { z } from 'zod'; | ||
| import { encrypt } from '../utils/encryption.js'; |
| }, | ||
| }); | ||
|
|
||
| return reply.status(201).send({ |
| * Returns all webhook endpoints for the authenticated user. | ||
| * The secret field is never returned. | ||
| */ | ||
| app.get('/', async (request: FastifyRequest, reply: FastifyReply) => { |
There was a problem hiding this comment.
Request route and Request schema seems is missing here.
| app.get('/', async (request: FastifyRequest, reply: FastifyReply) => { | ||
| const userId = (request.user as any).id; | ||
|
|
||
| const endpoints = await app.prisma.webhookEndpoint.findMany({ |
There was a problem hiding this comment.
Should we add a limit here?
|
Error handling around the business logic can be improved here. |
Summary
Implements the webhook delivery system described in issue #40. Users can register external URLs to receive signed POST requests whenever their card or profile is viewed. The system handles payload signing with HMAC-SHA256, retries failed deliveries with exponential backoff, and logs every attempt for observability.
The
contact.savedevent is wired into the schema and validation but isn't dispatched yet since the contact-save feature doesn't exist in the codebase - left a TODO for when that gets built.Closes #40
What Changed
prisma/schema.prisma- AddedWebhookEndpointandWebhookDeliverymodels with a relation back toUser. Endpoints store an encrypted secret and a list of subscribed event types. Deliveries track status, response codes, attempt counts, and retry scheduling.src/utils/webhookDispatch.ts(new) - Core dispatch logic.dispatchWebhook()finds matching endpoints for a user+event, creates delivery records, then fires off async HTTP POSTs. Each request is signed withX-DevCard-Signature: sha256=<hex>using HMAC-SHA256. Failed deliveries retry up to 3 times at 30s, 5min, and 30min intervals.src/routes/webhooks.ts(new) - CRUD routes for managing webhook endpoints: register (max 5 per user), list, delete, view delivery logs (paginated), and rotate secret. Secrets are auto-generated, encrypted at rest, and only shown in plaintext once at creation/rotation.src/app.ts- Registered the new webhook routes at/api/webhooks.src/routes/public.ts- HookeddispatchWebhook()into the two card/profile view handlers socard.viewedevents fire after view tracking.src/__tests__/webhooks.test.ts(new) - 17 tests covering endpoint registration, max limit enforcement, validation, listing, deletion, delivery logs pagination, secret rotation, HMAC signature correctness, and delivery success/failure/timeout scenarios.How to Test
pnpm installfrom the repo rootpnpm testfromapps/backend- all 25 tests should pass (17 new + 8 existing)npx prisma migrate devfromapps/backend(requires a running Postgres instance)POST /api/webhookswith{ "url": "https://your-endpoint.com", "events": ["card.viewed"] }(needs auth token)GET /api/u/:username- your endpoint should receive a signed POSTAdditional Context
tscbuild has errors but they're all pre-existing across the codebase (e.g.app.authenticatetype augmentation missing from every route file, implicitanyparams incards.ts,follow.ts, etc.). My new files only carry the sameauthenticatepattern -webhookDispatch.tscompiles clean.setTimeoutfor now. For production at scale, this should probably move to a proper job queue (e.g. BullMQ backed by the existing Redis instance), but that felt out of scope for this PR.contact.savedevent type is accepted in endpoint registration and validation, but nothing dispatches it yet since there's no contact-save feature. Added a TODO comment inpublic.tsso it's easy to wire up later.encryption.ts- no new crypto dependencies.