Last updated: 2026-07-21
This file is the current focus. One section at a time. When the user says "next", check the finished part's boxes, append it to completed.md, and pull the next part from toBeDone.md.
Codebase restructure (2026-07-21) — plan: CODEBASE_RESTRUCTURE_PLAN.md
- Part 1 (Phase 1 — delete the trash) — deleted
.env.local.bak,.DS_Stores,MC Final.zip,metadata.json(orphan),MARKETING-STRATEGY.html,MC/(older dup ofMC Final/),business-plan/(stale draft still claiming Certn/CPIC — root plan with Stripe-Identity model is canonical); archivedhomepage-mockup.html,tasker-profile-preview.html,email-preview/,taskly.ca-audit/,Taskly hosting/,Taskly-Module-Audit.xlsxinto_archive/(index updated); moved_seed.mjs→scripts/seed-demo.mjs. Gate: tsc ✓, 487 tests ✓, build ✓ (Tailwind doc-scan survived). Fixed stale local install (jsdommissing →npm install). Note for Phase 3:npm run linthas 622 pre-existing errors across 178 files — must be triaged/baselined before lint lands in CI. - Part 2 (Phase 2 — doc taxonomy) — root shrunk from 69 md files to 5 + SECURITY.md (new). Engineering specs →
docs/(+docs/pricing/,docs/ai/taskly_ai_package/); business/marketing/legal/GTM (incl.MC Final/,Social bio/,brand/,growth/,research/) →business/; completed audits/changelogs + PRICING v1/v2/PROMPT →_archive/; 28 shipped status logs →_archive/status/. 28 code files' doc-citation comments repointed (scripted, lookbehind-safe —community/DEPLOY.mduntouched); README index + repo-layout table rewritten; CLAUDE.md corrected (Vitest + CI reality, migrations up to 155 w/ duplicate-number warning) + new doc homes; AGENTS.md repointed;_archive/README.mdledger extended; md link-checker run — all move-caused breaks in live docs fixed (154 pre-existing breaks in archived/MC-Final bundles left as-is). Gate: tsc ✓ · 487 tests ✓ · build ✓ · lint baseline unchanged (622 pre-existing). — NEXT: Part 3 - Part 3 (Phase 3 — guardrails) — deleted dead
.eslintrc.json; found the 622-error mystery: 567 were ESLint lintingcommunity/.nextbuild output (flat config ignores added). Real errors driven to 0:--fixremoved 47 dead imports (54 files), renameduseOpener→applyOpener(rules-of-hooks false positive), escaped 3 JSX entities; burn-down warnings (162) kept visible: no-unused-vars 83 + react-hooks v6 rules (set-state-in-effect 43, purity/refs/immutability) downgraded to warn with counts in eslint.config.mjs. New rule:no-restricted-importswarns on direct@supabase/supabase-jscreateClientoutsidelib/supabase/**(11 offenders visible; flip to error in Part 5). knip installed + configured → deleted 30 verified-dead files (TaskMiniMap×3, old provider cockpits, landing2 Hero variants, posters sections, ui viz primitives, use-mobile, lib/geo/fsa, lib/marketplace/rates, lib/net/fetch-json, lib/stripe/client, marketplace barrel…) and 3 unused deps (@hookform/resolvers, @stripe/react-stripe-js, @tailwindcss/typography);sw.jskept (runtime-registered, knip entry). New CI (test.yml): concurrency-cancel +lint+hygiene(knip files/deps/unlisted +scripts/check-migrations.mjsduplicate-number gate, grandfathering the 11 historical collisions, next free = 156) + aggregaterequiredjob. New scripts:typecheck/knip/check:migrations. tsconfig: ES2017→ES2022,allowJsdropped. Gate: tsc ✓ · 487 tests ✓ · lint 0 errors ✓ · knip clean ✓ · build ✓. - Part 4 (Phase 4 — migration ledger) — new
supabase/migrations/README.md: rules (one file per number, next free = 156, CI-enforced), the 11 collision pairs tabulated with MCP-tracked sides, 041 gap + 070b noted. Verified against PROD (supabase-taskly MCP ref confirmed =.env.localbefore querying):schema_migrationsholds only 24 rows — most migrations were dashboard-applied and untracked, so files + README are the only real ledger. Spot-check found REAL DRIFT:135_ops_alert_log.sqlnever applied (ops-alert dedup is a no-op) and070bonly partially applied —customer_profiles.reengaged_atmissing, so the win-back lane of/api/cron/engagement-nudgeserrors every run. Both files are idempotent; awaiting founder go-ahead to apply to prod (documented in the ledger). No code changed. - Part 5 (Phase 5 — code seams) —
lib/utils.ts→lib/utils/index.ts(115 importers untouched — resolution seam),lib/ai-quota-config.ts→lib/ai-quota/config.ts(3 importers),lib/welcome-options.ts→lib/welcome/options.ts(6 importers);lib/geocollision self-resolved in Part 3. Haversine deduped: browse-tasks re-exportsdistanceKmfromlib/geo. Component buckets: NOT merged blindly — they're 4 different features; renamedcomponents/tasker/→tasker-gate/(customer-side gate), movedpro/ProLink→provider/, keptproviders/(React context providers) +taskers/(recruiting page); bucket map documented in CLAUDE.md. All 11 directcreateClientoffenders migrated tocreateServiceRoleClient()(each already env-guards, so the wrapper's fallback never fires) and the lint rule flippedwarn→error(0 violations). Dev-route gating audited: all 7api/dev/*+app/dev/seed+api/_internal/price-cache-statsalready properly gated (NODE_ENV / CRON_SECRET constant-time) — no change needed. New conventions in CLAUDE.md: kebab-case new files (no mass renames — case-insensitive FS),_components/colocation, Supabase-client rule. Gate: tsc ✓ · 487 tests ✓ · lint 0 errors (151 warn) ✓ · knip ✓ · build ✓. - Part 6 (Phase 6 — decisions, founder-answered 2026-07-21) — Push: approved; Parts 1–6 pushed to main. Prod drift: both fixes APPLIED to prod via verified MCP + schema-verified (
ops_alert_logtable w/ RLS;customer_profiles.reengaged_at— win-back email lane un-broken); ledger updated. D1: business docs STAY inbusiness/in this repo (founder call; repo knowingly public). D4:vercel.jsondeleted (Railway prod; crons covered by the GitHub Actions dispatcher). Deferred to their own tasks: D2community/extraction to its own repo · D3 one-off scripts stay (knip + time will decide) · D5api/bookings/v2consolidation + funnel work (belongs to the dispatch pivot). RESTRUCTURE COMPLETE — plan: CODEBASE_RESTRUCTURE_PLAN.md.
Multi-country: 3-domain restoration — CODE + DB COMPLETE (2026-07-23); founder go-live stages 2–4 remain
Part 11 executed in 5 parts (commits e30f8a72 → 214b3469 + go-live DB): 3-region registry restored (.ai=global/USD gateway) · /choose-country gateway routing · CAD leak fixes (GA4 + 4 formatters) · isolation guards (write guard, wrong-domain 301s, RLS scope) · live proof suite. Migrations 156+157 APPLIED to prod; isolation proof 7/7 green against the real DB. Remaining = founder dashboards: Stage 2 allowlists → Stage 3 REGION_ROUTING_LIVE flip (+ comms) → Stage 4 MULTI_REGION_LIVE + Search Console (closes the still-open duplicate-content window). Runbook: MULTI_COUNTRY_LAUNCH.md.
Founder confirmed the original 3-domain model (.ai=global/USD gateway · .in=India · .ca=Canada), reversing the 2026-07-20 2-domain pivot (cause was Railway Hobby's 2-domain cap; Pro=20 unblocks). Founder rule: strict region isolation — tasks never cross domains (plan §5 / tracker 11f: Part 10 already scoped all cross-user reads; Part 11 adds cross-region write guards, wrong-domain 301s, RLS belt-and-suspenders, isolation proof tests). Plan written + researched: MULTI_COUNTRY_PLAN.md (v2 consolidated: model §1, isolation §5, payments/tax §6, gateway UX §7, execution §8, add-a-country §9, local-dev §10) · execution tracker MULTI_COUNTRY_TASKS.md Part 11. Key research deltas: RBI PA Master Direction Sep-2025 confirms ₹40L Route gate → day-1 India = Razorpay standard checkout + RazorpayX payouts (pay-on-completion rail); Stripe Connect UAE = trade-licensed businesses only (future-country caveat); *.localhost subdomains for side-by-side local region tabs.
Goal: the Standard price a poster sees = the price they'd themselves write on Kijiji/FB/
Nextdoor; Premium = the insured/pro end; Budget = floor. Implemented in
lib/marketplace/price-table.ts (PRICE_TABLE_VERSION 7):
-
POSTER_FACTORcalibration (sourced in-file): cleaning 0.80 (independents $20-45/hr vs companies $35-70 — rates.ts verified), handyman/maintenance 0.75 (Kijiji ~$40-60/hr vs pro $60-120 + company minimums), assembly/mounting/appliances 0.85, moving/snow/auto 0.85, plumbing/electrical 0.80 (licensed minCharge floors hold). Already-independent categories (tutoring, pet, lawn, laundry…) untouched at 1.0. - Transform: Standard = pro-median × F · Budget = min(row low, Standard × 0.8, ≥minCharge) · Premium = pro high unchanged. Composes with urgency ×1.25 + learned calibration (mig 078). Bucket fallback restricted to hourly rows (bug caught in test: cleaning factor leaked onto laundry via bucket).
- Spot checks: deep 2BR/2bath $275 std / $430 prem (pro median $345 preserved in band) · TV mount $145 · sofa assembly $70 · 3h handyman $205 · 1BR move $455.
- Tests: price-table suite updated + new v7 invariants block → 51/51; full suite 335 pass;
tsc + lint clean.
research/seed-task-bank-500.csvgained asourcecolumn (rate-card citations per category).
Poster ⇄ tasker private counter-offers with notes, on top of marketplace bidding. Full-arcade layer (XP/levels/streaks/badges, gap meter, meet-in-the-middle, final-offer flag, confetti). Product decisions locked with the founder: counter only taskers who already bid, no expiry timer, full arcade feel.
- Migration
149_offer_negotiation.sql—task_offer_movesledger (one row per move; one livependingper lane via partial unique index) +negotiator_stats(arcade XP/streak/badges). RLS mirrorstask_messages(two-party read; propose-as-yourself insert; NO client update/delete — status transitions are service-role only). Realtime publication added.⚠️ Must be applied to prod Supabase (dashboard) before the feature works — NOT applied via MCP (wrong project). -
lib/marketplace/negotiation.ts(pure/isomorphic) — move types,computeStatelane machine, haggle math (meetInMiddleCents,gapFraction), arcade helpers (dealXp,levelForXp,negotiatorTitle,BADGES,badgesForDeal). -
lib/marketplace/negotiation.server.ts—createMove/respondMove(accept/decline/withdraw) /getLaneState/ stats. Note moderation viapreCheck(contact-strip). Acceptance freezes the agreed price ontotask_bids.amount_centsvia service-role only (migration 056 guard), then the unchanged accept-and-pay → escrow flow hires at it. No new money path. - API:
GET /api/tasks/[id]/negotiation,POST /api/tasks/[id]/offer-moves,POST /api/tasks/[id]/offer-moves/respond,GET /api/negotiator/stats. Rate-limited; role derived server-side. - Notifications:
negotiation_offer/negotiation_accepted/negotiation_declinedadded to the union + PUSH_TYPES, role-correct deep links. - UI: shared
components/marketplace/NegotiationPanel.tsx(arcade board) +hooks/use-negotiation.ts(realtime) wired into the posterOffersDrawer("Haggle" per bid; deal → chains intohire()/escrow) and the taskerMyOffersClient("Haggle" on active offers). -
tscclean ·npm test316 pass (18 new intests/marketplace/negotiation.test.ts) · 0 lint errors. - Apply migration 149 to prod Supabase · commit + push (awaiting go-ahead) · smoke-test the live haggle round-trip in-browser.
Tracker: POSTER_APP_BUILD.md — the complete authenticated Poster experience
(post → choose → track → pay → confirm → review → repeat) on the (customer)/app shell, poster-first with
UC hidden. Built one Part at a time; user says "next" to advance. Reviewed via two canvases (blueprint + UI mockup).
- Part 1 — 5-tab app shell. BottomNav + desktop TopAppBar + drawer → Home · Tasks · Community · Messages ·
Payments; Account moved to the header; new
/app/community+/app/paymentsplaceholder routes. tsc/build/tests green. - Part 2 — Home priority dashboard.
PosterNextActions— a "Needs your attention" strip driven by real posts + bookings + unread, priority-ordered (blocked → completion → offers → messages), self-hiding when quiet. - Part 3 — Tasks hub + submit hand-off. Live search + persistent "Post a task" on the My Tasks hub; submit hand-off already covered by PostedSuccess. (No Drafts/Scheduled tabs — data-dependent, deliberate skip.)
- Part 4 — Offers compare + award. Already built (OffersDrawer: compare/sort/shortlist/profile/award→escrow).
Verified, not rebuilt. Wired the dashboard "Review offers" →
#offersdeep-link so it lands on the decision. - Part 5 — Lifecycle task detail + active task. Active-task detail already rich; fixed the post→booking
seam — exposed
MyPost.bookingIdand deep-linked "View active task" → the specific/app/bookings/[id]. - Part 6 — Messages structured actions. Task-status header + a status-driven action bar beside the chat (Approve & release / Confirm new time / Report / Review), linking to the booking detail's money flow.
- Part 7 — Payments tab. Real money centre — Held-vs-Paid overview + task-tied transactions with plain-language states + receipts, from useBookings (no new API). Replaced the placeholder.
- Part 8 — Completion & review loop. Pre-review chain already existed; closed the loop by adding "Post a similar task" (prefilled from the original task) to the review-submitted state.
- Part 9 — Community. Lightweight read surface — 4 expandable Taskly Guides + 6 task-idea cards that deep-link into a prefilled post. Not the tasker lounge; no fake feed.
- Part 10 — Repeat-post everywhere + polish. "Post a similar task" on the completed booking + a
poster_dashboard_viewedfire. ALL 10 PARTS DONE ✅ — Poster app functionally whole. Awaiting "commit".
Tracker: COMMUNITY_BUILD.md — live ephemeral domain chat
rooms for approved Taskers, a separate app at tasklyapp.ai/community. Plan:
/TASKER_COMMUNITY_PLAN.md. Building one Part at a time
(user says "next" to advance). ALL 10 PARTS DONE 🎉 — standalone community/ app
(scaffold + branding), Supabase auth + approved-tasker gate + signed WS ticket,
migration 077 + registry API, the WebSocket Gateway (server/, smoke 10/10), live
chat UI (dark lounge), Redis backplane/scaling, moderation + rate limiting, admin
broadcast + console, deploy runbook (community/DEPLOY.md), and fine-grained
subcategory rooms (task_skills rollup, department-grouped rail). Remaining = ops/QA
only: apply migration 077, create Railway services + DNS, live browser + multi-instance
Redis QA. Nothing committed yet (per direct-push-when-asked rule).
Tracker: DESIGN_SYSTEM_REVAMP.md — make one central design
system (tokens + components/ui primitives), applied consistently everywhere;
adopt the UC booking-flow typography/feel app-wide; fix readability (text must be
properly dark, ≥4.5:1); round buttons + corners. Shipped in parts; user says "next".
- Part 1 (done, 2026-06-14): Foundation — darkened readability tokens in
globals.css(body #3A3A3A,mute #5E5E5E), canonical roundedButtonprimitive (components/ui/button.tsx), master plan doc. - Part 2 (done, 2026-06-14): Core primitives in
components/ui/*+ barrel — Card, Eyebrow, Heading, SectionHeading, Chip, Badge, Input, Textarea, Field. Build green. - Part 3a (done, 2026-06-14): Customer app — readability sweep across ALL
customer screens (faded
text-*/40,/50,warm-gray-light→ readable) + profile hub rounded & on primitives (Card-style menu groups, Button sign-out)- sharp-corner fixes. Build green.
- Part 3b (done, 2026-06-14): Customer shared chrome rounded (PageHeader, BookingCard, Sidebar, BottomNav) → every customer screen; my-posts fully rounded. Build green. (3c deeper per-page customer refactor deferred to Part 9.)
- Part 4 (done, 2026-06-14): Provider workspace — readability sweep + rounded ProviderShell nav, jobs page, and provider dashboard main blocks. Build green.
- Part 5 (done, 2026-06-14): Marketplace — readability sweep (post-task, tasks, marketplace comps); post-task wizard already rounded; postModals (edit/delete) fully rounded; OffersDrawer already rounded; public task page clean. Build green.
- Part 6 (done, 2026-06-14): Auth/onboarding — readability sweep + login fully rounded (buttons, tab control, Google). Onboarding/verify/training/ welcome/join already rounded. Build green.
- Part 7 (done, 2026-06-14): Public/marketing — readability sweep across all
marketing components (landing2, NavBar, Footer, HowItWorks, hero sections, pro
- u profiles, advantages). Already rounded (recent brand build). Build green.
- Part 8 (done, 2026-06-14): Admin — readability sweep + AdminShell nav, card pattern, and common action buttons rounded across admin pages. Build green.
- Part 9 (done, 2026-06-14): Final audit & polish — full-codebase sweep: ZERO faded-text violations anywhere, ZERO rounded-none, and the last 32 sharp buttons rounded (guarded pass). Build green. 🎉 Design-system revamp complete — all 9 parts shipped.
Tracker: MANAGER_GAP_AUDIT.md — 10 gaps the manager's review found in the live app. Working through them part by part, highest-impact first; update that file's status column as each lands.
Order: G1 Referrals → G3 bid-placed notification → G2 escrow cron → G7 promo codes → G6 tips → G5 notification prefs → G4 escrow-bypass accept → G8 demo gate → G9 dead UI → G10 pre-launch flags.
- G1 — Referrals loop ✅ 2026-06-12 —
/r/[code]capture cookie +/api/referral/claimsetsreferred_by+<ReferralClaim/>in the/applayout. Verified mechanics + real-code lookup. Credit accrual already worked; loop now closed. - G3 — Bid-placed notification to poster ✅ 2026-06-12 —
new_offernotification on a new bid (edits don't re-notify). - G2 — Escrow auto-release + retry cron ✅ 2026-06-12 — single
/api/cron/rundispatcher (all 6 jobs) on the existing GitHub Actions 15-min cron. Ops: confirmCRON_SECRETin Railway + GH secret. See CRON_SETUP.md. - G7 — Promo codes ✅ 2026-06-12 —
promo_codestable (migration 052) + validate endpoint + server re-validation in/api/bookings/v2+ review-page UI. Verified all cases. Codes:WELCOME10,TASKLY15. - G6 — Tips charged ✅ 2026-06-12 —
/api/tips/chargeoff-session PI on the saved card + 100% Connect payout +tip_receivednotification (migration 053). Review page wired. Operator: verify a charge in Stripe test mode. - G5 — Notification preferences honored ✅ 2026-06-12 —
createNotificationskips non-critical in-app notifications the user opted out of (per-event +pushchannel); critical types always send.emailChannelEnabledgates the confirmation email. - G8 — Demo-data launch gate ✅ 2026-06-12 —
showDemoSeed()flag (NEXT_PUBLIC_SHOW_DEMO_SEED) gates the directory, the feed, and the/proseed fallback. Set=falseat launch. - G9 — Stubs / dead UI ✅ 2026-06-12 — all 6: #16 dead Call/Attach + false "encrypted"→"Private & secure"; #8 training persists; #1 admin "Soon"; #11 (Phase 3); #4 task views (migration 054); #6 saved-card list + remove via Stripe. (Also fixed 2 missing notification-type union entries from a teammate rebase.)
- G4 — Escrow-bypass accept path ✅ 2026-06-12 — Browse-Tasks owner-accept now routes to the My-Posts escrow flow; the bypass route
/accept-bidis neutralized (409). No real task assigns without an escrow hold. - G10 — Pre-launch flags ✅ 2026-06-12 — mock OTP was dead code (wizard uses real Supabase OTP via /login) → removed AuthPanel/OTPInput/mock adapter; noindex on post-task + /taskers now gated by
indexWipPages()(NEXT_PUBLIC_INDEX_WIP_PAGES).
🎉 Manager Gap Audit complete — all 10 gaps closed. See MANAGER_GAP_AUDIT.md for the operator env/verify follow-ups.
Why: profile new members (occupation, about, interests, skills) to tailor recommendations + promos, and spot skilled people to recruit as taskers.
- Migration
055(applied):customer_profiles+=occupation, about, interests[], skills[], open_to_earning, profile_completed_at. /onboarding/welcome— premium stepped flow (occupation → about + interests → skills + "open to earning" → done), single-viewport, skippable. Done step shows a Become-a-Tasker upsell when skilled/open./api/welcome/enhance— AI polishes the blurb + suggests interests/skills from occupation (verified: "software engineer" → skillsCoding, Tech Setup)./api/welcome— saves profiling data + setsuser_metadata.profile_completed.- Gate:
/applayout routes members without the flag here once (finish OR skip clears it). Existing members get it once too. - Edit from profile (2026-06-13):
/app/profile/about— single-form editor (same data + AI enhance) for skippers / updates, linked as "About you" under Profile → Account./api/welcomegained a GET; options shared vialib/welcome-options.ts.
Started: 2026-06-07 Goal: Move Taskly from "one user = one role (customer XOR provider XOR admin)" to a capability model — every member is a customer; "tasker" is an in-app capability; the tasker surface is a strict superset of the customer surface; admin stays a separate console. Full design, flows (mermaid), RLS rewrites, and the phased plan live in ARCHITECTURE.md.
Build rule: dependency order, each step non-breaking on its own. DB migrations are written in-repo and applied by the operator via the Supabase dashboard after confirming the project ref matches .env.local (the MCP points at a different project — never apply blindly). User says "next" to advance.
Roadmap (see toBeDone.md for detail)
- Part 1 — Capability data model (migration
039, additive) - Part 2 — RLS rewrite + self-deal guards (migration
040) - Part 3 — Capability plumbing (
getCapabilities(),/api/me/context, capability gates) - Part 4 — Routing & gating (drop mutual redirects;
/app= any member; tasker layout = capability) - Part 5 — Unified member shell + nav — Get help⇄Earn toggle ✅ (5a+5b); route move
/provider→/app/taskerdeferred as cosmetic (5c) - Part 6 — Become-a-tasker activation (canonical
/onboarding/taskerdoor, both entry paths) - Part 7 — Conversion CTAs + capability gate (offer gate + "Start earning" upsell)
- Part 8 — Marketplace money (convert-to-booking) — Stages 1–4 ✅ feature-complete: accept→pay→escrow→assign→complete→release (poster/3-day cron) + disputes/refunds/cancel. Ops: schedule auto-release cron on Railway.
- Part 8a — Bidirectional reviews ✅ 2026-06-07. Migration
045(applied):customer_profiles.rating_avg/ratings_count+ direction-awarerecompute_customer_rating()trigger + backfill +get_customer_rating(uuid)RPC (authenticated). Tasker review page/provider/jobs/[id]/review+ "Rate the customer" CTA; customer rating shown in the tasker job detail; own rating + bookings count on/app/profile; fixed the customer review page's hardcoded provider placeholder. - Part 9 — Cleanup, role collapse & hardening (migration
042) ← just built & applied
Part 1 — Capability data model (migration 039) ✅ APPLIED & verified on prod (taskly.ca · nfhflltrbsyraeibvbiw)
File: supabase/migrations/039_capability_model_additive.sql
What it does (additive only — no role values change, safe to apply anytime):
- Capability helpers
is_admin()/is_tasker()/is_tasker_or_pending()(SECURITY DEFINER,search_pathpinned, granted toauthenticated). A tasker is derived from an approvedprovider_profilesrow, so these already resolve correctly for today's providers regardless ofrole. handle_new_user()rewrite: every new member now also gets acustomer_profilesrow (customer base is universal); still createsprovider_profilesfor the legacy/join-authprovider signup until that door is retired (Part 6); and stops honoringrole='admin'from client signup metadata — a hardening fix (a self-service signup could previously landrole='admin'viaraw_user_meta_data).- Backfill: inserts a
customer_profilesrow for every existing profile that lacks one (incl. current providers → they gain the customer base). - Idempotent (
create or replace+on conflict do nothing).
Verification: SQL checked against the live 001 schema — customer_profiles/provider_profiles accept insert (id) (all other columns nullable/defaulted); the on_auth_user_created trigger already targets handle_new_user(), so replacing the function is sufficient. (Cannot run against the DB from here — no app DB creds; operator applies.)
Applied 2026-06-07 via the Supabase Management API (project-scoped MCP token, verified ref). Verified live:
is_admin()/is_tasker()/is_tasker_or_pending()exist, allSECURITY DEFINER.- Backfill closed the gap: profiles=27, customer_profiles 19 → 27, missing_customer_base=0 (the 8 provider-only accounts gained the customer base).
handle_new_user()now has the admin-from-metadata guard + universal customer base.- Security advisors: 039 added zero new findings.
Advisor baseline (pre-existing, fold into Part 2): rls_references_user_metadata ERRORs on bookings ("Providers view available jobs", "Providers claim jobs", "Admins full access bookings"), disputes, booking_timeline (spoofable user_metadata.role gates); rls_disabled_in_public ERRORs on service_categories/service_pricing/service_addons/subscription_plans (no RLS on prod). Part 2 replaces the user_metadata gates with the helpers and enables RLS on the catalog tables.
MCP: project-scoped supabase-taskly server configured in gitignored .mcp.json (pinned to nfhflltrbsyraeibvbiw); token verified working. Tools (mcp__supabase-taskly__*) activate on extension reload; until then migrations are applied via the verified Management API.
File: supabase/migrations/040_capability_rls_rewrite.sql (applied via MCP apply_migration, recorded in migration history).
Inspected the live policy set first, then rewrote authorization to the capability helpers and fixed the pre-existing advisor ERRORs:
- De-spoofed admin — 8 policies that gated on client-spoofable
user_metadata.role(bookings,profiles,disputes,provider_profiles,customer_profiles,booking_checklists,booking_timeline×2) → nowis_admin()(readsprofiles.role). This closed a real escalation surface: a user couldupdateUser({data:{role:'admin'}})and get admin RLS access via direct PostgREST. - Tasker pool — dropped the 2 duplicate spoofable
bookingsprovider policies + rewrote the 2profiles.roleones into a singleis_tasker()pair on the unassigned pool;addressesprovider read →is_tasker(). - Self-deal guards —
bookings_provider_not_customerCHECK (a member can't be dispatched their own booking; 0 existing violations);task_bidsinsert now requiresis_tasker()AND poster ≠ bidder (0 existing bids). - Future-proofed the public tasker-profile read to key off an approved
provider_profilesrow (survives the Phase-4 role collapse). - Catalog RLS — enabled RLS + public-read on
service_categories/service_pricing/service_addons/subscription_plans(were RLS-OFF on prod; reads stay open for the wizard, writes denied).
Verified live: get_advisors(security) → 0 ERRORs (was 15); structural check → 0 remaining user_metadata policies, 5 new capability policies, 4 catalog tables RLS-on, 1 self-deal constraint. Remaining advisor items are pre-existing/expected WARNs (the security_definer_function_executable ones are inherent to RLS helper functions — anon/authenticated must execute them during policy eval; they return a safe boolean) + INFO + an auth setting; none introduced by 040.
App-side mirror of the DB capability model so routes gate on the capability, not role='provider' (which Phase 4 removes).
- lib/api/auth.ts: added
getTaskerStatus(),isTasker()(approved → can earn),isTaskerOrPending()(approved|pending → tasker access for browsing/management),getCapabilities()→{ role, isAdmin, isMember, isTasker, taskerStatus }. Mirrors the DBis_tasker()/is_admin(). - GET /api/me/context: the shell's capability source (
{ authed, role, isMember, isAdmin, isTasker, taskerStatus },no-store) — for Parts 4–7 nav/gating. - Swapped 7 provider routes (
public-profile,profile,portfolio/upload,jobs,linkedin/start,linkedin/callback,stripe/connect) fromgetProfileRole(...) !== 'provider'→!(await isTaskerOrPending(...)). Now survives the role collapse; also tightens (suspended/rejected taskers blocked). Earning stays gated by RLS (is_tasker()= approved) as defense-in-depth.
Note: the audit already moved all admin/provider API auth onto profiles.role (not user_metadata); remaining user_metadata references are benign display-name fallbacks. Admin route gates stay role-based (admin role is preserved; not affected by the collapse). profile/route.ts role==='customer' branch + feedback role label flagged for Part 9 review (post-collapse semantics).
The change that actually lets one account use both surfaces.
- app/(customer)/app/layout.tsx: removed the
role==='provider' → /providerbounce. A tasker now gets the customer surface at/app(the shared base). Onlyadmin → /adminremains. (Legacy self-heal left intact; Part 9 removes it.) - app/(provider)/provider/layout.tsx: gating is now capability-based (
getTaskerStatus()), notrole==='provider'— survives the Phase-4 collapse. admin→/admin; non-tasker (status none/suspended/rejected) →/app; pending/approved continue; unfinished onboarding →/onboarding. Title relabeled "Tasker Dashboard". - lib/supabase/middleware.ts: login post-auth redirect collapsed to admin→
/admin, everyone else→/app(deep-links allowed under/app,/book,/provider; layouts enforce capability). Removed the provider auto-bounce. The metadata-based unpaid-tasker →/onboarding/verifyguard is untouched.
/app, and until Part 5's unified nav/toggle ships there's no in-UI link from /app → /provider (URL still works). Land Part 5 before deploying so existing taskers aren't stranded (batching the commit with Part 5 satisfies this).
Verified: tsc + lint clean. Needs a manual nav smoke check once Part 5's shell lands.
Resolves the Part 4 interim caveat: taskers can now round-trip between the two surfaces.
- hooks/use-capabilities.ts: client hook backed by
GET /api/me/context(module-cached + in-flight dedupe) →{ isTasker, canEarn, isAdmin, taskerStatus }. - Customer Sidebar + MobileDrawer: show a "Switch to earning" →
/providerrow whencanEarn(approved or pending tasker). Non-taskers see nothing yet (the "Start earning" upsell is Part 7); admins never see it. - ProviderShell: added "Customer app" →
/appin the utility nav (a tasker is always a member).
Verified: tsc + lint clean. The interim "stranded tasker" risk from Part 4 is gone — there's now a link both ways.
The headline of Part 5 — a real context switch, replacing 5a's rudimentary links.
- components/app/ContextToggle.tsx: shared segmented control (Get help →
/app· Earn →/provider), active context derived from the path, renders only whencanEarn.variant='full'(sidebar pill) +variant='compact'(icon button for collapsed rails / mobile bars). - Wired into all four nav surfaces: customer Sidebar (top of nav; compact when collapsed) + MobileDrawer; provider ProviderShell desktop sidebar + mobile top bar (compact). Removed the 5a placeholder links. Closed the provider-mobile gap (taskers can now switch to the customer app from a phone).
Scoping call (deferred — see 5c): the physical /provider/* → /app/tasker/* route move + ProviderShell retirement is deferred. It's a large, mostly-cosmetic refactor (relocating every route file, rewriting every /provider/... link, adding redirects) with no new capability — high blast radius against the "unbreakable" goal. The product outcome (one account, superset, toggle between contexts) is fully delivered without it; the URLs just differ (/provider/* vs /app/tasker/*). Tracked as Part 5c in toBeDone for a later careful pass.
Verified: tsc + lint clean. Manual nav smoke check recommended once the batch is run.
app/onboarding/tasker/page.tsx — the canonical "Become a tasker" door, one destination for both entry paths, capability-gated:
- Anonymous →
/login?next=/onboarding/tasker(preservingreturnTo), back here after auth. - Admin →
/admin(admins don't become taskers). Already-approved tasker →/provider. - Signed-in customer (in-app upgrade) / pending tasker →
/onboarding— the existing onboarding flow already creates theprovider_profilesrow + flips role + runs bg-check/Stripe Connect (verified atonboarding/page.tsx~L2370 andverify/page.tsx~L760, both gated on not-admin). So a member becomes a tasker with no second account; the customer base stays intact.
This is the destination Part 7's "Start earning" CTAs point to, and what the signup next= carries.
Deferred (still functional, cleanup later — Part 9-ish): retire /join-auth + signup_intent + the role-select fork (the legacy provider signup door still works post-039). returnTo consumption (resume the originating action after activation) is owned by Part 7's client capability gate via sessionStorage — onboarding isn't instant, so a server round-trip cookie added little.
Verified: tsc + lint clean.
The growth loop — a non-tasker can now discover, then convert.
- hooks/use-require-tasker.ts:
useRequireTasker()→requireTasker(returnTo)gates a tasker-only action (gates on approvedisTasker, matching RLS); non-taskers get routed to/onboarding/tasker?returnTo=…with returnTo also stashed in sessionStorage (takeTaskerReturnTo()reads+clears it for the resume). - Preview-then-gate on the marketplace: TaskDetail "Make an offer" — anyone can browse + read offers, but tapping offer as a non-tasker routes into activation instead of opening the composer (and would otherwise 403 at the RLS bid policy).
- "Start earning" upsell: components/app/StartEarningCard.tsx — shown only to pure customers (never pending/approved taskers or admins), placed on the customer dashboard below the hero →
/onboarding/tasker. - Drive-by: fixed a pre-existing lint nit (
useMemo(buildSearchIndex,…)) in the dashboard. (A separate pre-existingset-state-in-effectat page.tsx:835 is in unrelated search code, build-ignored viaeslint.ignoreDuringBuilds— left alone.)
Deferred polish (full CTA catalog): profile-row entry, empty-state + post-completion nudges, "X tasks near you" demand stat, dismissal/snooze, and the returnTo resume consumption after activation. The headline loop (preview→gate→activate) + primary upsell are done.
Verified: tsc clean; new Part 7 files lint clean.
The final step — the role model is now fully capability-based.
- Removed every
role='provider'write (so the collapse sticks): onboarding (page + verify) now only upsertprovider_profiles(no role change); auth/callback defaults new users to customer + droppedsignup_intent; the customer-layout self-heal (which re-createdrole='provider') is gone. - Retired the legacy doors: /join-auth → redirects to
/onboarding/tasker; /onboarding/role-select → redirects to/app(no more signup role fork). - Migration 042 applied on prod:
role='provider' → 'customer'(12 → 0, verified all had aprovider_profilesrow first);handle_new_user()simplified (always customer base; tasker rows via activation; admins out-of-band); addedidx_notifications_user_created. - Verified live: providers-by-role 0; customers 26 / admins 1; taskers intact — 1 approved + 11 pending via
provider_profiles(is_tasker reads the row, not the role); trigger no longer makes providers; index present. Indexes for feed/bids/bookings/messages already existed (only the notifications-list gap was filled).
Harmless dead branches left for opportunistic cleanup (not breaking — role==='provider' is now always false): landing redirects (app/page, instahelp), login post-auth, admin layout bounce, feedback label, callback provider branch; dev/seed-test-users still seeds providers (dev-only, NODE_ENV-gated).
Verified: tsc + lint clean (only pre-existing <img> warnings).
One auth → everyone's a customer → become a tasker in-app via CTAs/gate → tasker = superset with a Get help⇄Earn toggle → admin separate. DB · RLS · server · routing · UI · activation · conversion · role collapse — all shipped & committed on main (dd2d632 spec/migrations → 631c5b3 parts 3–5 → 7a9bd03 parts 6–7 → 3154f50 part 9). Migrations 039/040/042 applied on prod.
Maintenance pass (2026-06-07, pending commit): dead-branch cleanup — removed the now-dead role==='provider' reads in app/page, instahelp, login, admin layout, and the auth/callback redirect helper (taskers resolve to /app); added the StartEarningCard upsell to the browse-tasks feed (high-intent). tsc + lint clean.
Deferred / backlog: Part 5c (cosmetic /provider→/app/tasker route move — see below). Part 7 profile-row entry ✅ done; the rest of the CTA-catalog polish (nudges, snooze, "X near you" demand stat, returnTo-resume) is intentionally not built — marketing polish, and returnTo-resume is moot since tasker approval is async (not instant). 8a bidirectional reviews ✅ done. See toBeDone.md.
After the sprint roadmap, swept the remaining build work so only testing + the cron-scheduling op remain:
- Part 8a — bidirectional reviews ✅ (migration
045applied; tasker review page + CTA; ratings shown; profile stats; fixed the customer review page's hardcoded provider placeholder). - Part 7 — profile-row entry ✅ capability-aware "Earn" row on
/app/profile(Start earning / Finish setup / Tasker dashboard). - Dead-branch / post-collapse cleanup ✅ — fixed
dropoff-emails(was querying the retiredrole='provider'; now targetsprovider_profiles),feedbacktriage label (nowtaskerviais_tasker()instead of the dead role), and corrected misleading "set role='provider'" comments in onboarding (code was already capability-correct).dev/seed-test-usersstill seedsrole='provider'— noted for the testing phase (dev-only, NODE_ENV-gated; produces test users that aren't recognized as taskers). - Part 5c — route move still deferred (recommend leaving it; high blast radius, zero functional gain).
Closed the remaining buildable items so only operator/launch tasks remain:
dev/seed-test-users✅ now capability-correct: the test tasker is a member (role='customer') with a customer base + an approvedprovider_profilesrow (whatis_tasker()reads), instead of the retiredrole='provider'. (Note: the seed can't create a real Stripe Connect account, so a seeded tasker's payout still needs real Connect onboarding before a release transfer succeeds.)- Part 7 demand stat ✅
StartEarningCardnow shows a live "N open tasks waiting" count (marketplace USP signal) with graceful fallback to generic copy. - Part 7 — nudges / snooze / returnTo-resume: intentionally NOT built — speculative marketing UI without placement/design direction, and returnTo-resume is moot (tasker approval is async, not instant). Documented, not churned.
- Part 5c route move: the ONE remaining deferred code item. Quantified blast radius: 17 page files to relocate + ~46 files with
/providerreferences, with a real trap (many are/api/provider/*API routes +/providers/matchthat must NOT move). Pure-cosmetic URL change, zero functional gain → deliberately left as a dedicated post-launch pass to protect the "unbreakable" goal.
Net: building is complete for launch. Everything remaining is operator/verification (cron schedule, Stripe live config, env vars, tasker Connect onboarding, real-card smoke test, AI evals).
Model DECIDED + locked in ARCHITECTURE.md §16: commission take-rate + escrow. Poster pays exactly the agreed deal $X; platform commission 20% flat (HST-inclusive); tasker payout 80%. Hold = capture $X up front to the platform balance (NOT pre-auth — auth holds expire ~7 days; tasks can be weeks out), transfer 80% to the tasker's Connect account on release (Stripe separate charges & transfers; ≤90-day hold). v1 params: 3-day auto-release after the tasker marks complete; admin-mediated disputes (release / full refund / partial split); full refund refunds the commission too; free cancellation pre-work; chargebacks platform-liable (ToS).
✅ Foundation + Stage 2 server:
-
Fee engine — fees.ts commission model (
commissionCents/taskerPayoutCents/marketplaceBookingAmounts). Single source of money truth. -
Design locked — model, escrow state machine, params, 5 stages in ARCHITECTURE.md §16.
-
Stage 1 — schema ✅ migration 043 (prod).
-
Stage 2a — server ✅ convert.ts + accept-and-pay (creates pending marketplace booking, idempotent) + /api/stripe/booking escrow-aware (
capture_method:'automatic'+purpose:marketplace_escrow). -
Stage 2b — webhook finalize ✅ migration 044
finalize_marketplace_booking(booking_id)(atomic accept-bid+assign, service_role-only — verified anon/authed locked out) + webhook extended: onpayment_intent.succeededwithpurpose:marketplace_escrow→ capture (idempotent) → finalize RPC → notify tasker. Resolve-before-release holds. -
Stage 2c — UI + read null-safety ✅ 2026-06-07. Hire happens in the poster's own OffersDrawer (My posts → Offers), not TaskDetail (that's the tasker's browse view). "Hire & pay" →
accept-and-pay(pending booking) → escrow payment sheet mounting StripePayment with a newescrowprop (charged-now-held-in-escrow copy + "Hold in escrow" button) → confirm → webhook finalizes. Read null-safety:/api/bookingsGET +/api/provider/jobs(upcoming/completed) now join the linked task viatask:tasks!bookings_task_id_fkey; the available pool stays!inner(marketplace isn't dispatched). Customer bookings list + detail and tasker jobs list + detail all fall back to task title / location / "Marketplace" / "Remote" when curated fields are null; tasker detail shows a Task brief from the task description. tsc + lint clean. -
Stage 3 — release ✅ 2026-06-07. No migration (reused existing transfer columns + free-text notification types). Shared helper release.ts
releaseMarketplaceEscrow()transfers the tasker's 80% from the held escrow to their Connect account (source_transaction= captured charge; per-booking idempotency key so a poster-click racing the cron can't double-pay) → markspaid; on transfer failure it stayscompletedfor retry. Three triggers: (1) the tasker'scompleteaction now branches onbooking_type— marketplace skips the curated auto-capture and instead notifies the poster (confirm_release) + opens the window; (2) poster confirm route /api/bookings/[id]/release (user-auth → service-role release); (3) 3-day auto-release cron /api/cron/auto-release-escrow (BearerCRON_SECRET+ GET trigger; also the marketplace transfer-retry path). retry-transfers now skips marketplace (auto-release owns it + flips status). Poster UI: a "Confirm & release payment" card in the booking detail with the auto-release date. tsc + lint clean.⚠️ Ops: schedule a daily hit to/api/cron/auto-release-escrowon Railway (same as the other crons). -
Stage 4 — disputes & refunds ✅ 2026-06-07. No migration (the
disputestable already hadresolution/refund_amount/resolved_by/resolved_at). Hold: the dispute route now flips a marketplace booking todisputed(excludes it from the auto-release cron + blocks the poster release route — nothing pays out until admin acts). Resolve: admin endpoint /api/admin/disputes/[id]/resolve with 3 outcomes — release (full escrow → tasker, reusesreleaseMarketplaceEscrowwithallowedStatuses:['disputed','completed']), refund (full Stripe refund → poster, bookingcancelled), partial (refundrefundCentsto poster + pay tasker 80% of the remainder;releaseMarketplaceEscrowgainedpayoutCentsOverride/refundedToPosterCentsopts). All idempotency-keyed (mkt_refund_*). Cancel = refund: the cancel route refunds marketplace escrow in full (free, no late fee — only reachable up toaccepted) and reopens the task (status→open, clears accepted bid, reactivates the bid). UI: admin disputes page gained Release / Full refund / Partial-split actions; poster gets a DisputeModal ("Report a problem") + an "Under review" held-escrow banner in the booking detail. tsc + lint clean.
Golden rule: resolve before release (post-transfer clawback is hard). Full money path live end-to-end: accept→pay→escrow→assign→complete→release (poster confirm / 3-day auto-release) with disputes + refunds + cancellation around it. All transfers and refunds are idempotency-keyed. Part 8 marketplace money is feature-complete. Remaining: ops (schedule the auto-release cron daily on Railway).
No browser/test-runner here, so testing = DB advisors + SQL simulation (rolled-back, zero prod residue) + adversarial logic-tracing + tsc/lint/build. Findings fixed:
- Trigger-function exposure (advisors 0028/0029) → migration 046 (applied): revoked RPC
EXECUTEon the 6 trigger fns + 1 event-trigger (incl. the newrecompute_customer_rating) + pinnedupdate_updated_at's search_path. Re-ran advisors: all 7 cleared, zero ERRORs, no new findings. RLS predicate helpers (is_admin/is_tasker/…) + the intentionalget_customer_ratingRPC deliberately left executable. Verified theon_auth_user_createdtrigger +finalize_marketplace_bookingservice-role lock survived. - Release TOCTOU vs dispute →
releaseMarketplaceEscrownow re-reads the authoritativestatus/stripe_transfer_idimmediately before the transfer and aborts if a dispute landed (or a transfer already exists). Shrinks the race to sub-ms, covered by the idempotency key. - Partial-refund mislabel → the
charge.refundedwebhook comparedamount_refundedvsamount; only a FULL refund flipspayment_status='refunded'now (a dispute "partial split" no longer wrongly marks the booking fully refunded — it logspartial_refund). - Marketplace completion was unreachable (MAJOR) — the curated execution flow forced en_route→arrived→OTP→before/after photos, which a (possibly remote, address-less) marketplace job can't satisfy → the tasker could never reach
completed→ release never triggered. Fixed: marketplace jobs use a 3-step flow (accepted → in_progress → completed) in both the API (start_workallowed fromaccepted, OTP gate skipped) and the tasker job UI ("Start task" / "Mark complete"; no OTP/photos; pool-release hidden). Curated flow untouched.
Verified: reviews recompute trigger + get_customer_rating RPC end-to-end (rolled-back sim → 5.00/1, zero residue); marketplace booking insert satisfies every 043 CHECK + all 7 NOT-NULL-no-default columns; no marketplace bookings currently in a stuck state (clean slate). Pre-existing, out-of-scope advisor items left as-is: provider_availability RLS-no-policy, booking-photos public bucket listing, leaked-password protection (Auth dashboard toggle), and the broad multiple_permissive_policies/auth_rls_initplan RLS-perf set (zero impact at current scale; not worth churning RLS pre-launch). Final npm run build green.
Now committed (landing/AI work in
dd2d632; spec + migrations ind8cc567). Kept here as a session log; not yet folded intocompleted.md.
- Landing swap:
/2→/(the "post any task" landing); old services-led landing →/instahelp; post-task back-links + sitemap updated. - Hero AI routing: typing a task + Enter resolves category→subcategory via Groq (
/api/resolve-task) and opens the subcategory popup pre-selected; out-of-scope tasks ("data analyst") fall through to the open marketplace instead of a forced near-bucket. - Post-task wizard AI (Step 1–2): live AI category suggestions; AI categorizes the post on Step 2 (
/api/suggest-task-meta, honesty-tested → accurate custom categories, not forced buckets); AI typo/format fix for a hand-typed category (/api/normalize-category); Enhance with AI rewrites heading + description (top-right, animated); description box auto-fills the AI draft on focus. - Budget step: removed budget auto-fill (suggestion only); rebuilt the "GTA market price" section as an animated USP (range bar, count-up rate cards, recommended glow).
- iOS-style wheel time picker (
TimeWheel) on the Where & when step (Hour/Minute/AM-PM drum), with the time flowing into preview/review/submit. - Groq reliability/cost fix: model tiering via
GROQ_INTENT_MODEL/GROQ_HELP_MODEL= 8B (per-keystroke) andGROQ_MODEL= 70B (enhance only) — fixes the daily-token-limit 502s; tolerant-JSON parse + one retry on the generative routes. Set the two tier vars on Railway too. - ARCHITECTURE.md — the v2 capability architecture + 4-phase migration plan (this sprint's source of truth).
(The Marketplace Post-a-Task wizard UI — the prior active section — remains complete; backend wiring is folded into Architecture v2 Parts 2/8.)