
Mini-Lancer
Mini-lancer is a SaaS platform that gives freelancers one place to manage clients, track projects, send invoices, and collect payments — without the chaos of spreadsheets and manual follow-ups. Every client gets a secure portal to track their project progress and pay instantly, so freelancers spend less time chasing and more time earning.
Timeline
2026
Role
Full-Stack Developer
Team
Solo
Status
In ProgressTechnology Stack
Key Challenges
- Implementing a secure, passwordless client portal using magic link tokens without any third-party auth dependency on the client side
- Designing a dual-audience architecture where the freelancer dashboard and the client portal share the same codebase but have completely separate layouts, auth strategies, and design languages
- Handling Razorpay webhook reliability — ensuring invoice and subscription statuses update correctly even when webhooks arrive out of order or are retried
- Building a dynamic line items invoice form with live total calculation, field array management, and consistent validation across both frontend and backend using shared Zod schemas
Key Learnings
- How to architect a multi-tenant SaaS product from scratch — including feature gating, plan limits, and subscription lifecycle management using webhooks as the source of truth
- The critical difference between Razorpay one-time order signature verification and subscription signature verification — and why mixing them causes silent auth failures
- How Clerk webhooks work in production and why localhost webhook URLs cause complete data sync failures that only appear when real users sign up on a deployed app
- How to build a clean separation between server components and client components in Next.js 16 App Router, including the new async params pattern for dynamic routes
Mini-Lancer
Overview
Mini-lancer is a production-grade SaaS platform built for independent freelancers. It replaces the fragmented workflow of spreadsheets, manual invoices, and payment follow-ups with a single, polished product.
Freelancers get a full business dashboard, and their clients get a dedicated portal to track work and pay invoices, all without either party needing to manage complex software.
The project covers the entire SaaS lifecycle: user authentication, mini-CRM, project tracking, invoice generation, client-facing portals, Razorpay payment collection, and a subscription-based monetization model with plan gating.
Core Experience
Mini-lancer has two distinct user experiences.
Freelancer Dashboard
A protected, authenticated workspace where freelancers manage their business end to end:
- Add clients
- Create projects
- Draft and send invoices
- Monitor payment status
- Upgrade plan
Client Portal
A public, passwordless portal accessed through a magic link sent by the freelancer. Clients can:
- See a live project timeline
- View itemized invoices
- Pay directly via Razorpay
No account creation is required.
Architecture & Implementation
Backend
The backend is built entirely with Next.js 16 App Router route handlers, following REST conventions. Routes are protected at the correct layer:
- Freelancer routes — protected by Clerk session (
await auth()) - Client portal routes — protected by magic link token validation
- Webhook routes — protected by cryptographic signature verification (svix for Clerk, HMAC SHA256 for Razorpay)
Database
The database layer uses Prisma 6 with MongoDB Atlas.
- All relationships use
onDelete: Cascade, so deleting a user or client removes associated data automatically. - Since MongoDB Atlas free tier does not support
prisma.$transaction, all operations use sequentialawaitcalls.
Frontend
The frontend is a fully client-side React app built on the App Router with React Query handling all data fetching, caching, and mutations. Every page has loading skeletons, empty states, and error toasts — nothing ever shows a blank screen.
Key Components
proxy.ts— Clerk middleware protecting all/dashboard/*routeslib/prisma.ts— Singleton Prisma client preventing connection exhaustion on hot reloadlib/verify-portal-token.ts— Shared token validation logic used across all portal API routeslib/validations.ts— Centralized Zod schemas shared between frontend forms and backend routeshooks/use-portal.ts— React Query hooks for the client portal — no Clerk auth, token-onlycomponents/portal/razorpay-pay-button.tsx— Dynamically loads Razorpay script and opens checkout modalcomponents/billing/upgrade-button.tsx— Full Razorpay subscription checkout flow with signature verificationapp/api/webhooks/razorpay/route.ts— Handlesorder.paidandsubscription.chargedevents as the payment source of truth
Features in Detail
Magic Link Client Portal
The freelancer clicks "Send Portal Link" on any client. The backend generates a 64-character random hex token using crypto.randomBytes(32), saves it with a 7-day expiry, and emails a portal URL to the client via Resend. The client clicks the link and enters a fully separate, light-themed portal — no login, no password.
Dynamic Invoice Builder
Invoices are created with a dynamic line items form using useFieldArray from React Hook Form. Each row has description, quantity, and rate fields. The total auto-calculates live as the user types. All amounts are stored in paise (smallest INR unit) to avoid floating point errors, and converted to rupees only at display time.
SaaS Plan Gating
FREE plan users are limited to 3 clients. The limit is enforced at the API level on POST /api/clients with a 403 response. The frontend reads plan status from GET /api/users/me and disables the "Add Client" button with a tooltip when the limit is reached. Upgrading to PRO is handled via Razorpay Subscriptions — the plan upgrades automatically when the subscription.charged webhook fires.
Razorpay Webhook as Source of Truth
No payment status is updated directly from the frontend. When a client pays an invoice, the frontend only opens the Razorpay modal. The actual status change (PENDING → PAID) happens on the backend when Razorpay fires the order.paid webhook, verified with HMAC SHA256. Same pattern for subscriptions — FREE → PRO only happens when subscription.charged fires.
Portal Links Hub
A dedicated dashboard page (/dashboard/portals) gives the freelancer a bird's-eye view of all client portal statuses — Active, Expired, or Not Sent — with one-click resend and direct preview links for each client.
State & Data Flow
React Query manages all server state with a structured query key hierarchy:
['clients'] → all clients list
['clients', clientId] → single client
['projects'] → all projects
['projects', { clientId }] → filtered by client
['invoices'] → all invoices
['invoices', filters] → filtered
['invoices', invoiceId] → single invoice
['user-me'] → plan + limits
['portal-projects', token] → portal projects
['portal-invoices', token] → portal invoices
Mutations invalidate the correct query keys on success, keeping all UI in sync without manual refreshes. The useSendMagicLink mutation invalidates both ['clients'] and ['clients', clientId] so the portal status indicator updates immediately across the clients list and detail page.
Accessibility & UX Considerations
- Every destructive action (delete client, delete invoice, cancel subscription) requires
AlertDialogconfirmation — never a single click delete - Every form shows inline Zod validation errors below the relevant field — not a generic top-level error
- All tables and lists have two empty states: one for no data at all and one for no search results — with contextually different messaging
- The client portal is always light-themed regardless of the freelancer's system theme — it is a client-facing product, not a developer tool
- All status badges (project and invoice) are color-coded consistently across every surface — dashboard, detail pages, portal, and emails
Technical Highlights
- Next.js 16 async params — every dynamic route correctly types params as
Promise<{ param: string }>and awaits them, following the new App Router pattern - Shared Zod schemas — the same validation schema is used in the React Hook Form
zodResolveron the frontend andzod.parse()on the backend API route, ensuring the frontend and backend never drift out of sync - Singleton Prisma client — prevents new database connections on every hot reload in development, a critical fix for MongoDB Atlas connection limits
- Dual signature verification — correctly distinguishes between Razorpay one-time order signatures (
payment_id|order_id) and subscription signatures (payment_id|subscription_id) — a subtle but critical difference that causes silent failures if mixed up - Environment variable validation — all env vars are validated at startup using Zod in
lib/env.tsandlib/env.server.ts, so missing variables fail loudly at boot rather than silently at runtime
Challenges
Magic Link Security Without a Full Auth System
Building a stateless, passwordless portal that is secure but requires no account creation was the core UX challenge. The solution was a 64-character cryptographically random token stored per client with a 7-day TTL — secure enough for the use case, simple enough for clients who are not technical.
Razorpay Subscription vs Order Signature Mismatch
Discovered in production that Razorpay uses completely different signature formulas for subscriptions vs one-time orders. Subscription verification uses payment_id|subscription_id while orders use order_id|payment_id. Mixing these causes a valid payment to return 400 Invalid signature — a non-obvious bug that required reading deep into Razorpay's documentation to diagnose.
Clerk Webhook Failures in Production
After deploying, new user signups worked in Clerk but failed everywhere in the app. Root cause: the Clerk webhook URL was still pointing to localhost:3000 from development. Since Clerk could not reach localhost from their servers, no user.created events were processed and no User records were created in MongoDB — causing every authenticated API call to return 404.
Dual Audience in One Codebase
The freelancer dashboard and client portal are architecturally separate — different layouts, different auth strategies, different design systems — but live in the same Next.js project. Managing this cleanly required careful use of App Router route groups, separate layout files, and a strict rule that no Clerk components or dashboard styles ever leak into the portal routes.
Future Enhancements
- PDF Invoice Export — generate a real downloadable PDF using a headless browser or a PDF library instead of relying on
window.print() - Overdue Invoice Automation — a scheduled cron job that automatically updates invoice status to
OVERDUEwhen the due date passes and sends a reminder email to the client - Multi-currency Support — currently INR-only via Razorpay; adding USD and EUR support for international freelancers
- Client Notes & Activity Log — a per-client timeline showing all interactions, status changes, and payment history in one view
- Invoice Templates — multiple invoice styles the freelancer can choose from before sending
Why It Matters
Most freelancers do not fail because of bad work — they fail because of bad business operations. Missed follow-ups, forgotten invoices, clients who never know what stage their project is in. Mini-lancer solves the operational side of freelancing so the freelancer can focus entirely on the work itself. It is not a tool built for enterprises — it is built for the solo developer, designer, or consultant who just needs things to work without complexity.
Closing
Mini-lancer is the project where I touched every layer of a modern SaaS product — database schema design, REST API architecture, authentication and webhook systems, payment integration, subscription billing, and a fully responsive frontend with real UX polish. Every decision from the magic link token length to the Razorpay webhook idempotency check was made deliberately. It is the clearest demonstration of how I think about building products end to end.