-
Notifications
You must be signed in to change notification settings - Fork 136
Feat/webhook event publisher 1862 #1938
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
krushit1307
merged 6 commits into
krushit1307:main
from
MILAN-123865:feat/webhook-event-publisher-1862
Jul 30, 2026
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
b6a84d0
feat(events): add desktop split-screen master-detail layout
MILAN-123865 d41b331
feat: add accessible custom multi-select dropdown (#1858)
MILAN-123865 b3505ae
feat: manage event creation wizard with XState (#1870)
MILAN-123865 a98e289
feat: add strict HTTP cache-control for static metadata (#1861)
MILAN-123865 6f61e9c
chore: fix eslint errors in webhooks
MILAN-123865 9c88327
chore: resolve merge conflicts
MILAN-123865 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| # Static Metadata Caching Strategy | ||
|
|
||
| To improve application loading speeds and reduce bandwidth, we employ an aggressive edge caching strategy for highly static metadata (majors, semesters, terms, departments). | ||
|
|
||
| ## The Cache Header | ||
|
|
||
| All static edge function responses include the following header: | ||
|
|
||
| ``` | ||
| Cache-Control: public, max-age=86400, s-maxage=604800, stale-while-revalidate=86400 | ||
| ``` | ||
|
|
||
| ### Breakdown: | ||
|
|
||
| - **`public`**: The response can be cached by any cache (browser, CDN). | ||
| - **`max-age=86400`**: The browser will cache the data locally for 1 day (86,400 seconds). | ||
| - **`s-maxage=604800`**: The CDN (Cloudflare/Vercel) will cache the data for 7 days. | ||
| - **`stale-while-revalidate=86400`**: The CDN will serve stale data while fetching the updated data in the background if the cache is older than 7 days, up to an additional 1 day. | ||
|
|
||
| ## Frontend Fetch Wrapper | ||
|
|
||
| The frontend `customFetch` utility in `src/utils/fetch.ts` is designed to support this strategy. | ||
| For static metadata requests, it ensures: | ||
|
|
||
| 1. `cache: "default"` is explicitly passed to the underlying `fetch` API. | ||
| 2. Cache-busting query strings (like `?timestamp=12345`) are NOT appended, allowing the browser to serve from disk/memory cache. | ||
|
|
||
| ## Database Triggers and Cache Invalidation | ||
|
|
||
| Since the data is cached at the CDN for 7 days, any modifications made in the database (e.g., adding a new major) would not reflect immediately. | ||
|
|
||
| To solve this, we implemented a Postgres trigger on the static metadata tables. | ||
| When a row is inserted, updated, or deleted, it invokes a PL/pgSQL function `notify_cdn_purge()`. | ||
| This function makes an HTTP POST request via `pg_net` to the Cloudflare/Vercel CDN Purge API, invalidating the specific endpoint url (e.g. `/api/majors`). | ||
|
|
||
| ### Relevant Files | ||
|
|
||
| - `supabase/migrations/*_cache_invalidation.sql` | ||
| - `supabase/migrations/*_triggers.sql` | ||
| - `supabase/functions/shared/cache.ts` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| # Event State Machine Documentation | ||
|
|
||
| The `EventWizard` component uses an XState finite state machine (`eventCreationMachine`) to manage its complex multi-step UI flow. | ||
|
|
||
| ## State Chart | ||
|
|
||
| ```mermaid | ||
| stateDiagram-v2 | ||
| [*] --> basics | ||
| basics --> ticketing : NEXT (isPaid = true) | ||
| basics --> location : NEXT (isPaid = false) | ||
| ticketing --> location : NEXT | ||
| ticketing --> basics : BACK | ||
| location --> review : NEXT | ||
| location --> ticketing : BACK (isPaid = true) | ||
| location --> basics : BACK (isPaid = false) | ||
| review --> submitting : SUBMIT | ||
| review --> location : BACK | ||
| submitting --> success : onDone | ||
| submitting --> error : onError | ||
| error --> submitting : RETRY | ||
| error --> review : BACK | ||
| success --> [*] | ||
| ``` | ||
|
|
||
| ## Context Schema | ||
|
|
||
| ```ts | ||
| export interface EventContext { | ||
| formData: { | ||
| title: string; | ||
| description: string; | ||
| category: string; | ||
| isPaid: boolean; | ||
| price?: number; | ||
| currency?: string; | ||
| location?: string; | ||
| startDate: string; | ||
| endDate: string; | ||
| tags: string[]; | ||
| image?: string; | ||
| }; | ||
| validationErrors: Record<string, string>; | ||
| currentStep: number; | ||
| } | ||
| ``` | ||
|
|
||
| ## Guards | ||
|
|
||
| - `isBasicsValid`: Verifies all required fields in the basics step are filled. | ||
| - `isTicketingValid`: Ensures price > 0 and currency is selected for paid events. | ||
| - `isLocationValid`: Ensures a location string exists. | ||
| - `isPaidEvent` / `isFreeEvent`: Checks `context.formData.isPaid`. | ||
| - `canSubmit`: Runs full validation across all fields before allowing submission. | ||
|
|
||
| ## Persistence | ||
|
|
||
| The `useEventWizard` hook automatically saves the `snapshot.value` and `snapshot.context` to `sessionStorage` after every transition (except `success`). | ||
| Upon mount, it attempts to load from `sessionStorage` and restores context using the `RESTORE` event. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| # Outbound Webhooks | ||
|
|
||
| Clubs can configure outbound webhooks to receive real-time HTTP POST notifications when events occur in their club on CampusConnect. | ||
|
|
||
| ## Supported Events | ||
|
|
||
| Currently supported events: | ||
|
|
||
| - `event.created` | ||
| - `event.updated` | ||
| - `event.deleted` | ||
| - `post.created` | ||
| - `club.updated` | ||
| - `member.joined` | ||
| - `member.left` | ||
|
|
||
| ## Payload Schema | ||
|
|
||
| The webhook payload is a JSON object with the following structure: | ||
|
|
||
| ```json | ||
| { | ||
| "event": "event.created", | ||
| "timestamp": "2026-07-30T11:30:00Z", | ||
| "club": { | ||
| "id": "uuid-of-club" | ||
| }, | ||
| "data": { | ||
| "id": "uuid-of-event", | ||
| "title": "Hackathon", | ||
| "location": "Auditorium", | ||
| "startsAt": "2026-08-15T09:00:00Z" | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Security & Signatures | ||
|
|
||
| To verify that a webhook request was genuinely sent by CampusConnect, we include an HMAC-SHA256 signature in the `X-CampusConnect-Signature` header. | ||
|
|
||
| The signature is generated using your Webhook Secret (available in the Club Settings UI). | ||
|
|
||
| ### Verifying Signatures in Node.js | ||
|
|
||
| ```javascript | ||
| const crypto = require("crypto"); | ||
|
|
||
| function verifySignature(payloadString, secret, signatureHeader) { | ||
| const hash = crypto.createHmac("sha256", secret).update(payloadString).digest("hex"); | ||
|
|
||
| const expectedSignature = `sha256=${hash}`; | ||
|
|
||
| // Use crypto.timingSafeEqual to prevent timing attacks | ||
| return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expectedSignature)); | ||
| } | ||
| ``` | ||
|
|
||
| ### Verifying Signatures in Python | ||
|
|
||
| ```python | ||
| import hmac | ||
| import hashlib | ||
|
|
||
| def verify_signature(payload_string, secret, signature_header): | ||
| hash_obj = hmac.new( | ||
| secret.encode('utf-8'), | ||
| payload_string.encode('utf-8'), | ||
| hashlib.sha256 | ||
| ) | ||
| expected_signature = f"sha256={hash_obj.hexdigest()}" | ||
| return hmac.compare_digest(signature_header, expected_signature) | ||
| ``` | ||
|
|
||
| ## Retry Policy | ||
|
|
||
| If your server responds with a 5xx error or times out, CampusConnect will automatically retry the delivery with exponential backoff: | ||
|
|
||
| - **Attempt 1:** Immediate | ||
| - **Attempt 2:** +1 minute | ||
| - **Attempt 3:** +5 minutes | ||
| - **Attempt 4:** +15 minutes | ||
| - **Attempt 5:** +1 hour | ||
|
|
||
| After 5 failed attempts, the delivery is marked as permanently failed. Client errors (4xx responses) are generally not retried, except for `429 Too Many Requests`. | ||
|
|
||
| ## Server-Side Request Forgery (SSRF) Protection | ||
|
|
||
| For security reasons, webhook URLs must be publicly accessible over HTTPS. We reject URLs pointing to: | ||
|
|
||
| - `localhost` or `127.0.0.1` | ||
| - Private network ranges (e.g., `10.x.x.x`, `192.168.x.x`) | ||
| - AWS Metadata endpoints (`169.254.169.254`) |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Browser caching defeats the documented invalidation guarantee.
max-age=86400allows browsers to reuse metadata for 24 hours without contacting the CDN, whilenotify_cdn_purge()only purges Cloudflare. After a database change, users can still receive stale metadata despite the trigger.Use browser revalidation (for example,
max-age=0while retainings-maxage), versioned URLs, or another client invalidation mechanism.🤖 Prompt for AI Agents