Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(billing-stripe): webhook idempotency resolves the app's discovered entity (mikro-orm 7.1.x crash) - #247

Merged
RohinBhargava merged 5 commits into
mainfrom
rohin/stripe-webhook-entity-discovery
Aug 11, 2026
Merged

fix(billing-stripe): webhook idempotency resolves the app's discovered entity (mikro-orm 7.1.x crash)#247
RohinBhargava merged 5 commits into
mainfrom
rohin/stripe-webhook-entity-discovery

Conversation

@RohinBhargava

@RohinBhargava RohinBhargava commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes the reported high-severity crash: every Stripe webhook 500'd with Cannot read properties of undefined (reading 'filter') because handleWebhookEvent passed the package's internal StripeWebhookEvent entity object to em.findOne/em.insert — an entity the consuming app's ORM never discovered, which mikro-orm 7.1.x rejects (meta.relations undefined for undiscovered metadata).

Fix (report's Alt A + Option 3 + Option 1, layered)

  • Default (zero consumer changes): lookup/write resolve by name against the app's discovered StripeWebhookEvent (the blueprint app and scaffolds ship one). Undiscovered name now fails with mikro-orm's clear discovery error instead of the cryptic crash. Typed via InferEntity generic; mikro-orm 7 removed string from EntityName's type only, runtime name resolution is unchanged.
  • Mapper-style injection: new optional constructor parameter webhookEventEntity lets an app inject its own entity object, consistent with how Plan/Subscription mappers already work.
  • Registerable internal entity: new ./persistence subpath exports the package's entity definitions for apps that prefer to discover those directly.
  • Second latent bug found by the acceptance harness: em.insert is a native insert that bypasses onCreate hooks — with sqlBaseProperties-style generated ids the row write failed NOT NULL even after the discovery fix. Now em.create + flush.

Acceptance (per report)

New __test__/webhookIdempotency.test.ts — a consuming app whose ORM discovers only its own entity: event handled without the crash, exactly one idempotency row written (generated id), replay of the same idempotency_key is a no-op, and entity injection works.

Also restored the never-committed __test__/test-utils.ts vitest setup in both billing implementations — their suites were unrunnable since inception (the long-standing "missing test-utils" gap) — and completed the plan schema-equality sample that always lacked its required billingProvider. Full suites now: stripe 8 passed, base 5 passed. Blueprint workspace builds green.

Published as @forklaunch/[email protected] (1.1.31 carried the name-resolution fix only; 1.1.32 adds injection + em.create).

Upstream note: the report's defensive (meta.relations ?? []).filter(...) belongs in mikro-orm itself; worth filing there.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved Stripe webhook idempotency so replayed events do not create duplicate records.
    • Ensured webhook records are created consistently with generated identifiers and lifecycle behavior.
  • Tests

    • Added regression coverage for webhook persistence and duplicate event handling.
    • Updated billing plan schema test data to include the Stripe billing provider.
  • Chores

    • Updated the Stripe billing package to version 1.2.0.

…d entity

Every Stripe webhook 500'd on mikro-orm 7.1.x: handleWebhookEvent
queried the package's internal StripeWebhookEvent entity object, which
the consuming app's ORM never discovers, and mikro-orm 7.1.11 crashes
on undiscovered entities (meta.relations undefined in
EntityLoader.lookupEagerLoadedRelationships).

- idempotency lookup/write resolve by entity NAME by default, against
  the app's discovered 'StripeWebhookEvent' (blueprint apps ship one);
  apps can instead inject their own entity mapper-style via a new
  optional constructor parameter
- row write uses em.create + flush instead of native em.insert, which
  bypassed onCreate hooks and failed NOT NULL on sqlBaseProperties-
  style generated ids
- the internal entity set is now importable from a new ./persistence
  subpath for apps that want to register the package's definitions
- acceptance test: an ORM discovering ONLY the app's own entity handles
  an event (no undiscovered-entity crash), writes exactly one
  idempotency row, replays as a no-op, and supports entity injection
- restore the never-committed __test__/test-utils.ts setup file in both
  billing implementations (their suites were unrunnable), and complete
  the plan schema-equality sample that was always missing its required
  billingProvider

Published as @forklaunch/[email protected].

Co-Authored-By: Claude Fable 5 <[email protected]>
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The Stripe package now uses the consuming application's registered webhook entity for MikroORM queries and persistence. The package exports the entity entry point and updates version alignment. Tests cover idempotency, entity injection, encryption setup, and the Stripe billing provider fixture.

Changes

Stripe webhook persistence

Layer / File(s) Summary
Expose persistence entities
blueprint/implementations/billing/stripe/package.json, blueprint/implementations/billing/stripe/tsconfig.build.json, cli/src/core/package_json/package_json_constants.rs
The package exports and builds persistence entities. The Stripe package version changes to 1.2.0, and the CLI constant changes to ~1.2.0.
Resolve the registered webhook entity
blueprint/implementations/billing/stripe/services/webhook.service.ts, blueprint/billing-stripe/registrations.ts
StripeWebhookService accepts a typed entity reference. Idempotency queries and webhook creation use the injected entity. Stripe registration supplies the application-discovered StripeWebhookEvent entity.
Validate webhook and billing test setup
blueprint/implementations/billing/base/__test__/test-utils.ts, blueprint/implementations/billing/stripe/__test__/test-utils.ts, blueprint/implementations/billing/stripe/__test__/schemaEquality.test.ts, blueprint/implementations/billing/stripe/__test__/webhookIdempotency.test.ts
Tests register deterministic encryptors, include the Stripe billing provider in the schema fixture, and verify persistence, replay handling, and injected entity references.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant StripeWebhookService
  participant StripeWebhookEvent
  participant MikroORM
  StripeWebhookService->>StripeWebhookEvent: use injected entity reference
  StripeWebhookService->>MikroORM: check webhook idempotency key
  StripeWebhookService->>MikroORM: create and flush webhook record
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the webhook idempotency fix and the MikroORM 7.1.x entity-discovery crash addressed by the changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rohin/stripe-webhook-entity-discovery

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@blueprint/implementations/billing/stripe/__test__/webhookIdempotency.test.ts`:
- Around line 10-14: Reorder the imports so the external dependencies from
`@mikro-orm/sqlite`, stripe, and uuid come first, followed by the Forklaunch
`@forklaunch/core/persistence` import, while preserving the existing relative
service import.
- Around line 91-125: Isolate the tests by clearing the StripeWebhookEvent table
in beforeEach. In the “replaying the same idempotency key is a no-op” test, call
makeService().handleWebhookEvent(event) twice and retain the single-row
assertion; update the injected-entity test to expect one row rather than relying
on data from earlier tests.

In `@blueprint/implementations/billing/stripe/package.json`:
- Around line 58-60: Update the package exports entry for "./persistence/*" so
it is not left with an incorrect types-only mapping: either remove the wildcard
export and retain "./persistence", or map it to the actual lib/persistence
outputs with valid types, import, require, and default targets.

In `@blueprint/implementations/billing/stripe/services/webhook.service.ts`:
- Around line 185-187: Update the webhook deduplication lookups in the service’s
webhook event handling flow to query by the persisted Stripe event identifier
using stripeId: event.id instead of event.request?.idempotency_key. Apply the
same change to the additional lookup noted by the review, while retaining
idempotencyKey only as stored metadata.
- Around line 185-187: Update the webhook flow around the lookup at em.findOne
and the record creation near the switch handlers so processing is claimed
atomically before any handler side effects run. Add a database uniqueness
constraint for the webhook identity, attempt the claim through an atomic insert
or equivalent, and immediately return when the claim already exists; retain the
existing processing path only for the delivery that successfully claims it.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ad1efdd5-93cc-4551-9c8e-f6406a8363c9

📥 Commits

Reviewing files that changed from the base of the PR and between 8f1e212 and 484c006.

⛔ Files ignored due to path filters (1)
  • blueprint/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • blueprint/implementations/billing/base/__test__/test-utils.ts
  • blueprint/implementations/billing/stripe/__test__/schemaEquality.test.ts
  • blueprint/implementations/billing/stripe/__test__/test-utils.ts
  • blueprint/implementations/billing/stripe/__test__/webhookIdempotency.test.ts
  • blueprint/implementations/billing/stripe/package.json
  • blueprint/implementations/billing/stripe/services/webhook.service.ts
  • blueprint/implementations/billing/stripe/tsconfig.build.json
  • cli/src/core/package_json/package_json_constants.rs

Comment on lines +10 to +14
import { defineComplianceEntity, fp } from '@forklaunch/core/persistence';
import { MikroORM } from '@mikro-orm/sqlite';
import Stripe from 'stripe';
import { v4 } from 'uuid';
import { StripeWebhookService } from '../services/webhook.service';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Move the Forklaunch import after external imports.

Place @forklaunch/core/persistence after @mikro-orm/sqlite, stripe, and uuid.

As per coding guidelines: Organize imports in 7 layers: Node built-ins, external dependencies, Forklaunch framework packages, cross-module imports, local persistence, local domain, and same directory.

Proposed fix
-import { defineComplianceEntity, fp } from '`@forklaunch/core/persistence`';
 import { MikroORM } from '`@mikro-orm/sqlite`';
 import Stripe from 'stripe';
 import { v4 } from 'uuid';
+import { defineComplianceEntity, fp } from '`@forklaunch/core/persistence`';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { defineComplianceEntity, fp } from '@forklaunch/core/persistence';
import { MikroORM } from '@mikro-orm/sqlite';
import Stripe from 'stripe';
import { v4 } from 'uuid';
import { StripeWebhookService } from '../services/webhook.service';
import { MikroORM } from '`@mikro-orm/sqlite`';
import Stripe from 'stripe';
import { v4 } from 'uuid';
import { defineComplianceEntity, fp } from '`@forklaunch/core/persistence`';
import { StripeWebhookService } from '../services/webhook.service';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@blueprint/implementations/billing/stripe/__test__/webhookIdempotency.test.ts`
around lines 10 - 14, Reorder the imports so the external dependencies from
`@mikro-orm/sqlite`, stripe, and uuid come first, followed by the Forklaunch
`@forklaunch/core/persistence` import, while preserving the existing relative
service import.

Source: Coding guidelines

Comment on lines +91 to +125
test('replaying the same idempotency key is a no-op', async () => {
await makeService().handleWebhookEvent(event);

const rows = await orm.em
.fork()
.find('StripeWebhookEvent' as never, {} as never);
expect(rows).toHaveLength(1);
});

test('an app can inject its own discovered entity object (mapper-style)', async () => {
const service = new StripeWebhookService(
null as unknown as Stripe,
orm.em.fork(),
null as never,
noopOtel as never,
null as never,
null as never,
null as never,
null as never,
null as never,
{ USER: 'user' } as never,
StripeWebhookEvent as never
);
const injectedEvent = {
id: 'evt_test_2',
type: 'some.unhandled.event',
request: { idempotency_key: 'ik_test_2' },
data: { object: {} }
} as unknown as Stripe.Event;
await service.handleWebhookEvent(injectedEvent);

const rows = await orm.em
.fork()
.find('StripeWebhookEvent' as never, {} as never);
expect(rows).toHaveLength(2);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Isolate each idempotency test.

The first test leaves one row in the database. The replay test calls the service once, so it passes without testing a replay. The injected-entity test also expects the row from an earlier test.

Clear the table in beforeEach. Call handleWebhookEvent twice in the replay test. Update the injected-entity assertion to expect one row.

Proposed fix
+  beforeEach(async () => {
+    await orm.em
+      .fork()
+      .nativeDelete('StripeWebhookEvent' as never, {} as never);
+  });
+
   test('replaying the same idempotency key is a no-op', async () => {
-    await makeService().handleWebhookEvent(event);
+    const service = makeService();
+    await service.handleWebhookEvent(event);
+    await service.handleWebhookEvent(event);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@blueprint/implementations/billing/stripe/__test__/webhookIdempotency.test.ts`
around lines 91 - 125, Isolate the tests by clearing the StripeWebhookEvent
table in beforeEach. In the “replaying the same idempotency key is a no-op”
test, call makeService().handleWebhookEvent(event) twice and retain the
single-row assertion; update the injected-entity test to expect one row rather
than relying on data from earlier tests.

Comment thread blueprint/implementations/billing/stripe/package.json
Comment on lines 185 to 187
await this.em.findOne<StripeWebhookEventEntity>(this.webhookEventEntity, {
idempotencyKey: event.request?.idempotency_key
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the Stripe event ID for webhook deduplication.

event.request?.idempotency_key can be absent. The current lookup then does not identify one webhook event. Events without that value can be treated as duplicates, or the ORM can receive an undefined criterion.

Query by stripeId: event.id. Retain idempotencyKey only as metadata.

Proposed fix
-      await this.em.findOne<StripeWebhookEventEntity>(this.webhookEventEntity, {
-        idempotencyKey: event.request?.idempotency_key
-      })
+      await this.em.findOne<StripeWebhookEventEntity>(this.webhookEventEntity, {
+        stripeId: event.id
+      })

Also applies to: 506-510

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@blueprint/implementations/billing/stripe/services/webhook.service.ts` around
lines 185 - 187, Update the webhook deduplication lookups in the service’s
webhook event handling flow to query by the persisted Stripe event identifier
using stripeId: event.id instead of event.request?.idempotency_key. Apply the
same change to the additional lookup noted by the review, while retaining
idempotencyKey only as stored metadata.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Claim the webhook atomically before processing side effects.

Two concurrent deliveries can both find no record at Line 185. Both deliveries then run the switch handlers before either creates a record at Line 506.

Add a database uniqueness constraint for the webhook identity. Create an atomic processing claim before the handlers run. Return when the claim already exists.

Also applies to: 503-512

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@blueprint/implementations/billing/stripe/services/webhook.service.ts` around
lines 185 - 187, Update the webhook flow around the lookup at em.findOne and the
record creation near the switch handlers so processing is claimed atomically
before any handler side effects run. Add a database uniqueness constraint for
the webhook identity, attempt the claim through an atomic insert or equivalent,
and immediately return when the claim already exists; retain the existing
processing path only for the delivery that successfully claims it.

…mapper pattern

Per review: the injected webhook entity is now a generic type parameter
on StripeWebhookService (WebhookEventEntity extends
StripeWebhookEventShape), inferred from the injected schema exactly
like the mapper entity generics — no EntityName<any>, no casts at
injection sites. Internally the idempotency flow queries through the
structural shape with the same internal-cast idiom the mapper services
use. Name resolution remains the zero-config default for apps that do
not inject.

Blueprint app registrations inject the app's discovered
StripeWebhookEvent entity (the registrations template is a symlink, so
scaffolds inherit the entity-first pattern automatically).

Published as @forklaunch/[email protected].

Co-Authored-By: Claude Fable 5 <[email protected]>
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Caution

CodeRabbit couldn't update its existing comment. The review summary may be out of date.

Error details
Validation Failed: {"resource":"IssueComment","code":"custom","field":"body","message":"body is too long (maximum is 65536 characters)"} - https://docs.github.com/rest/issues/comments#update-an-issue-comment

… entity

Per review: the sealed package receives the application's discovered
entity itself — a required constructor parameter typed by its ~entity
member (the mapper constraint idiom), generic-inferred for full typing.
The name-resolution default is gone: injection is the contract, and
this is the outer surface where the app always wires it. Internal
queries go through the structural StripeWebhookEventShape with the same
internal-cast idiom the mapper services use.

Published as @forklaunch/[email protected] (required
parameter is an API change). Blueprint app + symlinked scaffold
template already inject the entity.

Co-Authored-By: Claude Fable 5 <[email protected]>
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Caution

CodeRabbit couldn't update its existing comment. The review summary may be out of date.

Error details
Validation Failed: {"resource":"IssueComment","code":"custom","field":"body","message":"body is too long (maximum is 65536 characters)"} - https://docs.github.com/rest/issues/comments#update-an-issue-comment

RohinBhargava and others added 2 commits August 11, 2026 13:52
… files)

A git add sweep included the untracked cli/.docker-cargo registry
cache, which lit up CodeQL with third-party crate alerts. Remove it
from tracking and gitignore both docker build cache dirs.

Co-Authored-By: Claude Fable 5 <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@blueprint/implementations/billing/stripe/services/webhook.service.ts`:
- Around line 128-135: The webhookEventEntity documentation contains an
obsolete, contradictory description. In the service options/type declaration,
remove the preceding default-resolution wording and retain one concise
description stating that webhookEventEntity is required and must be the
ORM-discovered entity.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ab33afa7-4b18-4c34-a8f1-d1dd2545d8d1

📥 Commits

Reviewing files that changed from the base of the PR and between 484c006 and 084d187.

📒 Files selected for processing (6)
  • blueprint/billing-stripe/registrations.ts
  • blueprint/implementations/billing/stripe/__test__/webhookIdempotency.test.ts
  • blueprint/implementations/billing/stripe/package.json
  • blueprint/implementations/billing/stripe/services/webhook.service.ts
  • cli/.gitignore
  • cli/src/core/package_json/package_json_constants.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • cli/src/core/package_json/package_json_constants.rs

Comment on lines +128 to +135
/**
* The application's discovered webhook idempotency entity — this sealed
* package must operate on the entity the app's ORM actually discovered
* (its own definition, or this package's via the ./persistence subpath).
* Querying an undiscovered entity object crashes mikro-orm 7.1.x deep in
* EntityLoader (meta.relations undefined).
*/
webhookEventEntity: { '~entity': WebhookEventEntity }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the obsolete parameter documentation.

webhookEventEntity is required at Line 135. The preceding block states that it has a default name-resolution path. Line 128 starts a second, contradictory parameter block. Keep one description that states the entity is required.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@blueprint/implementations/billing/stripe/services/webhook.service.ts` around
lines 128 - 135, The webhookEventEntity documentation contains an obsolete,
contradictory description. In the service options/type declaration, remove the
preceding default-resolution wording and retain one concise description stating
that webhookEventEntity is required and must be the ORM-discovered entity.

@RohinBhargava
RohinBhargava merged commit 2200c94 into main Aug 11, 2026
15 checks passed
@RohinBhargava
RohinBhargava deleted the rohin/stripe-webhook-entity-discovery branch August 11, 2026 21:07
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.

2 participants