Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
692 commits
Select commit Hold shift + click to select a range
3e5af5d
improve bounty logic UI
steven-tey Aug 26, 2026
d0b56e0
Merge branch 'main' into commissions-metadata-filters-v2
steven-tey Aug 26, 2026
94b4607
Merge remote-tracking branch 'upstream/main' into feature/partner-ful…
Joker666 Aug 26, 2026
fe2d1be
Merge branch 'feature/partner-full-text-search' into feature/partner-…
Joker666 Aug 26, 2026
ffbdda0
Merge branch 'feature/partner-full-text-search-rollout' into feature/…
Joker666 Aug 26, 2026
28a9fa0
update footer
steven-tey Aug 26, 2026
72f54b3
Merge branch 'feature/partner-full-text-search-sync' into fix/search-…
Joker666 Aug 26, 2026
9ecf0a1
Merge branch 'main' into new-tracking-page
pepeladeira Aug 26, 2026
8f85b67
Show LTV (`saleAmount`) to partners
steven-tey Aug 26, 2026
da978fa
Correct candidate limit comment: ungrouped count is exact
Joker666 Aug 26, 2026
71d1074
Merge pull request #4400 from dubinc/show-ltv-partner
steven-tey Aug 26, 2026
08b6fae
fix gh login `next-auth`
steven-tey Aug 26, 2026
d5aa730
Move reconciliation sweep to a follow-up PR
Joker666 Aug 26, 2026
efc2314
Merge branch 'feature/partner-full-text-search-sync' into fix/search-…
Joker666 Aug 26, 2026
7720172
Merge branch 'feature/partner-full-text-search-rollout' into feature/…
Joker666 Aug 26, 2026
c0c5eb4
Merge branch 'feature/partner-full-text-search-sync' into fix/search-…
Joker666 Aug 26, 2026
5c0eb16
Trim search before the exact email and partner ID match
Joker666 Aug 26, 2026
e690965
rearrange columns + show LTV
steven-tey Aug 26, 2026
3059dd5
Queue the deactivation search sync off the request path
Joker666 Aug 26, 2026
39f87c9
Read retry status from the SDK error, not the message
Joker666 Aug 26, 2026
cefabb1
Merge branch 'feature/partner-full-text-search-rollout' into feature/…
Joker666 Aug 26, 2026
20ff62d
build tracking setup prompt
pepeladeira Aug 26, 2026
f12458b
fix tests, fix partner/customer redirect bug
steven-tey Aug 26, 2026
229083d
Merge branch 'main' into lemonsqueezy-importer
pepeladeira Aug 26, 2026
018b2a6
Merge pull request #4214 from dubinc/lemonsqueezy-importer
pepeladeira Aug 26, 2026
87494f4
Improve integration creation flow
steven-tey Aug 27, 2026
abaa998
Merge pull request #4404 from dubinc/improve-integration-flow
steven-tey Aug 27, 2026
cc46986
address CR feedback
steven-tey Aug 27, 2026
e82ad86
import tapfiliate customers from conversions
pepeladeira Aug 27, 2026
37353a6
fix: prevent horizontal overflow on long URLs in messages
cursoragent Aug 27, 2026
83c42b3
Merge branch 'main' into commissions-metadata-filters-v2
steven-tey Aug 27, 2026
8c375ed
code improvements
pepeladeira Aug 27, 2026
51948ec
Merge remote-tracking branch 'upstream/main' into feature/partner-ful…
Joker666 Aug 27, 2026
d146cec
Merge branch 'feature/partner-full-text-search' into feature/partner-…
Joker666 Aug 27, 2026
ef95988
Merge branch 'feature/partner-full-text-search-rollout' into feature/…
Joker666 Aug 27, 2026
a6d9937
fix tests
steven-tey Aug 27, 2026
92bcc3f
Merge pull request #4407 from dubinc/tapfiliate-conversion-customers
steven-tey Aug 27, 2026
de7d37e
Merge branch 'main' into commissions-metadata-filters-v2
steven-tey Aug 27, 2026
5733ae4
Queue a search sync for Lemon Squeezy partner imports
Joker666 Aug 27, 2026
3c0811a
Merge branch 'main' into stripe-first-time-order-setting
pepeladeira Aug 27, 2026
6fecd90
Trim design-justification comment from the index time budget
Joker666 Aug 27, 2026
50b5677
Trim provider switch comments
Joker666 Aug 27, 2026
ffad3f5
Trim call-site comment in complete-program-applications
Joker666 Aug 27, 2026
fdcd5be
Trim placement comments at two sync call sites
Joker666 Aug 27, 2026
faed125
Drop the link sync delay
Joker666 Aug 27, 2026
247973d
Trim two design-justification comments
Joker666 Aug 27, 2026
a732d51
harden flaky tests
steven-tey Aug 27, 2026
f552e84
Move the namespace strategy comment to the namespace constant
Joker666 Aug 27, 2026
7ce75e3
code improvements
pepeladeira Aug 27, 2026
dc21ec5
Merge pull request #4389 from dubinc/commissions-metadata-filters-v2
steven-tey Aug 27, 2026
680543c
code improvements
pepeladeira Aug 27, 2026
3f02691
Fix Google Ads integration
steven-tey Aug 27, 2026
c54ef00
Merge branch 'main' into stripe-first-time-order-setting
steven-tey Aug 27, 2026
6288080
code improvements
pepeladeira Aug 27, 2026
e4de098
Merge branch 'main' into new-tracking-page
pepeladeira Aug 27, 2026
6ff8723
code improvements
pepeladeira Aug 27, 2026
638da1b
main prompt view all
pepeladeira Aug 27, 2026
e1a9a25
harden flaky leadRecorded campaign workflow test
pepeladeira Aug 27, 2026
7a2e638
Merge pull request #4410 from dubinc/fix-flaky-leadrecorded-campaign-…
steven-tey Aug 27, 2026
2b579c2
Merge branch 'main' into fix-gad-integration
steven-tey Aug 27, 2026
a46b29a
address CR feedback
steven-tey Aug 27, 2026
5efb076
Merge branch 'main' into new-tracking-page
pepeladeira Aug 27, 2026
61bdf8b
Merge pull request #4409 from dubinc/fix-gad-integration
steven-tey Aug 27, 2026
c86f707
Add `eventName` filter in analytics
steven-tey Aug 27, 2026
03f2419
Merge pull request #4411 from dubinc/eventname-filter
steven-tey Aug 27, 2026
580d81c
Allow mapping event names for Google Ads settings
steven-tey Aug 27, 2026
fa90078
Merge pull request #4412 from dubinc/google-ads-events
steven-tey Aug 28, 2026
b9fb969
Merge branch 'main' into stripe-first-time-order-setting
steven-tey Aug 28, 2026
03541e5
Merge pull request #4397 from dubinc/stripe-first-time-order-setting
steven-tey Aug 28, 2026
3462d79
Update settings.tsx
steven-tey Aug 28, 2026
c44ce90
Merge branch 'main' into new-tracking-page
steven-tey Aug 28, 2026
cae9a69
copy change
steven-tey Aug 28, 2026
2931fdc
Group move rule bug fix
marcusljf Aug 28, 2026
bc6bdfd
code improvements
pepeladeira Aug 28, 2026
cfc91fd
Raise the search query deadline to 2s
Joker666 Aug 28, 2026
78ad14c
Merge branch 'feature/partner-full-text-search' into feature/partner-…
Joker666 Aug 28, 2026
e04e4e4
Merge branch 'feature/partner-full-text-search-rollout' into feature/…
Joker666 Aug 28, 2026
01bcc1d
Add `queueGoogleAdsConversionUpload` coverage to more call sites
steven-tey Aug 28, 2026
c361d6f
Merge pull request #4414 from dubinc/add-gad-coverage
steven-tey Aug 29, 2026
8de428a
account for isZeroDecimalCurrency
steven-tey Aug 29, 2026
cd857be
Merge pull request #4413 from dubinc/group-move-rules-bug
steven-tey Aug 29, 2026
36e0335
small tweaks
steven-tey Aug 30, 2026
736888d
reset draft submission fields
steven-tey Aug 30, 2026
1a8b9fc
change default `groupBy` to `type`
steven-tey Aug 30, 2026
e2beaaf
Update `redir_url` implementation to comply with Google click trackin…
steven-tey Aug 30, 2026
0322833
move logic into `LinkMiddleware` directly
steven-tey Aug 30, 2026
75e4f7b
address CR feedback
steven-tey Aug 30, 2026
6259bcd
safeguard redirectionUrl further
steven-tey Aug 30, 2026
53bdd3e
Merge pull request #4416 from dubinc/fix-redir-url
steven-tey Aug 30, 2026
aad8f71
Merge branch 'main' into new-tracking-page
steven-tey Aug 30, 2026
818d5f7
fix openapi specs
steven-tey Aug 30, 2026
46a63b1
Improve Google Ads integration settings
steven-tey Aug 30, 2026
8bf2b4b
Merge pull request #4417 from dubinc/improve-google-ads-settings
steven-tey Aug 30, 2026
22b93b8
Merge pull request #4343 from dubinc/feature/partner-full-text-search…
steven-tey Aug 30, 2026
e48ce07
Merge pull request #4333 from dubinc/feature/partner-full-text-search…
steven-tey Aug 30, 2026
3be373e
Merge branch 'feature/partner-full-text-search' of https://github.com…
steven-tey Aug 30, 2026
186f6fe
update comments
steven-tey Aug 30, 2026
8908b8e
Merge branch 'main' into feature/partner-full-text-search
steven-tey Aug 30, 2026
237f9ca
address CR feedback
steven-tey Aug 30, 2026
f38ec80
fix tests, bump to v4
steven-tey Aug 31, 2026
5958225
Merge pull request #4346 from dubinc/fix/search-box-debounce-and-prec…
steven-tey Aug 31, 2026
df27c97
Update turbopuffer-partner-search-provider.test.ts
steven-tey Aug 31, 2026
b94b5e2
Merge pull request #4316 from dubinc/feature/partner-full-text-search
steven-tey Aug 31, 2026
f216b94
update comments
steven-tey Aug 31, 2026
c2f8b52
Create trigger-failed-workflows.ts
devkiran Aug 31, 2026
5d16ce9
Route QStash workflows through the job queue for durable publish and …
devkiran Aug 31, 2026
63fd3eb
Default QStash deduplication to the job id so publish retries cannot …
devkiran Aug 31, 2026
66233f6
Delete published jobs older than 24 hours so the outbox does not grow…
devkiran Aug 31, 2026
52e4998
Remove unused workflow helpers and index Job for retry and cleanup.
devkiran Aug 31, 2026
f7a4233
Merge branch 'main' into improve-background-jobs
devkiran Aug 31, 2026
a38e47c
Delete trigger-failed-workflows.ts
devkiran Aug 31, 2026
18dae57
Persist workflows only when QStash publish fails, and drop the write-…
devkiran Aug 31, 2026
383f7b4
Split job publishing into shared outbox transports and drop Job status.
devkiran Aug 31, 2026
a733d53
Update send-jobs.ts
devkiran Aug 31, 2026
0e7a7a5
Tighten job helpers and fail unknown kinds on retry.
devkiran Aug 31, 2026
e718ba1
Name workflows kebab-case ending in -workflow so outbox routing is co…
devkiran Aug 31, 2026
f69b8ba
Share outbox persist so deferred jobs use the same retry accounting.
devkiran Aug 31, 2026
c9392fc
Merge pull request #4419 from dubinc/improve-background-jobs
steven-tey Aug 31, 2026
9ce0e54
Address CodeRabbit findings from the partner search PRs
Joker666 Aug 31, 2026
5b1e41b
Make pasted short links exact and stop indexing shared domains
Joker666 Aug 31, 2026
d73e878
Merge pull request #4399 from dubinc/improvement/partner-search-pagin…
Joker666 Aug 31, 2026
3eb49fc
remove unused index
steven-tey Aug 31, 2026
bd3f5e2
Exports skip the search field
Joker666 Aug 26, 2026
4f929a5
Merge branch 'main' into improvement/partner-search-export-completeness
steven-tey Aug 31, 2026
9ac6940
Exclude a relevance sort from export requests
Joker666 Aug 31, 2026
e22dd73
Merge pull request #4402 from dubinc/improvement/partner-search-expor…
Joker666 Aug 31, 2026
2922989
Merge branch 'main' into improvement/partner-search-link-indexing
Joker666 Aug 31, 2026
212f3ea
Index only link keys
Joker666 Aug 31, 2026
3607571
Merge branch 'main' into new-tracking-page
pepeladeira Aug 31, 2026
e951e96
Trim link field comment
Joker666 Aug 31, 2026
0262ed6
exclude the demo program
pepeladeira Aug 31, 2026
9ff33dc
Merge pull request #4420 from dubinc/improvement/partner-search-link-…
steven-tey Sep 1, 2026
b047add
Remove redundant `queuePartnerSearchSync`
steven-tey Sep 1, 2026
6f0117f
conditional sync for link operations
steven-tey Sep 1, 2026
951a636
Merge pull request #4423 from dubinc/remove-redundant-sync
steven-tey Sep 1, 2026
2a32f1f
Merge pull request #4422 from dubinc/demo-workspace
steven-tey Sep 1, 2026
ca645fc
Move welcome-user email onto defineJob
devkiran Sep 1, 2026
b0e236b
Handle `redisFailOver`
steven-tey Sep 1, 2026
464ba8c
only mint clickId if not in redisFailOver mode
steven-tey Sep 1, 2026
f0fdd4b
only run clickId logic if not in redisFailOver mode
steven-tey Sep 1, 2026
2bd7900
Move partner auto-approve onto defineJob.
devkiran Sep 1, 2026
13ec349
Merge pull request #4424 from dubinc/handle-redis-failover
steven-tey Sep 1, 2026
139bd4a
Merge pull request #4408 from dubinc/cursor/fix-message-url-overflow-…
steven-tey Sep 1, 2026
5bcf678
Move partner auto-reject onto defineJob.
devkiran Sep 1, 2026
32eebc5
Update auto-approve-partner-job.ts
devkiran Sep 1, 2026
c9c5020
Merge branch 'main' into jobs-1
devkiran Sep 1, 2026
bd00ed4
Merge branch 'main' into improve-shopify-integration
devkiran Sep 1, 2026
d574901
Track Shopify orders via click, existing lead, or discount code
devkiran Sep 1, 2026
a016876
Update checkout-cache.ts
devkiran Sep 1, 2026
a7f661a
Update process-order.ts
devkiran Sep 1, 2026
8236a27
Route Shopify checkout Redis access through a cache class.
devkiran Sep 1, 2026
d36351a
Prefer discount code attribution over click for new Shopify orders.
devkiran Sep 1, 2026
5ee7688
Generate fake click IDs inside recordFakeClick instead of callers.
devkiran Sep 1, 2026
36b940f
Restore Google Ads lead upload for Shopify discount-code attribution …
devkiran Sep 1, 2026
a12b6cb
simplify the test
devkiran Sep 1, 2026
28105fb
Update checkout-cache.ts
devkiran Sep 1, 2026
4844959
Make Shopify conversion sim wait for pixel, log responses, and suppor…
devkiran Sep 1, 2026
26f9511
Add Playwright coverage for Shopify orders/paid attribution.
devkiran Sep 1, 2026
fda18f5
Update route.ts
devkiran Sep 1, 2026
509d06d
Merge branch 'main' into new-tracking-page
pepeladeira Sep 1, 2026
aceb05a
Await Shopify order job logs so they persist before the worker exits.
devkiran Sep 1, 2026
0d14be0
Merge pull request #4426 from dubinc/jobs-1
steven-tey Sep 1, 2026
f2aacda
Merge branch 'main' into improve-shopify-integration
steven-tey Sep 1, 2026
861a31e
revalidate public partner pages
pepeladeira Sep 1, 2026
b478db4
fix missing install guides
pepeladeira Sep 1, 2026
d9ed475
only feature integrations with category
steven-tey Sep 1, 2026
effbbb0
fix ai reward builder crash when replacing a reward that already has …
pepeladeira Sep 1, 2026
d4caea2
Merge pull request #4430 from dubinc/ai-reward-condition-crash
steven-tey Sep 1, 2026
188a3d6
pass crypto wallet payout method id
pepeladeira Sep 1, 2026
a780a77
Show raw indexed text in the search debug script
Joker666 Sep 1, 2026
5add3f5
Merge branch 'main' into fix/partner-search-debug-raw
Joker666 Sep 1, 2026
7f58786
Strip the program domain from pasted short links before searching
Joker666 Sep 1, 2026
dc435cf
Cover the list, count and missing-program paths for pasted links
Joker666 Sep 1, 2026
e122c2a
Merge pull request #4431 from dubinc/stablecoin-payout-method
steven-tey Sep 2, 2026
9decf5a
Improve `fundFinancialAccount` for `forceWithdrawal`
steven-tey Sep 2, 2026
efd144d
Merge pull request #4434 from dubinc/improve-fund-fa
steven-tey Sep 2, 2026
2022127
address CR feedback
steven-tey Sep 2, 2026
9c39f66
Merge branch 'main' into improve-shopify-integration
devkiran Sep 2, 2026
341e05e
Merge pull request #4429 from dubinc/revalidate-partner-public-pages
steven-tey Sep 2, 2026
37d0a4c
Merge pull request #4394 from dubinc/new-tracking-page
steven-tey Sep 2, 2026
6174527
Merge pull request #4427 from dubinc/improve-shopify-integration
steven-tey Sep 2, 2026
cdfeaea
Add Shopify pixel, webhook, and order dispatch logs for debugging.
devkiran Sep 2, 2026
9665cfe
Update checkout-cache.ts
devkiran Sep 2, 2026
5506df6
Update route.ts
devkiran Sep 2, 2026
735dcc1
Update route.ts
devkiran Sep 2, 2026
dc75f05
Update route.ts
devkiran Sep 2, 2026
768c93f
Merge pull request #4435 from dubinc/shopify-logs-webhook
devkiran Sep 2, 2026
42af78e
Skip Intercom webhook API logs when no partner is found.
devkiran Sep 2, 2026
0d8c84e
Merge pull request #4436 from dubinc/improve-intercom-integration
steven-tey Sep 2, 2026
a655c92
Show an error when magic-link login fails instead of a success toast.
devkiran Sep 2, 2026
a1363da
revalidate public partner pages with after()
pepeladeira Sep 2, 2026
007fe63
Merge pull request #4440 from dubinc/fix/revalidate-program-pages-after
steven-tey Sep 2, 2026
51211bd
Merge pull request #4439 from dubinc/fix-magic-link-rate-limit-toast
steven-tey Sep 2, 2026
28888ee
Update customer-subscription-deleted.ts
steven-tey Sep 2, 2026
4a9d33a
Merge pull request #4428 from dubinc/fix/partner-search-debug-raw
steven-tey Sep 2, 2026
8f2c3dc
Merge pull request #4433 from dubinc/improvement/partner-search-strip…
steven-tey Sep 2, 2026
13cd490
persist country on customer PATCH
pepeladeira Sep 2, 2026
83993a7
test improvements
pepeladeira Sep 2, 2026
24b744d
Merge pull request #4441 from dubinc/update-customer-country
pepeladeira Sep 2, 2026
c53cd23
allow creating sale commissions by discount code
pepeladeira Sep 2, 2026
8ad98a0
code improvements
pepeladeira Sep 2, 2026
1eba8c1
Improvements to tracking page
steven-tey Sep 2, 2026
c06aeaa
Update apps/web/lib/zod/schemas/commissions.ts
steven-tey Sep 2, 2026
b0b8673
Merge pull request #4443 from dubinc/improve-tracking-page
steven-tey Sep 2, 2026
a717d9e
record fake click for demo workspace
pepeladeira Sep 2, 2026
6255a55
update record-fake-click
pepeladeira Sep 2, 2026
d9fa0c8
Merge pull request #4442 from dubinc/create-commission-discount-code
steven-tey Sep 2, 2026
6fb40dd
Update partners-redirect.ts
steven-tey Sep 2, 2026
4b23d56
add workspace_id, domain, key and country
pepeladeira Sep 2, 2026
4989f63
fix cron/domains/update
steven-tey Sep 2, 2026
fda6545
Address security advisory (#4445)
steven-tey Sep 3, 2026
d3d4b5e
Update create-manual-commissions.ts
steven-tey Sep 3, 2026
b9f4edd
Fix squashed browser icons in Events table (#4446)
steven-tey Sep 3, 2026
c3b5d49
Dynamic UTM parameters for partner links (#4245)
devkiran Sep 3, 2026
07311f5
Attribute Shopify orders from note_attributes dubClickId without wait…
devkiran Sep 3, 2026
c997902
Always return 200 from Shopify pixel route and validate input with zod
devkiran Sep 3, 2026
0016d16
Make the clickId belongs the same workspace
devkiran Sep 3, 2026
c0c0487
Skip if the clickEvent not found
devkiran Sep 3, 2026
91ffbaa
Return 200 on Shopify pixel errors and update tests for skip behavior
devkiran Sep 3, 2026
47f36e5
Merge branch 'main' into demo-fake-click
pepeladeira Sep 3, 2026
353ec92
demo commission
pepeladeira Sep 3, 2026
1b503ee
Merge pull request #4444 from dubinc/demo-fake-click
pepeladeira Sep 3, 2026
0b25092
Fix marketplace application form (#4448)
pepeladeira Sep 3, 2026
36e96c6
Avoid ECR Public rate limits when starting QStash in Playwright CI. (…
devkiran Sep 3, 2026
d245674
Add Axiom tracking to critical webhooks and payout crons (#4325)
pepeladeira Sep 3, 2026
7de765e
Format AI payout amounts as USD instead of cents (#4384)
pepeladeira Sep 3, 2026
6884316
fix: use constant-time comparison for Shopify and HubSpot webhook sig…
devkiran Sep 3, 2026
977eab4
Add external program marketplace (#4053)
pepeladeira Sep 4, 2026
d876907
Adding the marketplace to the nav (#4347)
marcusljf Sep 4, 2026
ba521ad
final touches
steven-tey Sep 4, 2026
88adba2
fix TS error
steven-tey Sep 4, 2026
2f102db
revalidateExternalMarketplacePages
steven-tey Sep 4, 2026
e56674d
rearrange DubAnalytics script
steven-tey Sep 4, 2026
899aa95
fix carousel layout shift
steven-tey Sep 4, 2026
d49bf1e
Fix `determinePartnerRewards` logic (#4453)
steven-tey Sep 4, 2026
a89a847
Merge branch 'main' into shopify-updates
devkiran Sep 4, 2026
73415cf
Merge pull request #4447 from dubinc/shopify-updates
devkiran Sep 4, 2026
d6d4c0e
Move reward tests to Playwright (#4454)
devkiran Sep 4, 2026
c6635ba
tweak title
steven-tey Sep 4, 2026
cae764a
Move partner group workflow tests to Playwright (#4457)
devkiran Sep 4, 2026
a3f695b
Replace bounty submission approve/reject server actions with APIs (#4…
devkiran Sep 4, 2026
2882798
Skip additionalLinks URL validation for workspace partner (#4461)
pepeladeira Sep 4, 2026
5d11a08
Skip invalid Stripe payout amounts on balance.available (#4458)
pepeladeira Sep 4, 2026
00dc858
payoutable → payable
steven-tey Sep 4, 2026
382882a
Adding holding period selection to payout settings (#4452)
marcusljf Sep 4, 2026
ff75ab3
add Suspense, fix layout shift
steven-tey Sep 4, 2026
478a5e1
improve admin
steven-tey Sep 5, 2026
64dc8a9
improve design
steven-tey Sep 5, 2026
4b40951
Update user-info.tsx
steven-tey Sep 5, 2026
4d66f25
Merge remote-tracking branch 'upstream/main' into automation/sync-ups…
github-actions[bot] Sep 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
130 changes: 130 additions & 0 deletions .agents/skills/jobs-define-job/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
---
name: jobs-define-job
description: Add or migrate background jobs with defineJob, register them in jobLoaders, and dispatch via job.dispatch/dispatchBatch instead of qstash.publishJSON to a cron URL. Use when adding a QStash worker, converting a POST /api/cron route into a job, or when the user mentions defineJob, job handlers, /api/jobs/process, or background jobs.
---

# Background jobs: use defineJob

Payload-driven QStash work goes through `defineJob`. Do **not** add a new HTTP route under `/api/jobs` — every job is executed by the existing [`apps/web/app/api/jobs/process/[jobName]/route.ts`](apps/web/app/api/jobs/process/[jobName]/route.ts).

Keep using `withCron` for Vercel GET schedules and for cron scanners that have no job envelope. See the `cron-use-with-cron` skill for those.

## File layout

```
apps/web/lib/jobs/
├── index.ts # defineJob — do not edit unless changing the framework
├── registry.ts # jobLoaders — register every new job here
├── send-jobs.ts # envelope + QStash request builder
└── handlers/
└── {name}-job.ts # one file per job
```

## 1. Create the handler

Add `apps/web/lib/jobs/handlers/{name}-job.ts`. The `name` must be kebab-case and end in `-job` (enforced by `jobNameSchema`: `/^[a-z][a-z0-9]*(-[a-z0-9]+)*-job$/`).

```ts
import * as z from "zod/v4";
import { defineJob } from "../index";

const inputSchema = z.object({
programId: z.string(),
partnerId: z.string(),
});

export const unbanPartnerJob = defineJob({
name: "unban-partner-job",
schema: inputSchema,
defaults: {
retries: 3, // optional; QStash retries on 5xx
// queue: "unban-partner", // optional; named QStash queue
// flowControl: { key: "unban-partner", parallelism: 20 },
},
async handle(input) {
// skip (permanent / not found) → return (process route returns 2xx, QStash does not retry)
// transient failure → throw (process route returns 500, QStash retries)
},
});
```

Export the const as camelCase `{name}Job` matching the kebab `name`.

Reference handlers:

- Simple skip/work: `unban-partner-job.ts`, `create-tremendous-campaign-job.ts`
- Self-pagination: `folder-deleted-job.ts`, `domain-deleted-job.ts`, `partner-search-sync-job.ts`
- `defaults.flowControl`: `partner-search-sync-job.ts`

## 2. Register it

Add a **static** `import()` in [`apps/web/lib/jobs/registry.ts`](apps/web/lib/jobs/registry.ts) `jobLoaders`. The object key must equal `defineJob({ name })`. Webpack code-splits each handler.

```ts
"unban-partner-job": () =>
import("./handlers/unban-partner-job").then((m) => m.unbanPartnerJob),
```

Do not register by editing the process route. `loadJob` throws if `job.name !==` the registry key.

## 3. Dispatch from call sites

Import the handler (not `qstash`) and call `dispatch` / `dispatchBatch`:

```ts
import { unbanPartnerJob } from "@/lib/jobs/handlers/unban-partner-job";

await unbanPartnerJob.dispatch(
{ workspaceId, programId, partnerId },
{ label: partnerId },
);

await folderDeletedJob.dispatchBatch(
folderIds.map((folderId) => ({ folderId })),
({ folderId }) => ({ label: folderId }),
);
```

Per-dispatch options merge over `defaults`: `delay`, `notBefore`, `deduplicationId`, `retries`, `queue`, `flowControl`, `label`.

Failed QStash publish is persisted to the jobs outbox automatically — do not catch-and-swallow unless even the outbox persist failing must not fail the source mutation (see `queue-partner-search-sync.ts`).

Self-pagination: call `theJob.dispatch(nextPayload, { delay: 1 })` from `handle` (see `folder-deleted-job`, `partner-search-sync-job`).

Do not call `job.execute` from app code. That is only for the process route (and cron drain shims below).

## 4. Convert an existing cron worker

1. Move the `withCron` body into `handle`. Replace `logAndRespond("skip…")` with `console.info` / `console.error` + `return`.
2. Register + switch every `qstash.publishJSON` / `enqueueJSON` / `enqueueBatchJobs` targeting that cron URL to `job.dispatch` / `dispatchBatch`.
3. Keep a thin POST shim at the old `/api/cron/...` URL that parses the **old** body (not the job envelope) and calls `job.execute(payload)`. That drains in-flight QStash messages. Do not add a shim for brand-new jobs.
4. Delete cron-only helpers that moved with the handler.

## Handle semantics

| Outcome | What to do | HTTP from process route | QStash |
| --- | --- | --- | --- |
| Work done | `return` | 200 | stop |
| Skip (not found, already done, env not configured) | `console.*` + `return` | 200 | stop |
| Bad payload | throw `ZodError` (schema.parse) | 200 | stop (non-retryable) |
| Transient failure | `throw` | 500 | retry |

Unknown job names and invalid envelopes also return 2xx so QStash does not retry forever.

## Do not use defineJob for

- Vercel GET crons in `apps/web/vercel.json`
- Scanners that only fan out work (the worker they enqueue can be a job)
- Importers that republish continuation state to the same URL
- `/api/cron/queue/retry` (job replay infrastructure)
- Path-param identity (`/api/cron/links/[linkId]/…`) unless the id moves into the payload
- Outbound webhook forwarding and postbacks

## Do not

- Add a new `/api/jobs/...` route or a new `/api/cron/...` POST worker for payload-driven work.
- Call `qstash.publishJSON` / `enqueueJSON` / `enqueueBatchJobs` with `/api/jobs/process/...` — use `dispatch`.
- Register jobs with a dynamic `import()` that webpack cannot statically analyze, or a key that differs from `defineJob({ name })`.
- Name a job without the `-job` suffix.
- Use `job.execute` at dispatch call sites.
- Run `pnpm build` after adding a job.
6 changes: 1 addition & 5 deletions .agents/skills/playwright-api-tests/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,6 @@ import { expect } from "@playwright/test";
import { randomName } from "../../utils";
import { test, type ApiClient } from "../fixtures";

test.describe.configure({
mode: "parallel",
});

async function createThing(
api: ApiClient,
overrides: Record<string, unknown> = {},
Expand Down Expand Up @@ -98,7 +94,7 @@ test("POST /things", async ({ api }) => {
| Rule | Detail |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| Import `test` from `../fixtures` | Provides `api`, `workspace`, and `program` |
| `test.describe.configure({ mode: "parallel" })` | At top of every API spec file |
| Serial only when tests share state | API project is `fullyParallel: true`. Do not add `mode: "parallel"`. Use `test.describe.configure({ mode: "serial" })` only when tests in a file/describe share state (e.g. domains, seeded pagination) |
| Cleanup in `finally` | Create → assert → always delete created rows |
| Unique names/ids | Use `randomName` / `randomCustomer` / `randomPartnerEmail` from `../../utils` — never fixed colliding names |
| Assert status + body | Prefer `toStrictEqual` / `toEqual` on full shapes; use `expect.any(String)` for ids/timestamps |
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,5 @@ packages/stripe-app/.build/*
playwright-report/
**/playwright/.auth/
test-results/
blob-report/
blob-report/
.pnpm-store
17 changes: 15 additions & 2 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@
# Generate secrets with: node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
NEXTAUTH_SECRET=
NEXTAUTH_URL=http://localhost:8888 # (only needed for localhost)
# Secret for Vercel cron jobs (https://vercel.com/docs/cron-jobs/manage-cron-jobs#securing-cron-jobs)

# Secret for Vercel cron jobs + sync-embeddings
CRON_SECRET=
# Shared with the LoopWork demo. Mints geo-accurate clicks via POST /api/demo/click
# and backdated commissions via POST /api/demo/commission
DEMO_CLICK_SECRET=
# Encryption key (AES-256-GCM) for encrypting sensitive data in the database
ENCRYPTION_KEY=
# Email unsubscribe token secret (optional, falls back to NEXTAUTH_SECRET)
Expand All @@ -23,6 +27,10 @@ PLANETSCALE_DATABASE_URL="http://root:unused@localhost:3900/planetscale"
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=

# Full-text partner search. Unset falls back to the database search path.
TURBOPUFFER_API_KEY=
PARTNER_SEARCH_READ_ENABLED=

# Upstash QStash – required for queues and background jobs
# Get your QStash Token here: https://upstash.com/docs/qstash/overall/getstarted
QSTASH_URL="https://qstash-us-east-1.upstash.io"
Expand Down Expand Up @@ -195,4 +203,9 @@ E2E_PARTNER_PASSWORD=

# Veriff (Identity Verification)
VERIFF_API_KEY=
VERIFF_SHARED_SECRET=
VERIFF_SHARED_SECRET=

# Domain Connect (auto-configure DNS for custom domains)
# Generate key: openssl genrsa -out private.pem 2048
# Get public key: openssl rsa -in private.pem -pubout -outform DER | base64 | tr -d '\n'
DOMAIN_CONNECT_PRIVATE_KEY=
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function ImpersonateUser() {
await fetch("/api/admin/impersonate", {
method: "POST",
body: JSON.stringify({
email: formData.get("email"),
query: formData.get("query"),
}),
}).then(async (res) => {
if (res.ok) {
Expand Down Expand Up @@ -89,9 +89,9 @@ const Form = () => {
return (
<div className="relative flex w-full rounded-md shadow-sm">
<input
name="email"
id="email"
type="email"
name="query"
id="query"
type="text"
required
disabled={pending}
autoComplete="off"
Expand All @@ -100,16 +100,14 @@ const Form = () => {
pending && "bg-neutral-100",
)}
onPaste={(e: React.ClipboardEvent<HTMLInputElement>) => {
// remove mailto: on paste
e.preventDefault();
const text = e.clipboardData.getData("text/plain");
if (text.startsWith("mailto:")) {
e.currentTarget.value = text.replace("mailto:", "");
} else {
e.currentTarget.value = text;
let text = e.clipboardData.getData("text/plain").trim();
if (text.toLowerCase().startsWith("mailto:")) {
text = text.slice(7);
}
e.currentTarget.value = text;
}}
placeholder="panic@thedis.co"
placeholder="panic@thedis.co, acme, or acme.com"
aria-invalid="true"
/>
{pending && (
Expand Down

This file was deleted.

Loading
Loading