feat(wallet): worker withdrawal flow + auto-release cron - #19
Conversation
|
CodeAnt AI is reviewing your PR. Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds DB withdraw and auto-release migrations, deterministic RPC error mapping and a server withdraw action, client WithdrawDialog and top-up idempotency via generateUuid(), plus many formatting, docs, and seed script updates. ChangesAll changes (single cohort)
Sequence Diagram sequenceDiagram
participant Client
participant WithdrawDialog
participant ServerAction as withdrawWalletAction
participant DB as withdraw_wallet_RPC
participant Wallet as wallets_table
participant Ledger as escrow_ledger_table
Client->>WithdrawDialog: open, enter amount, submit (idempotency_key)
WithdrawDialog->>ServerAction: call withdrawWalletAction(amount, idempotency_key)
ServerAction->>DB: CALL withdraw_wallet(p_amount, p_idempotency_key)
DB->>Ledger: lookup or insert idempotent escrow_ledger row (withdraw)
DB->>Wallet: SELECT ... FOR UPDATE and debit available_balance (if new)
DB->>ServerAction: return available_balance, ledger_id
ServerAction->>WithdrawDialog: return success/result
WithdrawDialog->>Client: invalidate wallet queries, show toast, close
Estimated code review effort 🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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 `@src/app/_actions/wallet.ts`:
- Around line 95-109: The current error handling in the block that inspects
error.message is fragile; update the logic to inspect error.code (the
Postgres/RPC errcode) instead of parsing error.message: in the same
function/block in src/app/_actions/wallet.ts replace the message-substring
checks with a mapping/switch on error.code (e.g., check error.code ===
'INVALID_AMOUNT_CODE' / 'INSUFFICIENT_BALANCE_CODE' / 'NOT_AUTHENTICATED_CODE' /
'WALLET_NOT_FOUND_CODE' and return the same user-facing messages) and keep a
default fallback returning the generic withdraw error; ensure you reference the
same error variable and preserve existing returned shapes ({ success: false,
error: ... }).
In `@supabase/migrations/20260518181500_withdraw_wallet_fix_ambiguous_column.sql`:
- Around line 30-33: The NULL guard for p_amount was removed so NULL now
bypasses validation; update the conditional to explicitly check for NULL and low
values by changing the block that currently reads "if p_amount < 100 then" to
include an IS NULL check (e.g. "if p_amount IS NULL OR p_amount < 100 then") so
that NULL values raise the same exception 'invalid_amount' with errcode '22023'.
In `@supabase/migrations/20260518225500_schedule_auto_release_cron.sql`:
- Around line 101-106: The current IF check against public.wallets (the clause
referencing w.profile_id = v_milestone.client_id and w.locked_balance >=
v_milestone.amount) silently skips milestones when balance is insufficient;
update the procedure that processes v_milestone to explicitly handle this
branch: either INSERT a log entry (into a milestones_log/audit table) or UPDATE
the milestone row (e.g., set status = 'failed' or add a failure_reason like
'insufficient_locked_balance' and updated_at), and/or call your notification
procedure (e.g., notify_client_of_insufficient_balance) so the worker and client
are informed; make the change around the IF ... THEN block that references
v_milestone so the insufficient-balance path is not a silent no-op but records
the condition and triggers notification or a documented state transition.
🪄 Autofix (Beta)
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 Plus
Run ID: 03bf56e5-2255-480f-98e7-32076a9b096c
📒 Files selected for processing (10)
src/app/_actions/wallet.tssrc/components/features/topup-dialog.tsxsrc/components/features/wallet-view.tsxsrc/components/features/withdraw-dialog.tsxsrc/lib/schemas/wallet.tssrc/lib/uuid.tssupabase/functions/auto_release_milestones/index.tssupabase/migrations/20260518135600_withdraw_wallet.sqlsupabase/migrations/20260518181500_withdraw_wallet_fix_ambiguous_column.sqlsupabase/migrations/20260518225500_schedule_auto_release_cron.sql
💤 Files with no reviewable changes (1)
- supabase/functions/auto_release_milestones/index.ts
There was a problem hiding this comment.
Pull request overview
Adds a worker withdrawal RPC + UI flow and replaces the prior Edge Function scaffold with a pure-Postgres auto-release cron job for submitted milestones.
Changes:
- Introduces
withdraw_wallet()RPC (with per-user idempotency) and wires it into a newwithdrawWalletAction+<WithdrawDialog />UI. - Adds a UUID helper to support idempotency key generation in insecure dev contexts (LAN/HTTP).
- Schedules
auto_release_milestones()viapg_cronand updates status-guard triggers to accommodate cron-driven status updates.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| supabase/migrations/20260518225500_schedule_auto_release_cron.sql | Adds pg_cron scheduling, updates guard triggers, and modifies auto_release_milestones() to support cron execution. |
| supabase/migrations/20260518181500_withdraw_wallet_fix_ambiguous_column.sql | Replaces withdraw_wallet to fix available_balance ambiguity by aliasing table references. |
| supabase/migrations/20260518135600_withdraw_wallet.sql | Introduces initial withdraw_wallet RPC and a partial unique idempotency index. |
| supabase/functions/auto_release_milestones/index.ts | Removes the placeholder Edge Function scaffold. |
| src/lib/uuid.ts | Adds UUID v4 generator with randomUUID()/getRandomValues() fallback. |
| src/lib/schemas/wallet.ts | Adds Zod schema/type for withdrawal inputs. |
| src/components/features/withdraw-dialog.tsx | Adds worker withdrawal dialog with validation, quick amounts, and idempotency key generation. |
| src/components/features/wallet-view.tsx | Role-gates wallet dialogs (TopUp for clients, Withdraw for workers). |
| src/components/features/topup-dialog.tsx | Switches top-up idempotency key generation to the shared UUID helper. |
| src/app/_actions/wallet.ts | Adds withdrawWalletAction server action calling withdraw_wallet with error mapping and revalidation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
CodeAnt AI finished reviewing your PR. |
- withdraw_wallet: add is_worker() check (A), p_amount NULL guard (F), p_idempotency_key NULL guard (C), insert-first/catch unique_violation pattern mirroring topup (D), public. schema qualifier (I), w. table alias to disambiguate available_balance vs RETURNS TABLE column - worker-form: replace as any cast with typed cast to fix pre-existing lint error surfaced during PR #19 pipeline - guard_milestones_status + guard_jobs_status: replace GUC bypass with direct session_user='postgres' check (E) - auto_release_milestones: drop set_config GUC line (E), add RAISE NOTICE on insufficient-locked-balance skip (H) - wallet.ts: mapWalletRpcError helper routing by error.code + exact RAISE EXCEPTION token (G), defense-in-depth is_worker() pre-check in withdrawWalletAction (A), raw error logging before generic fallback - topup-dialog + withdraw-dialog: stable idempotencyKey via useState initializer + useEffect reset on close (B), normalize 4-space to 2-space indent on withdraw-dialog (Copilot #9)
|
CodeAnt AI is running Incremental review Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
|
CodeAnt AI Incremental review completed. |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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 `@src/app/_actions/wallet.ts`:
- Around line 90-97: The unmapped RPC error branches and catch blocks in the
wallet server actions (e.g., inside topupWalletAction and the other try/catch
paths referenced) currently only log and return a generic error; add
Sentry.captureException(error) in each of those branches before returning to
ensure exceptions are recorded, and import Sentry if not already present; ensure
you call Sentry.captureException(error) (or Sentry.withScope if you want to add
context/tags) in the unmapped RPC error block and inside each catch block around
lines referenced (including the other error branches at 115-119, 162-169,
187-190) so every Server Action try/catch path captures the error.
In `@src/lib/uuid.ts`:
- Around line 25-30: The fallback UUID generator using Math.random() in
src/lib/uuid.ts (the "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx" replace block) must
be removed and converted to a fail-fast when secure crypto is unavailable:
detect secure randomness via globalThis.crypto?.getRandomValues (or similar) at
the start of the UUID function and if not present throw an explicit error (or
return a clear failure) instead of using Math.random; replace the existing
replace(...) fallback block with that fail-fast behavior and ensure the function
documents/propagates the thrown error so idempotency callers (wallet idempotency
key flows) cannot silently get weaker non-cryptographic IDs.
In `@supabase/migrations/20260521080000_pr19_review_fixes.sql`:
- Around line 152-265: The migration modifies the protected SECURITY DEFINER
function auto_release_milestones(), which violates policy; revert any changes to
that existing function and instead implement the new behavior as a separate RPC
(e.g., public.rpc_auto_release_milestones) or create an ADR explicitly approving
this exact modification; update the migration to remove the CREATE OR REPLACE
for auto_release_milestones and add a new migration that defines the new RPC
wrapper (or include the ADR file reference) so the original SECURITY DEFINER
function remains unchanged unless an ADR is accepted.
🪄 Autofix (Beta)
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 Plus
Run ID: 88f528ef-4a4b-43ca-8134-219553f8128a
📒 Files selected for processing (67)
AGENTS.mdCLAUDE.mdREADME.mdadr/0001-auth.mdadr/0002-escrow.mdadr/0003-dispute.mdadr/0004-auto-release.mdadr/0005-realtime-contract.mdadr/0006-nav-shell.mdadr/0007-rpc-pattern.mdadr/template.mddocs/adr/0002-rls-and-security-definer-as-only-escrow-path.mddocs/adr/0003-auth-state-hygiene.mddocs/adr/0004-migration-discipline.mddocs/data-model.mddocs/state-machine.mdscripts/seed-demo-users.tssrc/app/_actions/jobs.tssrc/app/_actions/wallet.tssrc/app/client/account/page.tsxsrc/app/client/jobs/[id]/client-job-detail.tsxsrc/app/client/jobs/[id]/fund/page.tsxsrc/app/client/jobs/[id]/milestones/client-milestones.tsxsrc/app/client/jobs/client-job-list.tsxsrc/app/client/jobs/new/post-job-form.tsxsrc/app/client/layout.tsxsrc/app/login/actions.tssrc/app/onboarding/actions.tssrc/app/onboarding/worker/worker-form.tsxsrc/app/page.tsxsrc/app/worker/account/page.tsxsrc/app/worker/applications/worker-applications.tsxsrc/app/worker/feed/worker-feed.tsxsrc/app/worker/jobs/[id]/apply-modal.tsxsrc/app/worker/jobs/[id]/milestones/worker-milestones.tsxsrc/app/worker/jobs/[id]/worker-job-detail.tsxsrc/app/worker/layout.tsxsrc/components/account/change-phone-dialog.tsxsrc/components/account/delete-account-dialog.tsxsrc/components/account/edit-profile-dialog.tsxsrc/components/account/sign-out-button.tsxsrc/components/features/topup-dialog.tsxsrc/components/features/wallet-view.tsxsrc/components/features/withdraw-dialog.tsxsrc/components/nav/client-nav-shell.tsxsrc/components/nav/worker-nav-shell.tsxsrc/components/ui/alert-dialog.tsxsrc/components/ui/badge.tsxsrc/components/ui/dialog.tsxsrc/components/ui/progress.tsxsrc/components/ui/separator.tsxsrc/components/ui/skeleton.tsxsrc/components/ui/status-badge.tsxsrc/components/ui/textarea.tsxsrc/hooks/use-user.tssrc/lib/__tests__/escrow.property.test.tssrc/lib/escrow-machine.tssrc/lib/format.tssrc/lib/schemas/jobs.tssrc/lib/schemas/onboarding.tssrc/lib/schemas/wallet.tssrc/lib/supabase/client.tssrc/lib/supabase/server.tssrc/lib/types/database.tssrc/lib/uuid.tssupabase/functions/auto-release/index.tssupabase/migrations/20260521080000_pr19_review_fixes.sql
💤 Files with no reviewable changes (2)
- src/lib/format.ts
- src/app/login/actions.ts
✅ Files skipped from review due to trivial changes (48)
- adr/template.md
- adr/0006-nav-shell.md
- src/components/ui/separator.tsx
- src/app/client/jobs/[id]/fund/page.tsx
- src/app/worker/jobs/[id]/apply-modal.tsx
- README.md
- src/app/client/layout.tsx
- src/lib/supabase/client.ts
- docs/data-model.md
- docs/adr/0003-auth-state-hygiene.md
- supabase/functions/auto-release/index.ts
- src/lib/tests/escrow.property.test.ts
- src/components/nav/client-nav-shell.tsx
- src/components/account/change-phone-dialog.tsx
- src/components/ui/skeleton.tsx
- adr/0005-realtime-contract.md
- src/components/ui/status-badge.tsx
- src/app/client/jobs/client-job-list.tsx
- CLAUDE.md
- src/app/onboarding/actions.ts
- src/components/ui/progress.tsx
- src/lib/schemas/onboarding.ts
- src/app/worker/layout.tsx
- docs/adr/0004-migration-discipline.md
- adr/0002-escrow.md
- src/lib/types/database.ts
- src/components/account/sign-out-button.tsx
- src/lib/escrow-machine.ts
- adr/0003-dispute.md
- src/app/worker/feed/worker-feed.tsx
- src/app/_actions/jobs.ts
- src/components/nav/worker-nav-shell.tsx
- src/app/worker/account/page.tsx
- src/hooks/use-user.ts
- src/app/page.tsx
- docs/state-machine.md
- src/app/client/jobs/new/post-job-form.tsx
- src/app/client/account/page.tsx
- src/components/ui/textarea.tsx
- src/app/worker/applications/worker-applications.tsx
- adr/0001-auth.md
- src/components/account/edit-profile-dialog.tsx
- adr/0007-rpc-pattern.md
- src/components/ui/badge.tsx
- adr/0004-auto-release.md
- src/components/ui/dialog.tsx
- docs/adr/0002-rls-and-security-definer-as-only-escrow-path.md
- src/components/account/delete-account-dialog.tsx
|
@copilot review |
Reviewed and re-validated this PR state. The review feedback is already addressed in commit Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
- uuid.ts: remove Math.random fallback, throw if secure crypto API unavailable; collision resistance is a correctness requirement for wallet idempotency keys (CodeRabbit follow-up #2) - docs/adr/0005: ADR approving in-place modification of auto_release_milestones per Copilot #1 (drop GUC bypass) and CodeRabbit #3 (RAISE NOTICE on silent skip), as required by ADR 0004 - pr19_review_fixes.sql: add ADR reference comment above auto_release_milestones rewrite for discoverability (CodeRabbit follow-up #3) Deferred to PR #20: Sentry.captureException wiring (CodeRabbit follow-up #1) — Sentry SDK installation is the dedicated scope of PR #20 per the roadmap; adding capture calls now would not compile.
|
@CodeAnt-AI review |
|
CodeAnt AI is running the review. Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
|
@CodeAnt-AI review |
|
CodeAnt AI is running the review. Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
Sequence DiagramThis PR adds a worker-only wallet withdrawal path with idempotent RPC handling and a database cron that auto-releases eligible milestones, updating wallets and ledger entries while emitting notifications. sequenceDiagram
participant Worker
participant App
participant Database
participant Cron
Worker->>App: Submit withdraw request
App->>Database: Call withdraw_wallet via server action
Database->>Database: Validate worker, idempotency, balance and write ledger
Database-->>App: Return new available balance and ledger id
App-->>Worker: Show updated wallet balance
Cron->>Database: Run auto_release_milestones on schedule
Database->>Database: Release due milestones and update wallets and ledger
Database-->>App: Create notifications for client and worker
Generated by CodeAnt AI |
… race guard (round-12)
Fix 1 - worker-milestones.tsx, client-milestones.tsx, client-job-detail.tsx:
move router.replace("/login") out of the render path and into a
useEffect([isAuthLoading, user, router]). Render guard collapses back to
the combined `isAuthLoading || !user?.id` -> <Skeleton /> shape, so the
user sees a skeleton both during auth hydration and during the
post-resolution navigation tick. No flash of stale UI; no React
render-phase side-effect warning. Dependency array uses `user` (whole
object) rather than `user?.id` to avoid the exhaustive-deps lint warning
about optional chaining in deps. Closes the regression from round-11
Fix 2/3.
Fix 2 - worker/account/page.tsx: clear local profile and worker state
(setProfile(null), setWorker(null)) before router.replace("/login") in
load()'s !user branch, so stale private data cannot flash between
sign-out and navigation.
Fix 3 - worker/account/page.tsx: add a requestIdRef race-condition guard
to load(). Increments at call start; checks before every setState after
each await (post-getUser, post-Promise.all). Concurrent triggers
(initial mount, Retry, onSaved, onChanged) can no longer have an older
in-flight request overwrite fresher state. Pattern is dependency-free;
no AbortController used because supabase-js v2 doesn't support signal
forwarding on .from() queries.
Refs round-12 review: CodeAnt Major x4 (3 render-phase redirects, 1
stale state, 1 race condition; the render-phase finding was applied
proactively to worker-milestones.tsx as well even though CodeAnt only
flagged it on the two client files).
Round-12 CodeAnt CRITICAL x1 (client-milestones.tsx:238 fund
idempotency forwarding) reply-dismissed and conversation resolved in
PR thread: see Phase 4.5 escrow idempotency PR.
|
@CodeAnt-AI review |
|
CodeAnt AI is running the review. Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
Sequence DiagramThis diagram shows the new worker withdrawal flow that debits wallets via a secure RPC with idempotent keys, and the pg_cron-driven auto_release_milestones job that automatically releases eligible escrowed milestones and updates wallets. sequenceDiagram
participant Worker
participant WebApp
participant ServerAction
participant Database
participant CronScheduler
Worker->>WebApp: Submit withdraw request (amount)
WebApp->>ServerAction: Call withdrawWalletAction
ServerAction->>Database: Verify worker role and validate amount
ServerAction->>Database: RPC withdraw_wallet with idempotency key
Database-->>ServerAction: Updated balance and ledger entry
ServerAction-->>WebApp: Return success and revalidate wallet view
CronScheduler->>Database: Run auto_release_milestones daily
Database->>Database: Auto-release eligible milestones and update wallets
Database-->>CronScheduler: Return count of released milestones
Generated by CodeAnt AI |
|
CodeAnt AI finished running the review. Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
shaiksohelll
left a comment
There was a problem hiding this comment.
PR #19 — Final Review Summary
This PR went through 12 review rounds (rounds 5–12 documented in commit history). All Critical/High/Major findings against in-scope changes have been addressed. Remaining open threads fall into the following intentional-deferral buckets:
Deferred to Phase 4.5 — Escrow Idempotency PR
RPC-level uniqueness enforcement on p_idempotency_key for fund_escrow, submit_milestone, approve_milestone, dispute_milestone. The client-side per-intent key shipped in round-8 is the application-layer half of the contract; the RPC signatures don't currently accept the parameter, so wiring it through the server action would have nothing to bind to. Tracked separately.
Threads: worker-milestones submit handler; client-milestones fund/approve/dispute handlers.
Deferred to PR #20 — Sentry Observability Sweep
All // TODO: Sentry.captureException() markers across the codebase will land together once Sentry is wired in. Markers were intentionally left as TODOs rather than partial wiring.
Threads: escrow.ts (8 sites); worker-milestones.tsx; worker/account/page.tsx (round-11 authError marker).
Deferred to PR #22 — Canonical Async-Dialog Migration
Migration of apply-modal, change-phone-dialog, delete-account-dialog, edit-profile-dialog, worker-form, and withdraw-dialog Zod schema to the canonical async-dialog pattern (reference: src/components/features/topup-dialog.tsx). Three new server actions (updatePhoneAction, requestAccountDeletion, updateProfile) accompany.
Threads: all *-dialog files flagged for pattern migration.
Deferred to PR #23 (downgraded) — Realtime Auth Propagation
Originally flagged: per-file .setAuth() calls on Realtime channels. Resolution: the singleton client at src/lib/supabase/client.ts handles auth propagation to Realtime via eager-prime + onAuthStateChange listener; no per-file action needed. PR #23 scope reduced to verifying all .channel() sites use the singleton (not fresh createClient()).
Threads: 3 Realtime setAuth comments.
Intentional Design Choices (Not Bugs)
- Migration consolidation (
20260521080000_pr19_review_fixes.sql): consolidated round-5 fix-pack for review feedback against the original three feature migrations. Section headers (A/C/D/F/I/E/H) document the per-concern split internally. Future migrations follow one-per-logical-change. - Step4 skillInput draft tag clearing on nav-away: by design.
Bot Convergence Status
- CodeRabbit: converged at round-9 (last finding: Realtime setAuth, dismissed per above).
- Copilot: converged at round-11 (last findings: cron mismatch + exception scope, both fixed).
- CodeAnt: active; remaining re-flags are repeats of the Phase 4.5 finding above.
Ready to merge pending final CI green light.
Summary
Ships worker wallet withdrawal end-to-end and daily auto-release of escrow funds that have passed their review window. Both flows are RPC-backed, idempotent at the application layer, and observable via the existing escrow ledger. PR also rolls in a focused round of state-machine hardening on the escrow flow surfaced during review.
What's in this PR
🏦 Worker withdrawal flow
withdraw_wallet(p_user_id, p_amount, p_idempotency_key)— locks the wallet row, validates available balance, debits balance, and emits a singleescrow_ledgerrow inside a nestedBEGIN..EXCEPTIONblock scoped to the INSERT.unique_violationon the ledger insert (i.e. retry with the samep_idempotency_key) is the only swallowed error path — every other UNIQUE violation propagates. Pattern adopted after review round-11.WithdrawDialogwith Zod-validated amount input, balance display, mapped RPC errors viamapWalletRpcError(typed againstPostgrestError).⏰ Auto-release cron
pg_cronjobauto_release_escrowsscheduled at30 20 * * *(20:30 UTC / 02:00 IST, daily), calling arelease_eligible_escrows()SQL function.escrow_in_reviewmilestones whose review window has elapsed and have no active dispute. Transitions state toreleasedand credits the worker wallet.adr/0004-auto-release.md.🛡️ State-machine hardening
fund_escrow,submit_milestone,approve_milestone,dispute_milestoneagainst out-of-order transitions; previously-possible "double-fund" and "submit-after-release" paths now error explicitly.adr/0002-escrow.mdandadr/0003-dispute.md.🔑 Client-side idempotency (application-layer half)
getOrCreateIdempotencyKey(actionName, milestoneId)shipped insrc/lib/idempotency.ts. Generates a stable per-intent UUID, rotated only on success, so retries reuse the same key.fund,submit,approve,dispute) on the client side.p_idempotency_key uuidparameter + UNIQUE constraint) for the four escrow RPCs is tracked in a follow-up "Phase 4.5 escrow idempotency PR". The client-side key is the application-layer half of the contract; the RPC signatures don't currently accept the parameter. Forwarding it through the action today would have nothing to bind to.🔐 Auth-hydration handling
worker-milestones.tsx,client-milestones.tsx,client-job-detail.tsx) now redirect unauthenticated users to/loginvia auseEffect-drivenrouter.replace, with<Skeleton />rendered during hydration and the navigation tick. Avoids both stale-render and render-phase side-effect issues.worker/account/page.tsx:errorfromsupabase.auth.getUser(); transient auth/network failures now show a retryable error card instead of redirecting signed-in users to/login.profile/workerstate before redirect to prevent stale data flash.load()is guarded by arequestIdRefrace-condition pattern so concurrent triggers (initial mount, Retry,onSaved,onChanged) can't have an older request overwrite fresher state.📜 ADRs
adr/0001-auth.md— auth flow designadr/0002-escrow.md— escrow state machineadr/0003-dispute.md— dispute flowadr/0004-auto-release.md— daily auto-release cron contractadr/0005-realtime-contract.md— Realtime channel ownership and auth propagation via singleton clientadr/0006-nav-shell.md— navigation shell patternadr/0007-rpc-pattern.md— RPC error mapping and contract conventions🗃️ Migrations
202604260004_create_escrow_functions.sql— escrow RPC suite202604260005_harden_state_machine.sql— state-guard hardening20260518135600_withdraw_wallet.sql+20260518181500_withdraw_wallet_fix_ambiguous_column.sql— withdrawal RPC + ambiguity fix20260518225500_schedule_auto_release_cron.sql—pg_cronjob registration (canonical cron source)20260521080000_pr19_review_fixes.sql— consolidated review-fix pack covering rounds 5–6. Section headers (A/C/D/F/I/E/H) document the per-concern split internally. Future migrations follow one-per-logical-change per repo guidelines.Out of scope / deferred
p_idempotency_keyenforcement onfund_escrow/submit_milestone/approve_milestone/dispute_milestoneSentry.captureException()wiring for the 10+// TODO: Sentrymarkers inescrow.ts,worker-milestones.tsx,worker/account/page.tsxapply-modal,change-phone-dialog,delete-account-dialog,edit-profile-dialog,worker-form,withdraw-dialogZod.setAuth()calls on Realtime channelsonAuthStateChange. Tracked under PR #23 only to verify all.channel()sites use the singleton.acceptWorkerActiontransactional RPC + ADRTesting
pnpm lintpnpm typecheckpnpm testpnpm buildManual verification:
select release_eligible_escrows();against seeded escrow rows inescrow_in_reviewpast their review window.Risk
pg_cronjob (select cron.unschedule('auto_release_escrows');) — escrows simply remain inescrow_in_reviewuntil manually released or the job re-enabled.Review history
This PR was through 12 review iterations across CodeRabbit, GitHub Copilot, and CodeAnt. All Critical/High/Major findings against in-scope changes are resolved. Remaining open threads are intentional deferrals tracked in the table above; a consolidated summary review comment documents per-thread disposition.