-
Notifications
You must be signed in to change notification settings - Fork 0
feat(wallet): worker withdrawal flow + auto-release cron #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
shaiksohelll
merged 14 commits into
main
from
feat/wallet-withdrawal-and-autorelease-cron
May 23, 2026
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
9ba6c92
feat(wallet): worker withdrawal flow + auto-release cron
shaiksohelll 60ad8da
fix(wallet): address PR #19 review feedback
shaiksohelll 3ffa256
fix(wallet): PR #19 follow-up review (uuid fail-fast + ADR-0005)
shaiksohelll 912159a
fix: address PR #19 CodeAnt security and correctness findings
shaiksohelll 0e1649d
ο»Ώfix: address PR #19 round-4 review findings
shaiksohelll d851731
ο»Ώfix(escrow): mapEscrowRpcError + wallet scope + auth gates + migratiβ¦
shaiksohelll 1e4c559
ο»Ώfix(withdraw-wallet): INSERT-first idempotency, doc drift, auth hydrβ¦
shaiksohelll 6b67b8f
chore: strip embedded NULL bytes from .gitignore (was treated as binary)
shaiksohelll 55a6034
ο»Ώfix(round-7): 7 in-scope fixes for CR/Copilot/CodeAnt round-7 review
shaiksohelll 272f3a6
ο»Ώfix(round-8): NaN guard, seed wallet locked, Sentry TODO, auth-hydraβ¦
shaiksohelll 51e8c40
ο»Ώdocs(adr): heading indent + pg_cron drift (round-9)
shaiksohelll 6ad2d2c
ο»Ώfix(escrow,wallet): honest idempotency comments + PostgrestError typβ¦
shaiksohelll 2e98378
ο»Ώfix(round-11): auth error handling, auth-hydration redirect (4 filesβ¦
shaiksohelll 394d858
ο»Ώfix(auth): move render-phase redirects to useEffect + worker/accountβ¦
shaiksohelll File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
Binary file not shown.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,10 @@ | ||
| Title: ADR-0004: Auto-release cadence | ||
| Status: Proposed | ||
| Context: | ||
|
|
||
| - Milestones should auto-release after a fixed window if no dispute exists to prevent funds from staying locked indefinitely. | ||
| Decision: | ||
| - Implement a recurring edge function cron (every 5 minutes in prod cadence) to release funded milestones past their auto_release_at and with no active disputes. | ||
| - Schedule `auto_release_milestones()` via `pg_cron` (database-native scheduler) with a cron expression of `30 20 * * *` (20:30 UTC / 02:00 IST, daily). The job is registered in `supabase/migrations/20260518225500_schedule_auto_release_cron.sql`, which is the source of truth. This replaces the originally considered Edge Function cron approach. | ||
|
|
||
| Consequences: | ||
| - Automates the release flow, reduces manual intervention, and requires robust idempotency and event notifications. | ||
| - Auto-release runs inside the database transaction boundary (no cold-start latency, no separate Edge Function deploy). Idempotency is enforced by the `auto_release_milestones()` function itself. Monitoring must target `cron.job_run_details` rather than Edge Function logs. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| # ADR 0005: Permitting In-Place Modification of auto_release_milestones in PR #19 | ||
|
|
||
| - **Status:** Accepted | ||
| - **Date:** 2026-05-21 | ||
| - **Author:** Sohel | ||
| - **Supersedes:** β | ||
| - **Amends:** ADR 0004 (Migration Discipline) | ||
| - **Related:** PR #19, ADR 0002 (RLS + SECURITY DEFINER) | ||
|
|
||
| ## Context | ||
|
|
||
| PR #19 review surfaced two defects in `public.auto_release_milestones()`, the cron-invoked SECURITY DEFINER function that releases milestones whose 72-hour auto-release window has elapsed: | ||
|
|
||
| 1. **Copilot review #1** (`#discussion_r3261123627`): The function uses `perform set_config('pakka.allow_milestone_status_change', 'on', true)` as the bypass mechanism that lets it satisfy `guard_milestones_status` and `guard_jobs_status`. The GUC pattern is fragile β any database role with `SET` privilege can flip the same GUC and bypass the guards from a non-cron context. | ||
| 2. **CodeRabbit review #3** (`#discussion_r3261116639`): When the function skips a milestone because the client's wallet has insufficient `locked_balance` to cover the milestone amount, it does so silently with no log entry. Stuck milestones become invisible in operations. | ||
|
|
||
| ADR 0004 requires an explicit ADR before modifying any SECURITY DEFINER financial state-machine function in place. This ADR provides that approval for the two modifications above. | ||
|
|
||
| ## Decision | ||
|
|
||
| In migration `supabase/migrations/20260521080000_pr19_review_fixes.sql`, modify `public.auto_release_milestones()` to: | ||
|
|
||
| 1. **Remove** the `perform set_config('pakka.allow_milestone_status_change', 'on', true)` call. | ||
| 2. **Replace** the GUC-based bypass with a direct cron-context check in the guard functions: `not (session_user = 'postgres' and current_user = 'postgres')`. Both `guard_milestones_status` and `guard_jobs_status` are updated atomically in the same migration. | ||
| 3. **Add** `raise notice 'auto_release_milestones: skipping milestone % (job %): insufficient locked balance for amount %', m.id, m.job_id, m.amount;` in the insufficient-locked-balance branch. | ||
|
|
||
| A wrapper RPC is not appropriate: the defects are inside the function body (the GUC call and the silent skip branch). A wrapper cannot remove either. | ||
|
|
||
| ## Rationale | ||
|
|
||
| - **Behavioral surface unchanged.** Signature remains `() -> integer`. Invocation pattern remains `pg_cron` at `30 20 * * *` UTC (02:00 IST daily, `jobid = 2`, verified live). Output semantics β count of released milestones β is preserved. | ||
| - **Bypass mechanism strictly stronger.** `session_user = 'postgres'` is set by the Postgres connection role used by `pg_cron`'s background worker and cannot be spoofed from a PostgREST / Supabase client connection, which authenticates as `authenticated` or `anon`. The prior GUC could be flipped by any role with `SET` privilege. | ||
| - **Observability improvement is purely additive.** A `RAISE NOTICE` writes to Postgres logs without changing function output or transaction behavior. | ||
| - **Atomic update of guard functions.** Updating `auto_release_milestones` without simultaneously updating the two guard functions would break the cron job. The three functions must move together; bundling them in one migration is correct. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - Bypass surface narrows from "any role with SET privilege" to "the literal `postgres` connection role used by pg_cron." Closes a theoretical privilege-escalation path. | ||
| - Skipped milestones are visible in `postgres-logs` going forward. Operations can grep for `auto_release_milestones: skipping`. | ||
| - Original function body remains preserved in migration `20260518225500_schedule_auto_release_cron.sql` (append-only history; nothing rewritten in place at the file level). | ||
| - Future modifications to `auto_release_milestones` still require a new ADR per ADR 0004. | ||
|
|
||
| ## Alternatives Considered | ||
|
|
||
| - **Wrapper RPC (`rpc_auto_release_milestones`).** Rejected: the defects are inside the body. A wrapper would still call the broken inner function. | ||
| - **Leave the GUC in place and only fix the silent skip.** Rejected: Copilot's concern about GUC settability is valid and a future security hardening pass would have to revisit it anyway. | ||
| - **Defer to PR #20 or later.** Rejected: the two defects are co-located with code already being modified in PR #19's review-fix cycle; deferring duplicates the migration churn. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,63 +1,77 @@ | ||
| State Machine β Pakka Milestones | ||
|
|
||
| Overview | ||
|
|
||
| - The escrow flow is driven by two interconnected state machines: | ||
| 1) jobs.status: the lifecycle of a job (open, assigned, in_progress, completed, cancelled, disputed). | ||
| 2) milestones.status: per-milestone funding and delivery workflow (pending, funded, submitted, approved, disputed, released, refunded). | ||
|
|
||
| 1. jobs.status: the lifecycle of a job (open, assigned, in_progress, completed, cancelled, disputed). | ||
| 2. milestones.status: per-milestone funding and delivery workflow (pending, funded, submitted, approved, disputed, released, refunded). | ||
|
|
||
| - All transitions must occur through SECURITY DEFINER Postgres functions. No direct client mutation of statuses. | ||
|
|
||
| Milestones State Transitions | ||
| 1) pending -> funded | ||
|
|
||
| 1. pending -> funded | ||
|
|
||
| - Trigger: fund_escrow(milestone_id) | ||
| - Effect: milestone.status becomes 'funded'; atomic ledger entry created; client.locked_balance increases by amount; escrow_ledger updated (fund entry). | ||
| - Caller: client action initiating escrow funding. | ||
|
|
||
| 2) funded -> submitted | ||
| 2. funded -> submitted | ||
|
|
||
| - Trigger: submit_milestone(milestone_id) when worker submits proof and clicks Submit for Review. | ||
| - Effect: milestone.status = 'submitted'; submitted_at = now(); auto_release_at = now() + interval '72 hours'. | ||
| - Caller: worker action. | ||
|
|
||
| 3) submitted -> approved | ||
| 3. submitted -> approved | ||
|
|
||
| - Trigger: approve_milestone(milestone_id) | ||
| - Effect: escrow_held decreases by milestone amount; worker.available increases by amount; milestone.status = 'released' or 'approved' depending on design (commonly 'released' in ledger terms). | ||
| - Caller: client action. | ||
| - Also: emits realtime notification to both parties. | ||
|
|
||
| 4) submitted -> disputed | ||
| 4. submitted -> disputed | ||
|
|
||
| - Trigger: dispute_milestone(milestone_id, reason) | ||
| - Effect: milestone.status = 'disputed'; create disputes row; funds remain locked; notification sent. | ||
| - Caller: client action. | ||
|
|
||
| 5) submitted -> auto-release (cron) | ||
| 5. submitted -> auto-release (cron) | ||
|
|
||
| - Trigger: auto_release_milestones() scheduler | ||
| - Effect: if auto_release_at <= now() and no dispute exists for milestone, milestone.status -> 'released'; worker wallet updated; ledger entry created; realtime notification. | ||
| - Caller: Edge Function cron. | ||
| - Caller: pg_cron (database scheduler). | ||
|
|
||
| 6. disputed -> resolved_client | ||
|
|
||
| 6) disputed -> resolved_client | ||
| - Trigger: admin_refund or adjudicated refund | ||
| - Effect: escrow_held -= amount; client.available += amount; milestone.status = 'refunded' or 'released' depending on outcome; create ledger entries and notification. | ||
| - Caller: admin action. | ||
|
|
||
| 7) disputed -> resolved_worker | ||
| 7. disputed -> resolved_worker | ||
|
|
||
| - Trigger: admin_force_release | ||
| - Effect: same as approved, but initiated by admin. | ||
| - Caller: admin action. | ||
|
|
||
| 8) disputed -> split | ||
| 8. disputed -> split | ||
|
|
||
| - Trigger: admin splits funds between parties using a dedicated SQL function. | ||
| - Effect: two ledger entries created; milestone.status = 'released' as per split outcome; wallet balances updated accordingly. | ||
| - Caller: admin action. | ||
|
|
||
| Cross-cutting considerations | ||
|
|
||
| - All writes go through SECURITY DEFINER functions; client code should never mutate status directly. | ||
| - RLS should constrain reads to the appropriate scope (owner, job participants, admin). | ||
| - Realtime subscriptions should be wired to milestone updates so both client and worker UIs reflect changes immediately. | ||
| - Idempotency: actions accept an Idempotency-Key; duplicate calls should be safely ignored. | ||
|
|
||
| Key Callers by Transition | ||
|
|
||
| - fund_escrow: client action (Phase 3/4 EFT) | ||
| - submit_milestone: worker action | ||
| - approve_milestone: client action | ||
| - dispute_milestone: client action | ||
| - auto_release_milestones: edge cron | ||
| - admin_*: admin role for overrides, refunds, releases, splits | ||
| - auto_release_milestones: pg_cron (scheduled in DB; see 20260518225500_schedule_auto_release_cron.sql) | ||
| - admin\_\*: admin role for overrides, refunds, releases, splits | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.