Skip to content

feat(inbound-webhooks): cursor-paged submissions read for project keys - #259

Merged
BenAbdou1001 merged 6 commits into
devfrom
feat/239-project-submissions-cursor
Nov 5, 2026
Merged

BenAbdou1001 merged 6 commits into
devfrom
feat/239-project-submissions-cursor

Conversation

@BenAbdou1001

Copy link
Copy Markdown
Collaborator

Closes #239

What and why

Adds GET /api/project/inbound-webhooks/:id/submissions so a project backend can pull its own webhook's submissions incrementally with its API key, oldest first, on a stable keyset cursor. The member endpoint and its offset paging are unchanged.

  • Cursor is opaque base64url of (received_at, id). received_at is carried as Postgres text so microseconds survive; rows sharing a timestamp are ordered by id.
  • limit default 50, max 200. dateFrom / dateTo filter on receivedAt. Items have id, payload, receivedAt, origin.
  • Another project's or unknown webhook: 404. Malformed cursor: 400. Missing key: 401.
  • Each read writes a submissions.read audit entry. The controller's existing project throttle (30/min) applies.
  • Index idx_inbound_webhook_submissions_webhook now covers (webhook_id, received_at, id) (migration 0010).
  • Added to the existing InboundWebhookProjectController; no new module.

Acceptance criteria

  • 250 submissions at limit 100: three pages, each row once, oldest first (e2e "pages 250 submissions")
  • Rows arriving between pages are not skipped or repeated (e2e)
  • Malformed cursor is 400 (e2e, service unit spec)
  • Another project's webhook is 404 (e2e, service unit spec)
  • Every read creates an audit entry (e2e)
  • Documented; OpenAPI reference and CLI API types regenerated (pnpm docs:api, generate:api-types)

How it was checked

  • pnpm lint, pnpm typecheck: pass (existing web warnings only)
  • jest src/modules/inbound-webhooks: 225 passed
  • pnpm run test:e2e -- test/inbound-webhook against Postgres 16: 74 passed
  • Web docs tests (tests/features/docs, tests/app/docs-routes.test.tsx): 716 passed
  • Not run: full pnpm build, full e2e suite, Redis-dependent e2e (left to CI)

Notes

  • Known ceiling: received_at is set by now() at insert, so a transaction that commits late with an earlier timestamp than an already-served cursor can be missed. Negligible at current volumes; a sequence column would remove it.

@vercel

vercel Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
mcid Ready Ready Preview Nov 5, 2026 2:14pm UTC

@BenAbdou1001 BenAbdou1001 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Acceptance criteria (from #239)

  • 250 submissions at limit 100: three pages, each once, oldest first (e2e)
  • Rows arriving between pages are neither skipped nor repeated (e2e)
  • Malformed cursor is 400 (e2e, unit) - now including cursors that pass Date.parse but not Postgres, see fixes
  • Another project's webhook is 404, unknown webhook 404 (e2e, unit)
  • Every read creates a submissions.read audit entry (e2e)
  • Documented; OpenAPI reference and CLI types regenerated (pnpm docs:api re-run: no diff)
  • Functional reqs: project key, opaque (receivedAt, id) cursor, limit 50/200, dateFrom/dateTo, member-endpoint fields, project throttle, member endpoint untouched

Fixes pushed

  • 01bd817 fix(database): rebuild the submissions cursor index only once. Production re-applies every migration on each boot (DEPLOY.md). The generated DROP INDEX + CREATE INDEX in 0010 passed the CI re-apply check but dropped and rebuilt the index on every boot (index OID changed per run, and a plain CREATE INDEX blocks writes on the table while it builds). 0010 is now a DO block that rebuilds only while the index still lacks id; 0004's IF NOT EXISTS then no-ops. Verified: three consecutive applies of the concatenated migrations keep the same OID and the 3-column definition. Entity and snapshot unchanged.
  • 9a6b0d9 fix(inbound-webhooks): answer 400 for a cursor Postgres cannot parse. decodeCursor validates r with Date.parse, which accepts values Postgres rejects ("1", "2026-02-31 ..."), so a tampered cursor answered 500. DrizzleExceptionFilter now maps 22007/22008 (invalid datetime) to 400 like 22P02. The e2e for malformed cursors covers both values (failed with 500 before the fix).

Validation

  • @mcid/api: lint, typecheck, build pass; test:ci 1418/1418 with coverage thresholds (one unrelated flake under parallel turbo load, passed on rerun with and without the change)
  • e2e test/inbound-webhook* against Postgres 16: 74/74
  • Not run locally: Redis-dependent e2e, web/cli suites (no web/cli source changed beyond generated files)

Left for a human

See the inline comments: scope enforcement (issue lists 403) and the late-commit ceiling the author already noted.

};
}

@Get(':id/submissions')

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Owner decision: #239 lists 403 when the project lacks the scope, but nothing here declares a @RequireScope, matching the sibling GET /project/inbound-webhooks. Any key whose project owns the webhook can read its submissions. Fine if webhook ownership is the intended authorisation; otherwise add a scope to both routes in a follow-up.

});
const rows = found.slice(0, f.limit);
const last = rows[rows.length - 1];
const nextCursor =

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Follow-up, not blocking: as the PR notes, received_at is now() at insert (transaction start), so a submission whose transaction commits after a later-started one was already served past the cursor is never returned. Rare at current volume; a bigserial sequence column (or reading only rows older than a few seconds) would close it before the Event Platform relies on it for completeness.

@BenAbdou1001 BenAbdou1001 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Follow-up to the earlier review, both owner decisions are now fixed:

  • 730088b project reads withhold rows received in the last 10 seconds, so a late-committing submission is never skipped (a sequence column would not have fixed this: it is assigned at insert, not commit).
  • 9c26dad the route requires the new read_inbound_submissions scope and a serverId query parameter, as the other scoped project routes do. Contract change for the Event Platform: send serverId and have the scope granted.

CI green on 9c26dad.

@BenAbdou1001
BenAbdou1001 merged commit d0d5e16 into dev Nov 5, 2026
5 checks passed
@BenAbdou1001
BenAbdou1001 deleted the feat/239-project-submissions-cursor branch November 5, 2026 14:58

This branch was successfully deployed

1 active deployment
Preview — 9c26dadf Deployed Nov 5, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant