Skip to content

Latest commit

 

History

History
413 lines (293 loc) · 54 KB

File metadata and controls

413 lines (293 loc) · 54 KB

In Progress — Taskly.ca Launch Tracker

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 of MC Final/), business-plan/ (stale draft still claiming Certn/CPIC — root plan with Stripe-Identity model is canonical); archived homepage-mockup.html, tasker-profile-preview.html, email-preview/, taskly.ca-audit/, Taskly hosting/, Taskly-Module-Audit.xlsx into _archive/ (index updated); moved _seed.mjsscripts/seed-demo.mjs. Gate: tsc ✓, 487 tests ✓, build ✓ (Tailwind doc-scan survived). Fixed stale local install (jsdom missing → npm install). Note for Phase 3: npm run lint has 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.md untouched); 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.md ledger 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 linting community/.next build output (flat config ignores added). Real errors driven to 0: --fix removed 47 dead imports (54 files), renamed useOpenerapplyOpener (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-imports warns on direct @supabase/supabase-js createClient outside lib/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.js kept (runtime-registered, knip entry). New CI (test.yml): concurrency-cancel + lint + hygiene (knip files/deps/unlisted + scripts/check-migrations.mjs duplicate-number gate, grandfathering the 11 historical collisions, next free = 156) + aggregate required job. New scripts: typecheck / knip / check:migrations. tsconfig: ES2017→ES2022, allowJs dropped. 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.local before querying): schema_migrations holds 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.sql never applied (ops-alert dedup is a no-op) and 070b only partially applied — customer_profiles.reengaged_at missing, so the win-back lane of /api/cron/engagement-nudges errors 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.tslib/utils/index.ts (115 importers untouched — resolution seam), lib/ai-quota-config.tslib/ai-quota/config.ts (3 importers), lib/welcome-options.tslib/welcome/options.ts (6 importers); lib/geo collision self-resolved in Part 3. Haversine deduped: browse-tasks re-exports distanceKm from lib/geo. Component buckets: NOT merged blindly — they're 4 different features; renamed components/tasker/tasker-gate/ (customer-side gate), moved pro/ProLinkprovider/, kept providers/ (React context providers) + taskers/ (recruiting page); bucket map documented in CLAUDE.md. All 11 direct createClient offenders migrated to createServiceRoleClient() (each already env-guards, so the wrapper's fallback never fires) and the lint rule flipped warnerror (0 violations). Dev-route gating audited: all 7 api/dev/* + app/dev/seed + api/_internal/price-cache-stats already 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_log table w/ RLS; customer_profiles.reengaged_at — win-back email lane un-broken); ledger updated. D1: business docs STAY in business/ in this repo (founder call; repo knowingly public). D4: vercel.json deleted (Railway prod; crons covered by the GitHub Actions dispatcher). Deferred to their own tasks: D2 community/ extraction to its own repo · D3 one-off scripts stay (knip + time will decide) · D5 api/bookings/v2 consolidation + 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.


Pricing v7 — poster-anchor three-point model (2026-07-18) · built, NOT yet pushed

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_FACTOR calibration (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.csv gained a source column (rate-card citations per category).

Taskly Haggle — two-sided offer negotiation (2026-07-16) · built, NOT yet deployed

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.sqltask_offer_moves ledger (one row per move; one live pending per lane via partial unique index) + negotiator_stats (arcade XP/streak/badges). RLS mirrors task_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, computeState lane machine, haggle math (meetInMiddleCents, gapFraction), arcade helpers (dealXp, levelForXp, negotiatorTitle, BADGES, badgesForDeal).
  • lib/marketplace/negotiation.server.tscreateMove / respondMove (accept/decline/withdraw) / getLaneState / stats. Note moderation via preCheck (contact-strip). Acceptance freezes the agreed price onto task_bids.amount_cents via 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_declined added 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 poster OffersDrawer ("Haggle" per bid; deal → chains into hire()/escrow) and the tasker MyOffersClient ("Haggle" on active offers).
  • tsc clean · npm test 316 pass (18 new in tests/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.

Active sprint: Poster App (2026-07-13)

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/payments placeholder 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" → #offers deep-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.bookingId and 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_viewed fire. ALL 10 PARTS DONE ✅ — Poster app functionally whole. Awaiting "commit".

Active sprint: Tasker Community (2026-06-23)

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).


Active sprint: Global Design System Revamp (2026-06-14)

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 rounded Button primitive (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.

Active sprint: Manager Gap Audit (2026-06-12)

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/claim sets referred_by + <ReferralClaim/> in the /app layout. Verified mechanics + real-code lookup. Credit accrual already worked; loop now closed.
  • G3 — Bid-placed notification to poster ✅ 2026-06-12 — new_offer notification on a new bid (edits don't re-notify).
  • G2 — Escrow auto-release + retry cron ✅ 2026-06-12 — single /api/cron/run dispatcher (all 6 jobs) on the existing GitHub Actions 15-min cron. Ops: confirm CRON_SECRET in Railway + GH secret. See CRON_SETUP.md.
  • G7 — Promo codes ✅ 2026-06-12 — promo_codes table (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/charge off-session PI on the saved card + 100% Connect payout + tip_received notification (migration 053). Review page wired. Operator: verify a charge in Stripe test mode.
  • G5 — Notification preferences honored ✅ 2026-06-12 — createNotification skips non-critical in-app notifications the user opted out of (per-event + push channel); critical types always send. emailChannelEnabled gates 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 /pro seed fallback. Set =false at 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-bid is 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.


Feature: first-sign-in "Welcome / get to know you" (2026-06-13)

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" → skills Coding, Tech Setup).
  • /api/welcome — saves profiling data + sets user_metadata.profile_completed.
  • Gate: /app layout 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/welcome gained a GET; options shared via lib/welcome-options.ts.

Prior sprint: Architecture v2 — Capability refactor

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 + navGet help⇄Earn toggle ✅ (5a+5b); route move /provider→/app/tasker deferred as cosmetic (5c)
  • Part 6 — Become-a-tasker activation (canonical /onboarding/tasker door, 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-aware recompute_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_path pinned, granted to authenticated). A tasker is derived from an approved provider_profiles row, so these already resolve correctly for today's providers regardless of role.
  • handle_new_user() rewrite: every new member now also gets a customer_profiles row (customer base is universal); still creates provider_profiles for the legacy /join-auth provider signup until that door is retired (Part 6); and stops honoring role='admin' from client signup metadata — a hardening fix (a self-service signup could previously land role='admin' via raw_user_meta_data).
  • Backfill: inserts a customer_profiles row 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, all SECURITY 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.


Part 2 — RLS rewrite + self-deal guards (migration 040) ✅ APPLIED & verified on prod

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) → now is_admin() (reads profiles.role). This closed a real escalation surface: a user could updateUser({data:{role:'admin'}}) and get admin RLS access via direct PostgREST.
  • Tasker pool — dropped the 2 duplicate spoofable bookings provider policies + rewrote the 2 profiles.role ones into a single is_tasker() pair on the unassigned pool; addresses provider read → is_tasker().
  • Self-deal guardsbookings_provider_not_customer CHECK (a member can't be dispatched their own booking; 0 existing violations); task_bids insert now requires is_tasker() AND poster ≠ bidder (0 existing bids).
  • Future-proofed the public tasker-profile read to key off an approved provider_profiles row (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.


Part 3 — Capability plumbing in code ✅ built (tsc + lint clean), pending commit

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 DB is_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) from getProfileRole(...) !== '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).


Part 4 — Routing & gating ✅ built (tsc + lint clean), pending commit

The change that actually lets one account use both surfaces.

  • app/(customer)/app/layout.tsx: removed the role==='provider' → /provider bounce. A tasker now gets the customer surface at /app (the shared base). Only admin → /admin remains. (Legacy self-heal left intact; Part 9 removes it.)
  • app/(provider)/provider/layout.tsx: gating is now capability-based (getTaskerStatus()), not role==='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/verify guard is untouched.

⚠️ Interim dependency: a tasker now lands on /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.


Part 5a — Context switch (capability-aware nav) ✅ built (tsc + lint clean), pending commit

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" → /provider row when canEarn (approved or pending tasker). Non-taskers see nothing yet (the "Start earning" upsell is Part 7); admins never see it.
  • ProviderShell: added "Customer app" → /app in 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.


Part 5b — Get help ⇄ Earn toggle ✅ built (tsc + lint clean), pending commit

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 when canEarn. 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.


Part 6 — Become-a-tasker activation ✅ built (tsc + lint clean), pending commit

app/onboarding/tasker/page.tsx — the canonical "Become a tasker" door, one destination for both entry paths, capability-gated:

  • Anonymous/login?next=/onboarding/tasker (preserving returnTo), 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 the provider_profiles row + flips role + runs bg-check/Stripe Connect (verified at onboarding/page.tsx ~L2370 and verify/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.


Part 7 — Conversion CTAs + capability gate ✅ built (tsc + lint clean), pending commit

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 approved isTasker, 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-existing set-state-in-effect at page.tsx:835 is in unrelated search code, build-ignored via eslint.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.


Part 9 — Cleanup, role collapse & hardening ✅ built + applied on prod

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 upsert provider_profiles (no role change); auth/callback defaults new users to customer + dropped signup_intent; the customer-layout self-heal (which re-created role='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 a provider_profiles row first); handle_new_user() simplified (always customer base; tasker rows via activation; admins out-of-band); added idx_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).


🏁 Capability architecture sprint — COMPLETE (Parts 1–7, 9)

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.


Post-sprint code finish (2026-06-07)

After the sprint roadmap, swept the remaining build work so only testing + the cron-scheduling op remain:

  • Part 8a — bidirectional reviews ✅ (migration 045 applied; 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 retired role='provider'; now targets provider_profiles), feedback triage label (now tasker via is_tasker() instead of the dead role), and corrected misleading "set role='provider'" comments in onboarding (code was already capability-correct). dev/seed-test-users still seeds role='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).

Leftover-work sweep (2026-06-07)

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 approved provider_profiles row (what is_tasker() reads), instead of the retired role='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 statStartEarningCard now 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 /provider references, with a real trap (many are /api/provider/* API routes + /providers/match that 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).


Part 8 — Marketplace money (convert-to-booking) · IN PROGRESS

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 enginefees.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 — serverconvert.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: on payment_intent.succeeded with purpose: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 new escrow prop (charged-now-held-in-escrow copy + "Hold in escrow" button) → confirm → webhook finalizes. Read null-safety: /api/bookings GET + /api/provider/jobs (upcoming/completed) now join the linked task via task: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) → marks paid; on transfer failure it stays completed for retry. Three triggers: (1) the tasker's complete action now branches on booking_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 (Bearer CRON_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-escrow on Railway (same as the other crons).

  • Stage 4 — disputes & refunds ✅ 2026-06-07. No migration (the disputes table already had resolution/refund_amount/resolved_by/resolved_at). Hold: the dispute route now flips a marketplace booking to disputed (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, reuses releaseMarketplaceEscrow with allowedStatuses:['disputed','completed']), refund (full Stripe refund → poster, booking cancelled), partial (refund refundCents to poster + pay tasker 80% of the remainder; releaseMarketplaceEscrow gained payoutCentsOverride/refundedToPosterCents opts). All idempotency-keyed (mkt_refund_*). Cancel = refund: the cancel route refunds marketplace escrow in full (free, no late fee — only reachable up to accepted) 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).


Testing & hardening pass (2026-06-07)

No browser/test-runner here, so testing = DB advisors + SQL simulation (rolled-back, zero prod residue) + adversarial logic-tracing + tsc/lint/build. Findings fixed:

  1. Trigger-function exposure (advisors 0028/0029) → migration 046 (applied): revoked RPC EXECUTE on the 6 trigger fns + 1 event-trigger (incl. the new recompute_customer_rating) + pinned update_updated_at's search_path. Re-ran advisors: all 7 cleared, zero ERRORs, no new findings. RLS predicate helpers (is_admin/is_tasker/…) + the intentional get_customer_rating RPC deliberately left executable. Verified the on_auth_user_created trigger + finalize_marketplace_booking service-role lock survived.
  2. Release TOCTOU vs disputereleaseMarketplaceEscrow now re-reads the authoritative status/stripe_transfer_id immediately 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.
  3. Partial-refund mislabel → the charge.refunded webhook compared amount_refunded vs amount; only a FULL refund flips payment_status='refunded' now (a dispute "partial split" no longer wrongly marks the booking fully refunded — it logs partial_refund).
  4. Marketplace completion was unreachable (MAJOR) — the curated execution flow forced en_route→arrived→OTPbefore/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_work allowed from accepted, 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.


Earlier this session — shipped on main

Now committed (landing/AI work in dd2d632; spec + migrations in d8cc567). Kept here as a session log; not yet folded into completed.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) and GROQ_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.)