Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .gitignore
Binary file not shown.
10 changes: 9 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
# Pakka β€” Agent context

## Project

Open-source milestone-escrow reference implementation for Indian marketplaces. MIT-licensed, production-grade engineering, mock wallet by design.

## Stack

- Next.js 15 (App Router) + TypeScript strict + Tailwind v4 + shadcn/ui
- Supabase (Postgres + Auth + Realtime + Storage + Edge Functions)
- TanStack Query (server state) + Zustand (client state)
- Zod everywhere, react-hook-form for forms
- pnpm package manager

## Inviolable rules

1. NEVER mutate wallets/ledger from client code. Only via Postgres SECURITY DEFINER functions.
2. NEVER disable RLS, even temporarily. Every table has it.
3. ALL mutations are Server Actions in `src/app/_actions/`. No direct supabase calls from client components for writes.
4. ALL inputs validated with Zod on both client and server.
5. Money values formatted with `Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR', maximumFractionDigits: 0 })`.

## Phase status

- Phase 0 (foundations): done
- Phase 1 (schema + RLS + state machine SQL): done
- Phase 2 (phone OTP auth + role bifurcation + KYC): done @ commit 2bd5443
Expand All @@ -26,6 +30,7 @@ Open-source milestone-escrow reference implementation for Indian marketplaces. M
- Phase 5-8: pending

## Key paths

- `supabase/migrations/` β€” DO NOT MODIFY, only consume
- `src/lib/supabase/{client,server,middleware}.ts` β€” auth-aware Supabase clients
- `src/app/onboarding/` β€” Phase 2 KYC flows
Expand All @@ -34,18 +39,21 @@ Open-source milestone-escrow reference implementation for Indian marketplaces. M
- `middleware.ts` β€” route protection

## Conventions

- Mobile-first, max-width 640px centered on desktop
- Status badges via shared `<StatusBadge variant="..." />` component
- Loading skeleton + empty state + error boundary on every async page
- Optimistic UI on mutations, rollback toast on error
- Sentry capture inside every Server Action try/catch

## What "done" means for any prompt

- `pnpm typecheck` passes
- `pnpm lint` passes
- Git commit with conventional message format

## Out of scope reminders

- No real money β€” Demo Mode banner everywhere
- No Razorpay integration (documented in `/docs/production-swap.md` instead)
- No React Native β€” PWA + TWA wrap only
- No React Native β€” PWA + TWA wrap only
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ Branch naming: <type>/<scope>-<short-description>, kebab-case.

## Migrations

- Filename: supabase/migrations/<YYYYMMDDHHMMSS>_<verb_noun>.sql
- Filename: supabase/migrations/<YYYYMMDDHHMMSS>\_<verb_noun>.sql
- Timestamps in IST β€” no need to convert
- Idempotent where possible: create or replace function, do $$ … exception when duplicate_object then null end $$, add column if not exists
- One migration per logical change. Don't bundle unrelated schema work.
Expand Down Expand Up @@ -182,4 +182,4 @@ Effective when Phase 5.5 lands.
- One PR, one focus. No mixing unrelated migrations.
- Short, clear, step-by-step. Lead with the diff plan, not philosophy.
- No filler. Don't repeat what the diff shows. Don't say "I have made the changes" β€” show the changes.
- Verify before declaring done. pnpm typecheck && pnpm lint && pnpm test && pnpm build β€” all four green.
- Verify before declaring done. pnpm typecheck && pnpm lint && pnpm test && pnpm build β€” all four green.
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,15 @@ See [`docs/adr/0001-escrow-state-machine-in-postgres.md`](docs/adr/0001-escrow-s

> The live demo runs in Demo Mode. Log in at `/login` with any phone below. OTP is always **123456** β€” no SMS is sent.

| Role | Phone | Notes |
| --- | --- | --- |
| Client | `+91 98765 00001` | Priya β€” active jobs, escrow funded |
| Client | `+91 98765 00002` | Rohit β€” milestone awaiting review |
| Client | `+91 98765 00003` | Anjali β€” has an open dispute |
| Role | Phone | Notes |
| ------ | ----------------- | ----------------------------------------- |
| Client | `+91 98765 00001` | Priya β€” active jobs, escrow funded |
| Client | `+91 98765 00002` | Rohit β€” milestone awaiting review |
| Client | `+91 98765 00003` | Anjali β€” has an open dispute |
| Worker | `+91 98765 00011` | Ravi β€” gold tier, verified, job completed |
| Worker | `+91 98765 00012` | Suresh β€” submitted milestone |
| Worker | `+91 98765 00014` | Deepak β€” KYC pending (browse-only) |
| Admin | `+91 98765 00099` | Demo admin β€” use `/admin` route |
| Worker | `+91 98765 00012` | Suresh β€” submitted milestone |
| Worker | `+91 98765 00014` | Deepak β€” KYC pending (browse-only) |
| Admin | `+91 98765 00099` | Demo admin β€” use `/admin` route |

## Local development

Expand Down
1 change: 1 addition & 0 deletions adr/0001-auth.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
Title: ADR-0001: Auth flow design
Status: Proposed
Context:

- We need a secure phone OTP authentication flow for India-only onboarding. The flow must integrate with Supabase Auth and rely on server actions for writes.
Decision:
- Use phone OTP (with optional DEMO_MODE bypass for development). Server-side actions handle OTP send/verify. Role bifurcation happens after onboarding.
Expand Down
1 change: 1 addition & 0 deletions adr/0002-escrow.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
Title: ADR-0002: Escrow data & money flow model
Status: Proposed
Context:

- The escrow system is the core of Pakka. We require a robust, auditable model that ensures money moves only via SECURITY DEFINER functions and is zero-sum.
Decision:
- Describe the data model (tables, relationships) and the money flow (fund, release, refund) with explicit guards and RLS rationale. Outline high-level API surface (RPCs) and their scope.
Expand Down
1 change: 1 addition & 0 deletions adr/0003-dispute.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
Title: ADR-0003: Dispute flow
Status: Proposed
Context:

- Disputes are a critical path that requires admin intercession and robust audit trails while preserving user trust.
Decision:
- Outline dispute lifecycle states, integration with the escalation path, and how disputes affect ledger state and notifications.
Expand Down
6 changes: 4 additions & 2 deletions adr/0004-auto-release.md
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.
1 change: 1 addition & 0 deletions adr/0005-realtime-contract.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
Title: ADR-0005: Realtime contract boundaries
Status: Proposed
Context:

- The UI must reflect changes in escrow state in real-time for both clients and workers.
Decision:
- Define the channel boundaries, data privacy constraints, and RPC wrappers that surface updates via Supabase Realtime with proper token binding.
Expand Down
1 change: 1 addition & 0 deletions adr/0006-nav-shell.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
Title: ADR-0006: Persistent navigation shells
Status: Proposed
Context:

- Phase 4/6 UX requires persistent bottom navigation shells for clients and workers to improve navigation consistency.
Decision:
- Implement a persistent Shell layout per role with a top-level nav and a bottom bar that remains visible across routes.
Expand Down
1 change: 1 addition & 0 deletions adr/0007-rpc-pattern.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
Title: ADR-0007: RPC pattern for SQL-bound actions
Status: Proposed
Context:

- All mutations rely on SECURITY DEFINER RPCs; we need a robust, auditable pattern for RPC exposure from the app.
Decision:
- Define naming conventions, input validation with Zod, access guards, and transaction patterns for RPCs such as fund_escrow, submit_milestone, etc.
Expand Down
3 changes: 3 additions & 0 deletions adr/template.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ ADR Template
Title: <Short descriptive title>
Status: <Proposed|Accepted|Rejected|Deprecated>
Context:

- <Context that motivates this ADR>

Decision:

- <What is decided and the rationale>

Consequences:

- <Implications, trade-offs, and how it affects other components>
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ Two designs were considered:
- `accept_application(...)`
- `cancel_job(...)`


## Consequences

### Positive
Expand Down Expand Up @@ -111,4 +110,4 @@ When adding a new RPC under this ADR:
- `supabase/migrations/20260514164000_prevent_self_application.sql` β€” example of business rule in RPC
- `supabase/migrations/20260516125000_request_account_deletion_atomic.sql` β€” atomic idempotency pattern
- `supabase/migrations/20260516131000_secure_request_account_deletion_revoke_anon.sql` β€” defense-in-depth anon revoke pattern
- `supabase/sql/rollbacks/README.md` β€” rollback file naming and emergency-execution procedure
- `supabase/sql/rollbacks/README.md` β€” rollback file naming and emergency-execution procedure
2 changes: 1 addition & 1 deletion docs/adr/0003-auth-state-hygiene.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,4 @@ A future Phase 5 task is to encode these as ESLint rules (custom rule set under
- `src/lib/supabase/server.ts` β€” server-side client (separate file, not covered here)
- `src/components/account/sign-out-button.tsx` β€” example call site of the sign-out helper
- `src/components/account/delete-account-dialog.tsx` β€” example call site post-deletion
- ADR 0002 β€” RLS + SECURITY DEFINER as the only escrow path
- ADR 0002 β€” RLS + SECURITY DEFINER as the only escrow path
2 changes: 1 addition & 1 deletion docs/adr/0004-migration-discipline.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,4 @@ When reviewing a PR that adds a migration, confirm:
- Supabase MCP β€” `apply_migration`, `get_advisors`
- ADR 0001 β€” Escrow state machine in Postgres
- ADR 0002 β€” RLS + SECURITY DEFINER as the only escrow path
- ADR 0003 β€” Auth state hygiene
- ADR 0003 β€” Auth state hygiene
47 changes: 47 additions & 0 deletions docs/adr/0005-auto-release-milestones-modification.md
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.
5 changes: 5 additions & 0 deletions docs/data-model.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
Data Model β€” Pakka Escrow Marketplace

Overview

- Pakka is a two-sided marketplace for local trades where clients post jobs and fund milestones. Escrow funds move only via SECURITY DEFINER functions and every mutation is audited through RLS-protected rows with server-side authorization.
- The data model emphasizes a strong separation of concerns: profiles define users, wallets track balances, jobs describe work, milestones drive funding, and the ledger records all money movements in an append-only fashion.

Expand All @@ -19,6 +20,7 @@ Tables
- notifications (id uuid PK, recipient_id uuid REFERENCES profiles(id), type text, title text, body text, data jsonb, read_at timestamptz, created_at timestamptz DEFAULT now())

Indexes (suggested)

- CREATE INDEX ON wallets(profile_id);
- CREATE INDEX ON jobs(client_id);
- CREATE INDEX ON jobs(worker_id);
Expand All @@ -31,6 +33,7 @@ Indexes (suggested)
- CREATE INDEX ON notifications(recipient_id);

RLS and Security Intent

- profiles: user reads own row only; cross-user reads go through SECURITY DEFINER RPCs.
- wallets: read protected; writes only via SECURITY DEFINER functions; client cannot mutate directly.
- escrow_ledger: read-only for participants of the associated job via RPCs; insertions occur via Edge Functions (SECURITY DEFINER).
Expand All @@ -39,6 +42,7 @@ RLS and Security Intent
- All money-related fields and balances computed via server-side RPCs; no client-side balance math.

Security Definer Helpers (high level)

- is_admin() returns boolean
- fund_escrow(milestone_id)
- submit_milestone(milestone_id)
Expand All @@ -49,6 +53,7 @@ Security Definer Helpers (high level)
- auto_release_milestones()

Notes

- All money movement must be atomic and audited via escrow_ledger with zero-sum invariants.
- Use SELECT FOR UPDATE when mutating wallets in SECURITY DEFINER calls to avoid race conditions.
- RLS policies should be as restrictive as possible with admin bypass only where required.
40 changes: 27 additions & 13 deletions docs/state-machine.md
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
Comment thread
shaiksohelll marked this conversation as resolved.
Loading
Loading