A dual-sided marketplace where creative professionals manage their entire business — and clients find exactly who they need.

Finding the right creative professional is still a fragmented process. Clients search across Instagram, word-of-mouth, and outdated directories. Creators lose time managing bookings through DMs, chasing invoices, and trying to stand out on platforms designed for commodity gig work. EVÖQ set out to solve both sides at once.
The platform needed to:
Generic marketplace builders couldn't handle the complexity. Freelancer platforms like Fiverr or Upwork are built around deliverable-based gig work, not service-based creative bookings with date/time scheduling, physical space rentals, and portfolio-first discovery. A custom build was the only way to get the experience right.
Marketplace data leaks kill trust. When a creator can see another creator's earnings, or a client's conversation leaks to the wrong user, the platform is dead. The challenge isn't building two interfaces — it's ensuring they share a transactional layer (bookings, payments, messaging) without leaking data across role boundaries.
This is enforced at the database level with row-level security policies scoped per role. Creators can only see their own bookings and earnings. Clients can only access their own orders and conversations. Admin operations use a separate service role with elevated permissions, verified on every request. Even a compromised API layer can't leak data across users.
The platform also maintains a deliberate separation between auth provider IDs and application user IDs via a mapping column. Messaging, bookings, and payments all reference the application user ID — not the auth provider's UID. This means the entire authentication provider could be swapped out without migrating a single row in the core business tables.
Both sides share a unified booking engine, real-time messaging, and notification layer — built once, with access controlled by RLS rather than duplicated application logic.
Even a compromised API can't leak data across users.
Distinct experiences sharing a unified booking, messaging, and notification layer
A broken booking means a creator doesn't get paid and a client doesn't get the service they need. The system handles two fundamentally different booking types — services and spaces — through a unified architecture, because splitting them would mean splitting every downstream system too.
Service bookings follow a structured lifecycle:
Request
The client submits a booking request with date, time, and details.
Review
The creator receives the request and can accept or reject it from their dashboard.
Payment
On acceptance, the client is prompted to pay via Stripe.
Completion
After the service is delivered, the booking is marked complete and the client can leave a review.
Dispute
If something goes wrong, either party can raise a dispute, triggering multi-channel admin notification with graceful degradation.
Space bookings support two distinct pricing models — hourly and daily — enforced at the database level with PostgreSQL CHECK constraints. If a booking is hourly, start and end times are required; if daily, a daily rate and date range are required. Duration fields are generated columns, computed automatically from the date range. Invalid booking states can't exist in the database, regardless of what the application code does.
Reviews are locked to completed bookings — you can only review a creator after a real, paid transaction. This eliminates fake reviews, competitor sabotage, and reputation gaming at the database level.
Invalid booking states can't exist in the database.
If creators don't get paid quickly and transparently, they leave. Handling money in a marketplace is fundamentally different from processing a simple checkout — the platform doesn't just collect payments, it splits them, routing funds to creators while retaining a platform commission.
This is built on Stripe Connect with a managed account model:
Creator onboarding
Creators connect their Stripe account through an OAuth flow directly from their dashboard. The platform handles identity verification and payout setup.
Payment intent creation
When a booking is confirmed, a Stripe payment intent is created with the platform's 2% fee built in. The remaining 98% is routed directly to the creator's connected account.
Webhook-driven state management
Payment events (success, failure, refund) are processed through Stripe webhooks, keeping booking state in sync with payment state at all times.
Full audit schema
Payment intents tracked across eight statuses, a transactions table recording completed payments with fee breakdowns, and a separate refunds table for refund history.
Creators get paid directly — the platform never holds their funds.
Initiates payment
Processes transaction
Automatic routing
98% direct
2% commission
Creators get paid directly by Stripe — the platform never holds their funds
Discovery is the lifeblood of a marketplace. If clients can't find the right creator quickly, nothing else matters.
The search system runs entirely at the database level using PostgreSQL's native full-text search. A trigger builds search indexes dynamically whenever a service is created or updated, combining core service fields, creator information, and the full category hierarchy — resolved via a recursive query that walks from the service's category up through every parent category.
This means a search for “video” will surface services categorised under “music video,” “corporate video,” or any other subcategory in the hierarchy — without the application needing to know the category tree at query time.
Search results are ranked using PostgreSQL's built-in relevance scoring. On top of this, a dedicated search suggestions function powers the search bar's autocomplete with three-tiered results: Categories → Services → Creators, each with independent relevance scoring and dynamic result limits. The entire search pipeline — indexing, ranking, and suggestions — runs in the database, not in application code.
Autocomplete triggers as they type
Entire search pipeline — indexing, hierarchy resolution, ranking, and suggestions — runs in the database
A booking request that sits unseen for hours is a booking that never happens. In a marketplace where bookings require back-and-forth between creators and clients, delayed notifications mean lost revenue. The platform uses Supabase's realtime channel subscriptions to push notifications instantly via PostgreSQL's change data capture — no polling, no external pub/sub service.
When a row is inserted into the notifications table — a new booking request, a payment confirmation, an accepted booking — the client receives it immediately through an open WebSocket connection. Notification types carry structured metadata (booking ID, amount, checkout URL) so the frontend can render contextual actions without additional API calls.
The notification provider includes in-flight fetch protection using React refs to prevent duplicate requests during rapid state changes, and computes unread counts locally from the notification array rather than making separate count queries.
The built-in messaging system keeps all communication — from initial enquiry to post-booking follow-up — in one auditable place, with unread counts surfaced in the navigation bar and threaded conversations with full history.
Instant delivery via WebSocket — no polling, no external pub/sub.
A duplicated webhook means a creator gets paid twice. A dropped one means they don't get paid at all. Payment webhooks are inherently unreliable — Stripe may send the same event multiple times, network failures cause retries, and out-of-order delivery is common. In a marketplace handling real money between two parties, every edge case is a financial risk.
The platform handles this with a dedicated webhook events table that logs every incoming webhook with its Stripe event ID, event type, and a processed flag. A unique constraint on the event ID means duplicate deliveries are silently ignored via the database, not application logic.
Events are marked as processed only after the associated database operations (booking status update, transaction record creation) complete successfully. If processing fails, the event remains unprocessed with the error recorded alongside it — making failed webhooks visible and replayable without re-triggering them from Stripe.
The webhook endpoint preserves the raw request body as text rather than parsing it as JSON for Stripe's signature verification — a subtle but critical detail, since JSON parsing and re-serialisation can alter whitespace and break the HMAC check.
Duplicate webhooks are silently ignored — failed ones are visible and replayable.
Swipe for more →
Next.js 15 / React 19 / TypeScript / Tailwind CSS / Radix UI
Framer Motion
Next.js API Routes / React Server Components
Supabase (PostgreSQL + Row-Level Security)
Supabase Auth (JWT-based, magic link + password)
Stripe Connect + Payment Intents + Webhooks
Resend API + React Email
Supabase Storage (S3-backed CDN)
Vercel (Edge Network)
PostgreSQL views denormalise creator information, category labels, and computed fields into a single query shape. A mapping function reshapes flat view rows into nested objects the frontend expects, keeping the transformation in one place. If the underlying schema changes, only the view definition needs updating — not every API function.
When services, bookings, or accounts are removed, the data is soft-deleted rather than destroyed. This preserves the audit trail for payments, reviews, and disputes without cluttering active queries.
Data fetching happens at the server level where possible, reducing client-side JavaScript and improving initial page load times. Combined with Turbopack in development, this keeps the development and user experience fast.
The platform includes a dedicated mobile navigation system, responsive grid layouts, and touch-optimised interactions. Creative professionals manage their business from their phones — the platform is designed for that reality.
A Supabase Edge Function on a daily cron schedule (2 AM UTC) automatically cancels bookings older than 7 days still in a pending state. The API layer applies the same 7-day check in real time, providing a consistent expiration policy across both automated and manual paths.
Reviews locked to completed bookings — no fake ratings, no reputation gaming
Structured disputes with multi-channel admin notification and graceful degradation
Physical venue discovery with hourly and daily booking through the same payment system
Drag-and-drop image management with CDN-backed delivery
Booking confirmations, notifications, and lifecycle communications via React Email
Bookings, earnings, services, and portfolio management in one place
In-platform communication with unread tracking and full history
Cron-based edge function cancels stale bookings after 7 days
Dedicated mobile navigation and touch-optimised responsive design
Dual-sided workflows, payment splitting, real-time features, or marketplace-grade security — we've built it before and we'd love to talk about yours.
Get in Touch →