Clean up Stripe DB and webhooks on uninstall#2367
Conversation
lkostrowski
commented
May 26, 2026
- Reuse existing abstraction to clean up APL
- Extend data access layer (repositories) to be able to prune data
- Prune data on uninstall
- Clean up webhooks
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: 14ff8ef The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #2367 +/- ##
==========================================
+ Coverage 38.01% 38.07% +0.06%
==========================================
Files 1048 1051 +3
Lines 67139 67479 +340
Branches 3580 3600 +20
==========================================
+ Hits 25521 25694 +173
- Misses 41228 41395 +167
Partials 390 390
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR adds an APP_DELETED webhook to the Stripe app to perform best-effort uninstall cleanup: removing Stripe-side webhooks and pruning app-owned DynamoDB data (configs, channel mappings, recorded transactions) in addition to APL pruning.
Changes:
- Introduce
WipeAppDataUseCaseand wire it into a newAPP_DELETEDwebhook route, and register it in the app manifest. - Extend the DynamoDB repositories to support “remove all for app” operations for configs/mappings/transactions.
- Add unit + integration tests covering uninstall cleanup and new repository pruning methods.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/stripe/src/modules/transactions-recording/repositories/transaction-recorder-repo.ts | Extends repo contract with removeAllForApp and a new error type. |
| apps/stripe/src/modules/transactions-recording/repositories/dynamodb/recorded-transaction-db-model.ts | Adds an SK prefix helper to query all transaction items in a partition. |
| apps/stripe/src/modules/transactions-recording/repositories/dynamodb/dynamodb-transaction-recorder-repo.ts | Implements removeAllForApp via query + delete. |
| apps/stripe/src/modules/app-uninstall/wipe-app-data-use-case.ts | New use case orchestrating Stripe webhook deletion + DynamoDB data pruning on uninstall. |
| apps/stripe/src/modules/app-uninstall/wipe-app-data-use-case.test.ts | Unit tests for uninstall cleanup behavior and error tolerance. |
| apps/stripe/src/modules/app-config/repositories/dynamodb/dynamodb-app-config-repo.ts | Adds getAllConfigs, removeAllConfigs, removeAllChannelMappings. |
| apps/stripe/src/modules/app-config/repositories/app-config-repo.ts | Extends AppConfigRepo interface with bulk read/delete operations. |
| apps/stripe/src/app/api/webhooks/app-deleted/webhook-definition.ts | New APP_DELETED webhook definition wiring uninstall cleanup + APL handling. |
| apps/stripe/src/app/api/webhooks/app-deleted/route.ts | New App Router route for APP_DELETED webhook. |
| apps/stripe/src/app/api/manifest/route.ts | Registers APP_DELETED webhook in the manifest. |
| apps/stripe/src/app/api/manifest/route.test.ts | Updates manifest test expectations to include APP_DELETED webhook. |
| apps/stripe/src/tests/mocks/mocked-transaction-recorder.ts | Updates mock repo with removeAllForApp. |
| apps/stripe/src/tests/mocks/app-config-repo.ts | Updates mock AppConfigRepo with new bulk methods. |
| apps/stripe/src/tests/integration/transaction-recorder/remove-all-transactions.integration.test.ts | Integration coverage for transaction repo bulk deletion. |
| apps/stripe/src/tests/integration/config-repo/remove-all-app-data.integration.test.ts | Integration coverage for config repo bulk read/delete operations. |
| .changeset/stripe-app-deleted-handler.md | Changeset describing the new uninstall handler behavior. |
| const query = this.entity.table | ||
| .build(QueryCommand) | ||
| .entities(this.entity) | ||
| .query({ | ||
| range: { | ||
| beginsWith: DynamoDbRecordedTransaction.accessPattern.getSKforAllItems(), | ||
| }, | ||
| partition: DynamoDbRecordedTransaction.accessPattern.getPK(accessPattern), | ||
| }) | ||
| .options({ maxPages: Infinity }); | ||
|
|
||
| try { | ||
| const result = await query.send(); | ||
| const items = result.Items ?? []; | ||
|
|
||
| await Promise.all( | ||
| items.map((item) => | ||
| this.entity.build(DeleteItemCommand).key({ PK: item.PK, SK: item.SK }).send(), | ||
| ), | ||
| ); |
| const query = this.stripeConfigEntity.table | ||
| .build(QueryCommand) | ||
| .entities(this.stripeConfigEntity) | ||
| .query({ | ||
| range: { | ||
| beginsWith: DynamoDbStripeConfig.accessPattern.getSKforAllItems(), | ||
| }, | ||
| partition: DynamoDbStripeConfig.accessPattern.getPK(access), | ||
| }) | ||
| .options({ maxPages: Infinity }); | ||
|
|
||
| try { | ||
| const result = await query.send(); | ||
| const items = result.Items ?? []; | ||
|
|
||
| await Promise.all( | ||
| items.map((item) => | ||
| this.stripeConfigEntity.build(DeleteItemCommand).key({ PK: item.PK, SK: item.SK }).send(), | ||
| ), | ||
| ); |
| const query = this.channelConfigMappingEntity.table | ||
| .build(QueryCommand) | ||
| .entities(this.channelConfigMappingEntity) | ||
| .query({ | ||
| range: { | ||
| beginsWith: DynamoDbChannelConfigMapping.accessPattern.getSKforAllChannels(), | ||
| }, | ||
| partition: DynamoDbChannelConfigMapping.accessPattern.getPK(access), | ||
| }) | ||
| .options({ maxPages: Infinity }); | ||
|
|
||
| try { | ||
| const result = await query.send(); | ||
| const items = result.Items ?? []; | ||
|
|
||
| await Promise.all( | ||
| items.map((item) => | ||
| this.channelConfigMappingEntity | ||
| .build(DeleteItemCommand) | ||
| .key({ PK: item.PK, SK: item.SK }) | ||
| .send(), | ||
| ), | ||
| ); |
| async execute({ saleorApiUrl, appId }: ExecuteArgs): Promise<void> { | ||
| const access = { saleorApiUrl, appId }; | ||
|
|
||
| this.logger.info("Wiping app data for uninstall", { saleorApiUrl, appId }); | ||
|
|
||
| const configsResult = await this.appConfigRepo.getAllConfigs(access); | ||
|
|
||
| const configs = configsResult.isOk() ? configsResult.value : []; | ||
|
|
||
| if (configsResult.isErr()) { | ||
| this.logger.error("Failed to fetch configs; Stripe webhooks will not be removed", { | ||
| error: configsResult.error, | ||
| saleorApiUrl, | ||
| appId, | ||
| }); | ||
| } | ||
|
|
||
| this.logger.info("Fetched configs for uninstall cleanup", { | ||
| saleorApiUrl, | ||
| appId, | ||
| configCount: configs.length, | ||
| }); | ||
|
|
||
| const [stripeWebhookOutcomes, removeConfigsResult, removeMappingsResult, removeTxResult] = | ||
| await Promise.all([ | ||
| this.removeStripeWebhooks(configs), | ||
| this.appConfigRepo.removeAllConfigs(access), | ||
| this.appConfigRepo.removeAllChannelMappings(access), | ||
| this.transactionRecorderRepo.removeAllForApp(access), | ||
| ]); | ||
|
|
| const results = await Promise.allSettled( | ||
| configs.map(async (config) => { | ||
| if (!config.webhookId) { | ||
| this.logger.warn("StripeConfig missing webhookId; skipping Stripe webhook removal", { | ||
| configId: config.id, | ||
| }); | ||
| skipped += 1; | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const result = await this.stripeWebhookManager.removeWebhook({ | ||
| webhookId: config.webhookId, | ||
| restrictedKey: config.restrictedKey, | ||
| }); | ||
|
|
||
| if (result.isErr()) { | ||
| this.logger.error("Failed to remove Stripe webhook", { | ||
| configId: config.id, | ||
| stripeWhId: config.webhookId, | ||
| error: result.error, | ||
| }); | ||
| failed += 1; | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| this.logger.info("Removed Stripe webhook", { | ||
| configId: config.id, | ||
| stripeWhId: config.webhookId, | ||
| }); | ||
| deleted += 1; | ||
| }), | ||
| ); |
| "saleor-app-stripe": minor | ||
| --- | ||
|
|
||
| Implemented APP_DELETED handler. On Saleor 3.23+, when the app is uninstalled, it now removes its DynamoDB data (Stripe configurations, channel-to-config mappings, recorded transactions) and best-effort deletes the webhooks it had registered on the Stripe side, in addition to pruning APL data. |