Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
1015 commits
Select commit Hold shift + click to select a range
f7ba443
code improvements
pepeladeira Sep 11, 2026
62a5051
Removing the extra "is"
marcusljf Sep 11, 2026
2d72190
simplify
steven-tey Sep 12, 2026
75ae766
Add disabledAt column
steven-tey Sep 12, 2026
b547f00
Merge pull request #4499 from dubinc/disabledat
steven-tey Sep 12, 2026
0d2703d
add custom billing portal
steven-tey Sep 12, 2026
859c552
show badge
steven-tey Sep 12, 2026
b5d994e
Update route.ts
steven-tey Sep 12, 2026
0d9d6d2
Merge pull request #4498 from dubinc/bounty-fix
steven-tey Sep 12, 2026
1e5dd4f
Add `partner.usersLimit` field
steven-tey Sep 12, 2026
771a4cc
Merge pull request #4500 from dubinc/partner-users-limit
steven-tey Sep 13, 2026
5439c57
Add `DOMAIN_REDIRECTS`
steven-tey Sep 13, 2026
3491ae5
Merge pull request #4501 from dubinc/domain-redirects
steven-tey Sep 13, 2026
b12244c
fix programs/recent + add emails
steven-tey Sep 14, 2026
537247d
Merge branch 'main' into cursor/assert-rate-limit-migration-1b05
devkiran Sep 14, 2026
d6291ee
Merge branch 'main' into add-commission-source
devkiran Sep 14, 2026
2e36408
update EMAIL_TEMPLATES_MAP
steven-tey Sep 14, 2026
1342037
Return payout cron errors instead of throwing
devkiran Sep 14, 2026
770624d
Merge pull request #4503 from dubinc/payouts-cron-error-handling
steven-tey Sep 14, 2026
09cfab6
update email template
steven-tey Sep 14, 2026
8bc8068
add SpecialNewsContent
steven-tey Sep 14, 2026
392bc12
resolve original sale reward maxDuration from modifiers
pepeladeira Sep 14, 2026
5abfeca
test improvements
pepeladeira Sep 14, 2026
9808989
test improvements
pepeladeira Sep 14, 2026
ae69680
Merge pull request #4504 from dubinc/original-reward-max-duration-mod…
steven-tey Sep 15, 2026
962af1b
Merge branch 'main' into reattribute-customer
steven-tey Sep 15, 2026
228c5f2
improve partner link dropdown
steven-tey Sep 15, 2026
14dc383
Persist commission descriptions from applied reward and conditions
devkiran Sep 15, 2026
e6fceb0
Show commission description on imported activity events
devkiran Sep 15, 2026
f3d01c9
Show zero condition values and 1-month commission durations.
devkiran Sep 15, 2026
1fd1d85
Expect generated commission descriptions in Playwright API tests.
devkiran Sep 15, 2026
e9e8ad6
Allow OAuth installs with the subset of scopes the user's role can gr…
devkiran Sep 15, 2026
30f58a4
Show which OAuth scopes a workspace role can grant on the consent scr…
devkiran Sep 15, 2026
972a008
Merge pull request #4505 from dubinc/commission-description-snapshot
steven-tey Sep 15, 2026
ca606a7
Merge pull request #4493 from dubinc/cursor/assert-rate-limit-migrati…
steven-tey Sep 15, 2026
0a7ae72
Merge branch 'main' into reattribute-customer
steven-tey Sep 15, 2026
a4803c0
add CSV export for partner program customers
pepeladeira Sep 15, 2026
1ef671f
Merge pull request #4506 from dubinc/oauth-grant-available-scopes
steven-tey Sep 15, 2026
65c7176
Merge branch 'main' into reattribute-customer
steven-tey Sep 15, 2026
9c51815
CUSTOMER_REATTRIBUTION_EVENTS_LIMIT
steven-tey Sep 15, 2026
24c3498
code improvements
pepeladeira Sep 15, 2026
83af7e4
Merge branch 'main' into partner-customers-csv-export
pepeladeira Sep 15, 2026
b8c95be
code improvements
pepeladeira Sep 15, 2026
9bcdff0
improve UX, simplify CustomerDetailsColumn
steven-tey Sep 15, 2026
7f999a6
rich text editor to partner lander
pepeladeira Sep 15, 2026
e3bc4fd
Update reattribute-customer.ts
steven-tey Sep 15, 2026
b1075a1
fix tests
steven-tey Sep 15, 2026
117fa66
Merge pull request #4496 from dubinc/reattribute-customer
steven-tey Sep 16, 2026
c88c19a
Update partners-redirect.ts
steven-tey Sep 16, 2026
5a4cf19
small changes
steven-tey Sep 16, 2026
7a7beca
prioritize bottomContent over newsContent
steven-tey Sep 16, 2026
0439789
add chatgpt.site to chatg.pt domains
steven-tey Sep 16, 2026
e7a95c6
update modal width
pepeladeira Sep 16, 2026
2b44a88
Merge branch 'main' into lander-rich-text-editor
pepeladeira Sep 16, 2026
929968c
disable payout reminder for migrating programs
steven-tey Sep 16, 2026
4fbdbfd
show one marketplace reward with a count badge
pepeladeira Sep 16, 2026
b95f68a
program hero logo
pepeladeira Sep 16, 2026
00ba6c4
Merge branch 'main' into lander-rich-text-editor
pepeladeira Sep 16, 2026
10f8a18
Merge pull request #4507 from dubinc/partner-customers-csv-export
steven-tey Sep 16, 2026
32e0601
code improvements
pepeladeira Sep 16, 2026
a0b1702
Merge branch 'main' into lander-rich-text-editor
pepeladeira Sep 16, 2026
503e1fc
code improvements
pepeladeira Sep 16, 2026
940f991
fix tests
pepeladeira Sep 16, 2026
89c241d
Merge branch 'main' into marketplace-card-rewards
pepeladeira Sep 16, 2026
5208749
ui improvements
pepeladeira Sep 16, 2026
f713cb6
ui improvements
pepeladeira Sep 16, 2026
8a6c087
Merge pull request #4509 from dubinc/marketplace-card-rewards
steven-tey Sep 16, 2026
24c45d7
Merge pull request #4508 from dubinc/lander-rich-text-editor
steven-tey Sep 17, 2026
9641172
Merge branch 'main' into add-commission-source
steven-tey Sep 17, 2026
68f5d18
CommissionSource.submitted → CommissionSource.user
steven-tey Sep 17, 2026
d03edf6
Merge pull request #4465 from dubinc/add-commission-source
steven-tey Sep 17, 2026
7d4b98f
fix payoutsToNotify logic
steven-tey Sep 17, 2026
82767f9
fix canonicalUrl
steven-tey Sep 17, 2026
64f6017
Scope rewards and discounts to groups so additional ones can be liste…
devkiran Sep 17, 2026
1efe475
Drop unused reward/discount endpoints and support additional group di…
devkiran Sep 17, 2026
525496e
Allow additional rewards and discounts to be assigned per partner.
devkiran Sep 17, 2026
40dfc83
Record partner reward overrides in the same transaction and restore g…
devkiran Sep 17, 2026
993e039
Show partner-level reward and discount overrides on partner info cards.
devkiran Sep 17, 2026
dcad3d8
List additional group rewards and discounts and skip emails when none…
devkiran Sep 17, 2026
98efb97
Add modals to edit partner-level reward and discount overrides.
devkiran Sep 17, 2026
e563254
Filter assigned partners and infer default vs additional rewards.
devkiran Sep 17, 2026
12d119e
Don't delete group defaults from partner dialogs and remap additional…
devkiran Sep 17, 2026
5747923
Update route.ts
devkiran Sep 17, 2026
4c40566
Pass SWR options as the second argument to useRewards.
devkiran Sep 17, 2026
1b17f24
Pass SWR options as the second argument to useDiscounts.
devkiran Sep 17, 2026
9cf24cf
Update reward-discount-partners-card.tsx
devkiran Sep 17, 2026
3e1f679
Keep group defaults visible while partner reward lists load.
devkiran Sep 17, 2026
d5f49e3
hover controls for lander table rows and columns
pepeladeira Sep 17, 2026
1d7bbf0
code improvements
pepeladeira Sep 17, 2026
4bee40c
table ui improvements
pepeladeira Sep 17, 2026
db8b236
Merge pull request #4515 from dubinc/lander-table-hover-controls
steven-tey Sep 17, 2026
5077ba4
Merge branch 'main' into partner-level-rewards
steven-tey Sep 17, 2026
809b3b1
fix import crash when customer.id is null
pepeladeira Sep 17, 2026
354bc9e
Merge pull request #4517 from dubinc/fix-null-external-id-in
steven-tey Sep 17, 2026
a6bc577
Update delete-program.ts
steven-tey Sep 17, 2026
77310db
Merge branch 'main' into partner-level-rewards
steven-tey Sep 17, 2026
eb34f6c
show partner.rewardChanged/discountChanged in history sheet
steven-tey Sep 17, 2026
2935380
Navigation hover content
marcusljf Sep 17, 2026
834cb7a
fix ts error
steven-tey Sep 17, 2026
bbefb4d
Merge pull request #4514 from dubinc/partner-level-rewards
steven-tey Sep 18, 2026
abd7930
Fill Reward.groupId in the local seed script
cursoragent Sep 18, 2026
a825db4
Merge pull request #4519 from dubinc/cursor/seed-reward-groupid-7352
steven-tey Sep 18, 2026
a7de49e
Merge pull request #4518 from dubinc/navigation-product-hover
steven-tey Sep 18, 2026
e641f6b
Add `rewardSpendLimit` flag
steven-tey Sep 18, 2026
2a171fa
Merge pull request #4523 from dubinc/spendlimit-flag
steven-tey Sep 18, 2026
d7f452e
Make the columns sort function more visible. (#4526)
marcusljf Sep 18, 2026
b8c23a3
Workspace and partners auth page updates (#4524)
marcusljf Sep 19, 2026
66223ae
Mobile analytics drawer width fix (#4527)
marcusljf Sep 19, 2026
70350f7
fix category
steven-tey Sep 19, 2026
a8db3fd
Add `typesafe/jev` link scanner (#4528)
steven-tey Sep 20, 2026
d30b07d
Link level rewards (#4516)
devkiran Sep 21, 2026
2e08e51
Soft-delete discounts and remap codes via cleanup workflow
devkiran Sep 21, 2026
bc81d5e
Persist selected group reward and discount ids on links (#4532)
devkiran Sep 21, 2026
af31dd0
Fix infinite loading on applications Filters (#4535)
pepeladeira Sep 21, 2026
54af543
move Submitted Leads to its own page
steven-tey Sep 21, 2026
4e8ff9c
Prevent empty-scope API tokens from inheriting full role permissions.…
devkiran Sep 21, 2026
bd69439
Pin Content-Type and size on signed R2 uploads. (#4482)
devkiran Sep 21, 2026
6cbaa0e
Inline emoji picker (#4536)
pepeladeira Sep 22, 2026
4ac21be
Link level ui updates (#4537)
marcusljf Sep 22, 2026
d306205
Remove queue default from aggregate-clicks job so flow control can pu…
devkiran Sep 22, 2026
2b4ace1
Merge pull request #4538 from dubinc/fix/aggregate-clicks-queue-flow-…
devkiran Sep 22, 2026
f40750c
look up customers by id in the commission selector
pepeladeira Sep 22, 2026
b3893c1
send the PartnerStack import email when Stripe is not connected
pepeladeira Sep 22, 2026
4546df5
Merge branch 'main' into soft-delete-discount-cleanup
devkiran Sep 22, 2026
2bf87f5
Merge pull request #4542 from dubinc/partnerstack-import-email-withou…
devkiran Sep 22, 2026
256001c
Only show link override icons when assignment differs from partner. (…
devkiran Sep 22, 2026
2c6ec55
Add `KeepPartnerInGroupNotice` (#4543)
steven-tey Sep 22, 2026
db2627f
Fix whitelisted domain check (#4544)
steven-tey Sep 22, 2026
f925ff6
improve error messaging
steven-tey Sep 22, 2026
7ab7485
Add Add magic-byte checks to R2 upload (#4539)
devkiran Sep 23, 2026
f97584e
update programs
steven-tey Sep 23, 2026
f37f019
Partner link via improvements
marcusljf Sep 23, 2026
23ccf6a
Animating the sidenav icons
marcusljf Sep 23, 2026
905a10d
CR Fix
marcusljf Sep 23, 2026
cec75d9
Respect link-level discount overrides on customer API responses.
devkiran Sep 23, 2026
cc8a5d4
Merge pull request #4546 from dubinc/sep22-partner-link-details
steven-tey Sep 23, 2026
432322a
Merge pull request #4547 from dubinc/animated-icons
steven-tey Sep 23, 2026
e69b416
Merge pull request #4548 from dubinc/fix/customer-link-discount-override
steven-tey Sep 23, 2026
1c7d703
Merge branch 'main' into soft-delete-discount-cleanup
devkiran Sep 23, 2026
1484042
Update partners-redirect.ts
steven-tey Sep 23, 2026
ac9f2f1
Block deleting partner groups that still have partners.
devkiran Sep 23, 2026
aa883b8
Hard-delete orphaned rewards only when no references remain.
devkiran Sep 23, 2026
62598f4
Update route.ts
devkiran Sep 23, 2026
b8e4f0f
Attach default discounts to enrollments in a background job.
devkiran Sep 23, 2026
1c02b4b
Rename discount cleanup to detach and extract attachDiscount.
devkiran Sep 23, 2026
93711ed
Update delete-discount.ts
devkiran Sep 23, 2026
7dcb004
Soft-delete group discounts on delete and fix detach remapping.
devkiran Sep 23, 2026
d24a3ea
Move partner group change side effects into a background job.
devkiran Sep 23, 2026
cce280b
Move discount-code queue-batches into publish-discount-codes-creation…
devkiran Sep 23, 2026
4ddaf3c
Defer soft-deleted discount hard-delete to orphaned-rewards after cod…
devkiran Sep 23, 2026
bad11a9
Sync partner discount codes directly after remapping default links.
devkiran Sep 23, 2026
ad5123f
Merge branch 'main' into soft-delete-discount-cleanup
devkiran Sep 23, 2026
8b1284a
Update route.ts
devkiran Sep 23, 2026
ce3d54f
Rename with syncDiscountCodes
devkiran Sep 23, 2026
ee553b8
Merge branch 'soft-delete-discount-cleanup' of https://github.com/dub…
devkiran Sep 23, 2026
3c29edd
Batch discount code sync across partnerIds in one query path.
devkiran Sep 23, 2026
e379495
Await group-delete detach and sync discount codes for inactive enroll…
devkiran Sep 23, 2026
a28b271
Page discount link cache invalidation and route coupon deletes throug…
devkiran Sep 23, 2026
a0f3465
Fix tests
devkiran Sep 23, 2026
0b34e7e
Null discount groupId on group delete and fix related soft-delete gua…
devkiran Sep 23, 2026
b2cbbed
Make partner group-change side effects idempotent across job retries.
devkiran Sep 23, 2026
893a752
Update coupon-deleted.ts
devkiran Sep 23, 2026
ba026ca
Filter orphan deletes to unreferenced rows and namespace group-change…
devkiran Sep 23, 2026
2d85b3d
Move discount code sync into waitUntil so failures cannot skip logging.
devkiran Sep 23, 2026
eb609ab
disable payout reminder for migrating programs
pepeladeira Sep 23, 2026
9f4126f
Merge pull request #4549 from dubinc/migrating-payout-reminders
steven-tey Sep 23, 2026
e2537e0
Update process-partner-group-change-job.ts
devkiran Sep 23, 2026
7f8f077
Add `v4_usage` with granularity support, improve `v4_timeseries`
steven-tey Sep 23, 2026
6542307
/disabled rewrite
steven-tey Sep 23, 2026
78c4d99
update packages
steven-tey Sep 23, 2026
3b4edc7
Exclude `/api/:path*` for non `.dub.co` domains
steven-tey Sep 23, 2026
54db220
include `dub.co`, remove old redirects
steven-tey Sep 23, 2026
5d4b6f6
match .dub.co for now
steven-tey Sep 23, 2026
9248fa9
remove disabled links test for now
steven-tey Sep 23, 2026
57b81f7
Update resource.ts
steven-tey Sep 23, 2026
5a50a91
Partner link copy position
marcusljf Sep 24, 2026
bd122ea
Merge pull request #4550 from dubinc/exclude-api-paths
steven-tey Sep 24, 2026
277d31d
Merge pull request #4551 from dubinc/sep23-partner-link-copy
steven-tey Sep 24, 2026
d873ce6
Update domains.ts
steven-tey Sep 24, 2026
1a4344b
Merge branch 'main' into soft-delete-discount-cleanup
devkiran Sep 24, 2026
279ff73
Update domains.ts
steven-tey Sep 24, 2026
5980678
Soft-delete group default links and fix partner-move side effects
devkiran Sep 24, 2026
c3be4cf
Move orphaned cleanup cron and hard-delete soft-deleted default links
devkiran Sep 24, 2026
0902be5
Re-provision codes after non-equivalent remaps and allow detach redrive
devkiran Sep 24, 2026
76b45df
Merge branch 'main' into soft-delete-discount-cleanup
devkiran Sep 24, 2026
5467557
Simplify discount link cache invalidation to by partners or discount
devkiran Sep 24, 2026
9b0ca1e
Page discount codes by default link and delete empty link rewards aft…
devkiran Sep 24, 2026
a9957c7
Add flow control to discount code remap and create jobs.
devkiran Sep 24, 2026
fffb8ed
Drop queueName when using flow control and skip sync link reward clea…
devkiran Sep 24, 2026
c02d26f
Ignore soft-deleted discounts at the edge and expire link cache on de…
devkiran Sep 24, 2026
b4af702
Move connect payout reminders to a background job.
devkiran Sep 24, 2026
b66592f
Page connect payout reminders by partner so large runs can resume.
devkiran Sep 24, 2026
36caf3f
Dedupe connect payout reminder emails by day and batch number.
devkiran Sep 24, 2026
f695fb2
Fix build
devkiran Sep 24, 2026
d95095b
clear defaultPartnerId when a partner member is removed
pepeladeira Sep 24, 2026
8ea737b
Partner codes
marcusljf Sep 24, 2026
1927ace
tweak prompt
steven-tey Sep 24, 2026
05b6b0f
Pin content type and size on resume signed uploads
cursoragent Sep 24, 2026
2fdc29d
Rate limit resume uploads and stop returning the storage key
devkiran Sep 24, 2026
e12a07f
Migrate 10 rate limits to assertRateLimit
cursoragent Sep 24, 2026
9995ab0
Fix upstash import order in migrated routes
cursoragent Sep 24, 2026
6f06cb4
CR Fix
marcusljf Sep 24, 2026
de2787e
Fix mobile overflow on admin commissions page
cursoragent Sep 24, 2026
feab622
Merge pull request #4559 from dubinc/cursor/fix-commissions-mobile-ov…
steven-tey Sep 24, 2026
ec345d2
Merge branch 'main' into commission-customer-id-search
pepeladeira Sep 24, 2026
5ece6d0
add buildCustomerSearchWhere
pepeladeira Sep 24, 2026
6d711c9
Filter `GET /discount-codes` by `code`
steven-tey Sep 24, 2026
d7ab405
Merge pull request #4561 from dubinc/discount-code-filter
steven-tey Sep 24, 2026
da6b306
Merge pull request #4556 from dubinc/discount-code-table
steven-tey Sep 24, 2026
2472cf9
Merge pull request #4541 from dubinc/commission-customer-id-search
steven-tey Sep 24, 2026
0a623f1
update placeholder text
steven-tey Sep 24, 2026
c6f55ba
Merge pull request #4558 from dubinc/cursor/assert-rate-limit-migrati…
steven-tey Sep 24, 2026
d984003
Merge pull request #4555 from dubinc/clear-default-partner-id
steven-tey Sep 24, 2026
1c8c711
Merge branch 'main' into connect-payout-reminders-job
steven-tey Sep 25, 2026
0afb351
address CR feedback
steven-tey Sep 25, 2026
d727768
Update send-payout-reminder.test.ts
steven-tey Sep 25, 2026
21b1040
Merge pull request #4552 from dubinc/connect-payout-reminders-job
steven-tey Sep 25, 2026
50e9885
Merge branch 'main' into soft-delete-discount-cleanup
steven-tey Sep 25, 2026
d0caa38
Merge pull request #4534 from dubinc/soft-delete-discount-cleanup
steven-tey Sep 25, 2026
0469f2f
update schema
steven-tey Sep 25, 2026
387eb22
Remove group delete test that moves the shared E2E partner
devkiran Sep 25, 2026
0bf5d04
Skip group change email when the group is missing
devkiran Sep 25, 2026
3f72047
Revert "Skip group change email when the group is missing"
devkiran Sep 25, 2026
d951cad
Update process-partner-group-change-job.ts
devkiran Sep 25, 2026
74d0531
Merge pull request #4562 from dubinc/remove-group-partner-delete-test
devkiran Sep 25, 2026
8ac6916
Icon update
marcusljf Sep 25, 2026
3ace213
Move discount code creation onto a job and skip deleted discounts.
devkiran Sep 25, 2026
51e1ffe
Update remap-discount-code.ts
devkiran Sep 25, 2026
db61d8a
Merge pull request #4563 from dubinc/sep24-icon-update
steven-tey Sep 25, 2026
daf4a03
Merge branch 'main' into cursor/pin-resumes-upload-url-5c11
devkiran Sep 25, 2026
3c88d01
Merge pull request #4566 from dubinc/improve-discount-code-jobs
steven-tey Sep 25, 2026
77b50d9
Merge pull request #4557 from dubinc/cursor/pin-resumes-upload-url-5c11
steven-tey Sep 25, 2026
cf8f25d
allow any characters in custom provider discount codes
pepeladeira Sep 25, 2026
04ec800
code improvements
pepeladeira Sep 25, 2026
e73fd45
Merge pull request #4570 from dubinc/allow-custom-discount-code-chara…
steven-tey Sep 25, 2026
d19db25
Update send-payout-reminder.ts
steven-tey Sep 25, 2026
636400a
Add discard draft action to branding form Unsaved draft label.
cursoragent Sep 25, 2026
4b3bc09
fix tests
steven-tey Sep 26, 2026
b2ab6ae
add window.confirm
steven-tey Sep 26, 2026
cb58c11
Merge pull request #4572 from dubinc/cursor/discard-draft-tooltip-a6e5
steven-tey Sep 26, 2026
ac13d86
improve redirect logic
steven-tey Sep 26, 2026
d85e883
update
steven-tey Sep 27, 2026
08cb0a6
fix: allow clearing link externalId with empty string or null
cursoragent Sep 28, 2026
4f4f5ab
chore: fix import order in links API playwright spec
cursoragent Sep 28, 2026
eb5d9c3
test: move externalId clear coverage to Vitest create-link suite
cursoragent Sep 28, 2026
bc0bbd9
test: cover clearing externalId on link update
cursoragent Sep 28, 2026
b7c7c0c
Revert "test: cover clearing externalId on link update"
devkiran Sep 28, 2026
ee10071
Clear tenantId on empty string and move clear tests to update-link
devkiran Sep 28, 2026
21c57ae
Merge pull request #4574 from dubinc/cursor/fix-externalid-clear-319d
devkiran Sep 28, 2026
7bd7ead
Merge remote-tracking branch 'upstream/main' into automation/sync-ups…
github-actions[bot] Sep 28, 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
21 changes: 19 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 @@ -108,6 +116,9 @@ STORAGE_BASE_URL=
STORAGE_PUBLIC_BUCKET=
STORAGE_PRIVATE_BUCKET=

# Shared secret for Cloudflare Workers that call Next.js webhooks
CLOUDFLARE_WORKER_WEBHOOK_SECRET=

# Used for internal monitoring & paging
# You can remove this by removing `DUB_SLACK_HOOK_CRON` and `DUB_SLACK_HOOK_LINKS` from the codebase
DUB_SLACK_HOOK_CRON=
Expand All @@ -121,6 +132,7 @@ DUB_SLACK_ASSISTANT_BOT_TOKEN=
NEXT_PUBLIC_NGROK_URL=

# For AI features
AI_GATEWAY_API_KEY=
ANTHROPIC_API_KEY=

# Axiom – used for logging and monitoring
Expand Down Expand Up @@ -195,4 +207,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=
61 changes: 39 additions & 22 deletions apps/web/app/(ee)/admin.dub.co/(dashboard)/commissions/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,19 @@ function CommissionsPageClient() {
const { queryParams, getQueryString, searchParamsObj } = useRouterStuff();
const { interval, start, end, programId } = searchParamsObj;

const { data: { programs: allPrograms } = {} } = useSWR<AdminCommissionsData>(
`/api/admin/commissions${getQueryString(
{
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
},
{ exclude: ["programId"] },
)}`,
fetcher,
{
keepPreviousData: true,
},
);

const { data: { programs, timeseries } = {}, isLoading } =
useSWR<AdminCommissionsData>(
`/api/admin/commissions${getQueryString({
Expand All @@ -62,7 +75,7 @@ function CommissionsPageClient() {
icon: GridIcon,
label: "Program",
options:
programs?.map((program) => ({
allPrograms?.map((program) => ({
value: program.id,
label: program.name,
icon: (
Expand All @@ -72,6 +85,7 @@ function CommissionsPageClient() {
className="size-4 rounded-full"
/>
),
right: currencyFormatter(program.commissions),
})) ?? null,
},
],
Expand Down Expand Up @@ -279,8 +293,8 @@ function CommissionsPageClient() {
});

return (
<div className="mx-auto grid w-full max-w-screen-xl gap-5 p-3 lg:px-10">
<div className="flex flex-col gap-3 md:flex-row md:items-center">
<div className="mx-auto grid w-full min-w-0 max-w-screen-xl gap-3 overflow-x-hidden p-3 lg:px-10">
<div className="flex min-w-0 flex-col gap-3 md:flex-row md:items-center">
<Filter.Select
className="w-full md:w-fit"
filters={filters}
Expand All @@ -294,18 +308,16 @@ function CommissionsPageClient() {
/>
</div>
{activeFilters.length > 0 && (
<div>
<Filter.List
filters={filters}
activeFilters={activeFilters}
onSelect={onSelect}
onRemove={onRemove}
onRemoveAll={onRemoveAll}
/>
</div>
<Filter.List
filters={filters}
activeFilters={activeFilters}
onSelect={onSelect}
onRemove={onRemove}
onRemoveAll={onRemoveAll}
/>
)}
<div className="flex flex-col divide-y divide-neutral-200 rounded-lg border border-neutral-200 bg-white">
<div className="scrollbar-hide grid w-full grid-cols-2 divide-x overflow-y-hidden sm:grid-cols-3">
<div className="flex min-w-0 flex-col divide-y divide-neutral-200 overflow-hidden rounded-lg border border-neutral-200 bg-white">
<div className="scrollbar-hide grid w-full grid-cols-1 divide-y overflow-hidden sm:grid-cols-2 sm:divide-x sm:divide-y-0">
{tabs.map(({ id, label, colorClassName, disabled }) => {
return (
<button
Expand All @@ -317,7 +329,7 @@ function CommissionsPageClient() {
});
}}
className={cn(
"border-box relative block h-full w-full flex-none px-4 py-3 sm:px-8 sm:py-6",
"border-box relative block h-full min-w-0 w-full flex-none overflow-hidden px-4 py-3 sm:px-8 sm:py-6",
"ring-inset ring-neutral-500 focus-visible:ring-1 sm:first:rounded-tl-xl",
disabled
? "cursor-not-allowed"
Expand All @@ -334,17 +346,17 @@ function CommissionsPageClient() {
<div className="flex items-center gap-2.5 text-sm text-neutral-600">
<div
className={cn(
"h-2 w-2 rounded-sm bg-current shadow-[inset_0_0_0_1px_#00000019]",
"h-2 w-2 shrink-0 rounded-sm bg-current shadow-[inset_0_0_0_1px_#00000019]",
colorClassName,
)}
/>
<span>{label}</span>
</div>
<div className="mt-1 flex h-12 items-center">
<div className="mt-1 flex h-12 min-w-0 items-center">
{(totals[id] || totals[id] === 0) && !isLoading ? (
<NumberFlow
value={(totals[id] ?? 0) / 100}
className="text-xl font-medium sm:text-3xl"
className="truncate text-xl font-medium sm:text-3xl"
format={{
style: "currency",
currency: "USD",
Expand All @@ -360,8 +372,8 @@ function CommissionsPageClient() {
);
})}
</div>
<div className="p-5 sm:p-10">
<div className="flex h-96 w-full items-center justify-center">
<div className="min-w-0 overflow-hidden p-5 sm:p-10">
<div className="flex h-96 w-full min-w-0 items-center justify-center">
{chartData ? (
chartData.length > 0 ? (
<TimeSeriesChart
Expand Down Expand Up @@ -419,7 +431,12 @@ function CommissionsPageClient() {
/>
<YAxis
showGridLines
tickFormat={(value) => currencyFormatter(value)}
tickFormat={(value) =>
currencyFormatter(value, {
notation: "compact",
maximumFractionDigits: 1,
})
}
/>
</TimeSeriesChart>
) : (
Expand All @@ -433,7 +450,7 @@ function CommissionsPageClient() {
</div>
</div>
</div>
<div className="w-full">
<div className="w-full min-w-0 overflow-x-auto">
<Table {...tableProps} table={table} />
</div>
</div>
Expand Down
Loading
Loading