Skip to content

feat(backend): Add webhook delivery system for card view events#590

Open
Dipti45sktech wants to merge 3 commits into
Dev-Card:mainfrom
Dipti45sktech:feature
Open

feat(backend): Add webhook delivery system for card view events#590
Dipti45sktech wants to merge 3 commits into
Dev-Card:mainfrom
Dipti45sktech:feature

Conversation

@Dipti45sktech

Copy link
Copy Markdown
Contributor

Summary

Implements the full webhook delivery system for card view and contact-save events as specified in issue #40. This includes endpoint registration, HMAC-SHA256 payload signing, exponential backoff retry logic, delivery logging, and full test coverage. The idea is simple — when someone views a card or saves a contact, any external system that has registered a webhook endpoint should get notified automatically with a signed POST request.

Closes #40


Type of Change

  • New feature
  • Tests only

What Changed

  • Added WebhookEndpoint and WebhookDelivery models to Prisma schema
  • Created apps/backend/src/routes/webhooks.ts with 4 REST endpoints (POST to register, GET to list, DELETE to remove, GET deliveries for logs)
  • Created apps/backend/src/utils/webhookDispatch.ts with HMAC-SHA256 payload signing via x-DevCard-Signature header and retry logic with exponential backoff (30s - 5min - 30min)
  • Hooked dispatchWebhook into card view and contact-save events in app.ts
  • Added 17 tests in webhooks.test.ts covering endpoint registration, signature generation, and delivery retry logic.

How to Test

  1. Run pnpm -r run test - all 17 webhook tests should pass
  2. Register a webhook endpoint via POST /api/webhooks
  3. Trigger a card view event and verify the external endpoint receives a signed POST request with x-DevCard-Signature header

Checklist

  • I have added or updated tests for the changes I made.
  • All tests pass locally (pnpm -r run test).
  • No new console.log or debug statements left in the code.
image

@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

@Dipti45sktech is attempting to deploy a commit to the Prashantkumar Khatri's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added backend gssoc:approved Required label for every approved PR. Gives the base +50 points and enables contribution tracking. labels Jun 17, 2026
@github-actions

Copy link
Copy Markdown

Hi @Dipti45sktech,

Thanks for opening this pull request.

This PR has been automatically classified based on the files modified.

Applied Labels

  • gssoc:approved
  • backend

Primary Review Area

  • backend

Reviewer

@Harxhit has been identified as the primary reviewer for this pull request.

If you have any questions regarding the affected area or implementation details, feel free to reach out to the assigned reviewer.

Thank you for your contribution!

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown

CI — Checks Failed

Backend — FAIL

Check Result
Lint SKIP
Test SKIP
Typecheck SKIP

Mobile — SKIP

Check Result
Lint -
Test -

Web — SKIP

Check Result
Build -

Last updated: Wed, 17 Jun 2026 04:14:06 GMT

@Dipti45sktech

Copy link
Copy Markdown
Contributor Author

Hi @ShantKhatri please reassign me on issue #40 - I had completed this implementation earlier but the PR had to be closed due to branch conflicts. This is a clean resubmission of the same work. Kindly take a look.

@Harxhit Harxhit left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of the webhook delivery system. The route/dispatch design is solid (HMAC signing, encrypted secret at rest, ownership checks, endpoint cap), but there are blocking issues: the Prisma schema is corrupted, the core WebhookEndpoint model is missing, and dispatchWebhook is never called from any event handler — so card-view/contact-save events don't actually trigger anything as shipped. Inline comments below. Note: this PR also carries unrelated changes (CI lint removal, app.ts/auth/seed rewrites) that I'd recommend splitting out.

ownedTeams Team[] @relation("TeamOwner")
teamMemberships TeamMember[] @relation("TeamMember")

webhookEndpoints WebhookEndpoint[]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User.webhookEndpoints references a WebhookEndpoint model that is never defined anywhere in the schema (the routes call prisma.webhookEndpoint.*). prisma generate will fail. The model needs to be added, e.g.:

model WebhookEndpoint {
  id        String   @id @default(uuid())
  userId    String   @map("user_id")
  url       String
  secret    String
  events    String[]
  isActive  Boolean  @default(true) @map("is_active")
  createdAt DateTime @default(now()) @map("created_at")

  user       User              @relation(fields: [userId], references: [id], onDelete: Cascade)
  deliveries WebhookDelivery[]

  @@index([userId])
  @@map("webhook_endpoints")
}

userId String
eventId String
joinedAt DateTime
joinedAt DateTime

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EventAttendee lost its relations and @@unique, and runs straight into WebhookDelivery with no separator (note the stray } on the next line). This breaks the existing EventAttendee model.

Suggested change
joinedAt DateTime
joinedAt DateTime
event Event @relation(fields: [eventId], references: [id])
user User @relation(fields: [userId], references: [id])
@@unique([userId, eventId])
}
model WebhookDelivery {

joinedAt DateTime
joinedAt DateTime
}
model WebhookDelivery {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As written, this WebhookDelivery block wrongly inherited EventAttendee's event/user relations and @@unique([userId, eventId]) (lines ~210-213). It should relate to the endpoint instead:

  endpoint WebhookEndpoint @relation(fields: [endpointId], references: [id], onDelete: Cascade)

createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
errorMessage String? @map("error_message")
deliveredAt DateTime? @map("delivered_at")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

errorMessage and deliveredAt are added here but deliverWebhook in webhookDispatch.ts never writes them — no error captured on failure, no timestamp on success. Either populate them in the delivery update calls or drop the columns.

Comment thread apps/backend/src/app.ts
cookieName: 'token',
signed: false,
},
secret: process.env.JWT_SECRET || 'dev-secret-change-me',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regression / security downgrade: this replaces a validated, required JWT_SECRET with a hardcoded fallback, and drops the cookie config. A predictable signing key is used if the env var is unset.

Suggested change
secret: process.env.JWT_SECRET || 'dev-secret-change-me',
secret: process.env.JWT_SECRET!,
cookie: {
cookieName: 'token',
signed: false,
},

Comment thread apps/backend/src/app.ts
secret: process.env.JWT_SECRET || 'dev-secret-change-me',
});

await app.register(cookie);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cookie is already registered a few lines above — this duplicate registration will throw FST_ERR_PLUGIN_ALREADY_PRESENT. Remove it.

Suggested change
await app.register(cookie);

Comment thread apps/backend/src/app.ts
// token is rejected immediately even if it has not yet expired.
// The blocklist check is skipped when Redis is not registered (test env).
app.decorate('authenticate', async function (request: FastifyRequest, reply: FastifyReply) {
app.decorate('authenticate', async function (request: any, reply: any) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't loosen the handler signature to any, any — keep the typed Fastify signature (this also reverts existing typing).

Suggested change
app.decorate('authenticate', async function (request: any, reply: any) {
app.decorate('authenticate', async function (request: FastifyRequest, reply: FastifyReply) {

},
}, async (request: FastifyRequest, reply: FastifyReply) => {
const userId = (request.user as any).id;
const limit = Math.min(100, parseInt((request.query as any).limit || '20', 10));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Fastify querystring schema already coerces limit to integer with a default of 20, so parseInt(... || '20', 10) runs parseInt on a number. Redundant — rely on the schema:

Suggested change
const limit = Math.min(100, parseInt((request.query as any).limit || '20', 10));
const limit = (request.query as any).limit ?? 20;

});

// Schedule retry (non-blocking, in-process)
setTimeout(() => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retries are scheduled with in-process setTimeout (up to 30 min). Any process restart silently drops all pending retries, and the persisted nextRetryAt + @@index([status, nextRetryAt]) you added are never used to recover them. Worth a comment acknowledging the limitation, or a follow-up to drive retries from a DB poller — otherwise the persisted retry state is misleading.

Separately: dispatchWebhook (defined in this file) has no call site anywhere in the diff, so no card-view/contact-save handler actually triggers delivery. It needs to be wired into those event handlers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend gssoc:approved Required label for every approved PR. Gives the base +50 points and enables contribution tracking.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

backend: implement webhook delivery system for card view and contact-save events

2 participants