Repairlah is a marketplace that connects Malaysians who need something fixed with repair technicians — post a repair job with photos and a budget, get responses from repairers near you, and close the deal over WhatsApp. Free, mobile-first, and live at repairlah.vercel.app.
- Full auth flow (
src/app/auth,src/components/AuthForm.tsx) — Supabase email sign-up with verification, forgot/reset password, and a role chooser (poster or repairer). - Job posting with real constraints (
src/app/post,src/lib/validation.ts) — title, description, tags, budget range in RM, photos uploaded to Supabase Storage, city + Malaysian state. Every field is zod-validated client-side and CHECK-constrained in Postgres. - A filterable feed (
src/components/FeedClient.tsx,StateFilter.tsx) — browse, search, and filter jobs by Malaysian state; each job card is shareable with its own Open Graph link preview (WhatsApp/WhatsApp-ready metadata). - Bids and one-tap responses (
src/app/api/bids,src/app/api/respond) — repairers bid on jobs; responding is limited to one per user per job (enforced by a UNIQUE constraint) and triggers an atomic auto-close plus a WhatsApp handoff so the conversation starts in the app people actually use. - Job lifecycle (
src/app/api/jobs/[id]/complete) — owner-only completion with a status lifecycle ofopen → in_progress → completed / expiredenforced in the schema. - Trust primitives — public profiles, abuse reporting (
src/app/api/report), and row-level security on every table (supabase/migrations/001_schema.sql).
1. AUTH 2. POST A JOB 3. FEED 4. BID + RESPOND 5. COMPLETE 6. TRUST
+------------------------+ +---------------------------+ +---------------------------+ +------------------------+ +------------------------+ +--------------------------+
| Supabase email auth: | | title, description, tags, | | browse + search the feed, | | repairers bid on jobs; | | owner marks completed; | | profiles, abuse reports, |
| verification, forgot / | | budget range (RM), | | filter by Malaysian | | responding is one per | | lifecycle: open -> | | row-level security on |
| reset password, | | photos -> Supabase | | state, shareable job | | user per job -> atomic | | in_progress -> | | every table |
| choose role | > | Storage, city + state | > | cards with OG link | > | auto-close + WhatsApp | > | completed / expired | > | |
| (poster or repairer) | | | | previews | | handoff | | | | |
+------------------------+ +---------------------------+ +---------------------------+ +------------------------+ +------------------------+ +--------------------------+
All database access goes through Supabase with server-side auth cookie handling
(src/lib/supabase/server.ts, middleware.ts); API routes re-verify the session on
every mutation (src/app/api/*). Photos land in the job-photos storage bucket and
are served by public URL.
| Layer | Choice |
|---|---|
| Framework | Next.js 16 (App Router) + React 19 |
| Styling | Tailwind CSS v4 (black & white + cobalt design system, see DESIGN.md) |
| Backend | Supabase — Postgres, Auth (email), Storage, row-level security |
| Validation | zod, mirrored by Postgres CHECK constraints |
| Icons | lucide-react |
| Tests | Vitest + React Testing Library (src/test/) |
| Deploy | Vercel |
Five tables, all with row-level security enabled — four in
supabase/migrations/001_schema.sql, plus bids (supabase/bids-table.sql):
| Table | Purpose | Notable constraints |
|---|---|---|
jobs |
Repair jobs | title 5–200 chars, description 20–5000, budget ≤ 50,000 RM, status ∈ {open, in_progress, completed, expired} |
bids |
Repairer bids on a job (budget range) | UNIQUE (job_id, user_id), authenticated users read |
responses |
Repairer responses / WhatsApp handoffs | UNIQUE (job_id, user_id) — one response per user per job |
profiles |
User profiles with account type | keyed to auth.users, cascade delete |
reports |
Abuse reports on jobs/users | — |
npm install
# 1. Create a Supabase project, then set these:
# NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY
npm run dev # http://localhost:3000
# 2. Apply the schema (in the Supabase SQL editor or CLI):
# supabase/migrations/001_schema.sql … 006_*.sql, then supabase/bids-table.sql
# 3. Tests
npx vitest runNo secrets are needed beyond the two public Supabase environment variables; the anon key is public-by-design and all authorization is enforced server-side by row-level security policies.
├── middleware.ts # Supabase session refresh
├── src/
│ ├── app/ # App Router pages + /api routes (jobs, bids, respond, report, profile)
│ ├── components/ # 18 UI components (feed, job cards, auth forms, tab bar, lightbox…)
│ ├── hooks/
│ ├── lib/ # supabase clients (browser/server), validation, image utils
│ └── test/ # Vitest suites (auth, components, utils, validation)
├── supabase/
│ └── migrations/ # ordered SQL schema migrations
├── DESIGN.md # the black & white + cobalt design system
├── docs/superpowers/specs/ # design spec from the UI revamp
└── public/ # manifest, favicon, OG image
- WhatsApp is the handoff, not the record — once a response redirects to WhatsApp,
the conversation lives outside the app; the platform keeps only the response record
(
whatsapp_redirected) and job status. - Single-response auto-close — a job closes atomically when someone responds; there is currently no multi-bid negotiation inside the app beyond the bids API.
- Malaysia-first — states, RM budgets, and WhatsApp-centric flow are intentional; localization beyond that is not built in.
- Moderation is minimal — reports are collected; there is no admin review UI.
Built through an agentic AI workflow: 98 commits between May and July 2026, from the
first schema migration through the UI revamp — design specs (docs/superpowers/),
agent-driven implementation, and systematic bugfix passes (mobile OG previews,
session-handling fixes, render regressions). The human role was direction and judgment:
the product idea, the feature set, and every accept/reject call.
Built by D. Desmaan.
Licensed under the MIT License.
