Best Tech Stack for Directory & Marketplace Websites in 2026
Most "best tech stack" articles read like someone browsed Product Hunt for an afternoon and wrote up their findings. They'll tell you to use React and maybe Postgres, toss in Stripe, and call it a day. That's not helpful when you're trying to build a directory website with 137,000 listing pages that need to render fast, support geo-search within a 50-mile radius, and let users find results using natural language queries.
I've spent the last two years building exactly these kinds of sites. Not toy projects -- production directory and marketplace platforms handling hundreds of thousands of records, multi-country payment processing (including the fun edge cases like zero-decimal currencies), and search systems that combine full-text, semantic AI, and geographic queries simultaneously. This article walks through every layer of the stack we use at Social Animal, why we chose each piece, and the production data that backs up those decisions.
Table of Contents
- Why Directory and Marketplace Sites Are Architecturally Unique
- The 10-Layer Stack Overview
- Layer 1: Frontend -- Next.js 15
- Layer 2: Database -- Supabase PostgreSQL
- Layer 3: Authentication -- Supabase Auth
- Layer 4: Payments -- Stripe Connect
- Layer 5: Search -- The Triple Search Pattern
- Layer 6: Media -- Supabase Storage + Next.js Image
- Layer 7: Hosting -- Vercel
- Layer 8: Email -- Brevo API
- Layer 9: AI -- Claude API
- Layer 10: Monitoring -- Vercel Analytics + PostHog
- Full Stack Comparison Table
- What This Stack Costs in Production
- FAQ

Why Directory and Marketplace Sites Are Architecturally Unique
Directory and marketplace websites look simple on the surface. List some things, let people search, maybe process a payment. But the moment you start building one with real data, you hit problems that standard SaaS architectures don't prepare you for.
First, there's the page count problem. A directory with 100K+ listings needs 100K+ pages. You can't server-render all of them on every request, and you can't statically generate them all at build time (your builds would take hours). You need something smarter -- Incremental Static Regeneration (ISR) or on-demand revalidation.
Second, search is multi-dimensional. Users want to search by text ("family therapist"), by meaning ("someone who helps with relationship anxiety"), and by location ("within 20 miles of Austin"). Most stacks handle one of these. Handling all three simultaneously requires a specific database architecture.
Third, marketplaces have payment complexity that goes way beyond a simple checkout. You're dealing with platform commissions, subscription tiers, multi-currency pricing, and regulatory differences across countries. Get any of this wrong and you're either losing money or breaking laws.
These constraints shaped every decision in our stack. Let's walk through each layer.
The 10-Layer Stack Overview
Before we dive deep, here's the full picture:
| Layer | Tool | Why |
|---|---|---|
| Frontend | Next.js 15 (App Router) | ISR for 100K+ pages, Server Components |
| Database | Supabase PostgreSQL | pgvector + PostGIS + full-text in one DB |
| Auth | Supabase Auth | Row-Level Security, role-based access |
| Payments | Stripe Connect | Marketplace commissions, multi-currency |
| Search | Triple pattern (tsvector + pgvector + PostGIS) | Text + semantic + geo simultaneously |
| Media | Supabase Storage + Next.js Image | Optimized delivery, simple uploads |
| Hosting | Vercel | Edge deployment, ISR support, preview URLs |
| Brevo API | Transactional + marketing from API routes | |
| AI | Claude API | Semantic search, content enrichment, chatbots |
| Monitoring | Vercel Analytics + PostHog | Traffic + user behavior tracking |
Every layer here is running in production across multiple projects. Let me show you what that actually looks like.
Layer 1: Frontend -- Next.js 15
We've built directory sites with both Next.js and Astro. Both are excellent. But for directories and marketplaces specifically, Next.js 15 with the App Router wins because of one feature: Incremental Static Regeneration.
Here's the real scenario. One of our directory projects renders 137,000 listing pages. Another handles 91,000. You cannot statically generate all of these at build time -- the build would take forever and you'd blow past Vercel's function execution limits. And you can't server-render them all on every request because your server costs would be absurd and your Time to First Byte would suffer.
ISR gives you the best of both worlds. The first visitor to a page triggers a server render, which gets cached at the edge. Subsequent visitors get the static version. You set a revalidation interval (we typically use 3600 seconds for listing pages), and the cache refreshes in the background.
// app/listings/[slug]/page.tsx
export const revalidate = 3600; // Revalidate every hour
export async function generateStaticParams() {
// Only pre-generate the top 1000 most-visited listings
const { data } = await supabase
.from('listings')
.select('slug')
.order('view_count', { ascending: false })
.limit(1000);
return data?.map((listing) => ({ slug: listing.slug })) ?? [];
}
export default async function ListingPage({ params }: { params: { slug: string } }) {
const { data: listing } = await supabase
.from('listings')
.select('*, categories(*), reviews(*)')
.eq('slug', params.slug)
.single();
// Server Component -- no client-side JS shipped for this
return <ListingDetail listing={listing} />;
}
Server Components are the other big win. Listing detail pages are mostly static content -- name, description, photos, reviews. There's no reason to ship React's runtime to the client for that. Server Components render on the server and send pure HTML. Your listing pages load fast and your JavaScript bundle stays small.
We use Client Components sparingly: the search bar, interactive maps, booking forms, and anything that needs user interaction. Everything else stays on the server.
Why Not Astro?
Astro is fantastic for content-heavy directories where interactivity is minimal. We've used it for documentation sites and content-focused projects. But marketplace sites need authenticated states, real-time features, and complex forms. Next.js handles these more naturally. If your directory is mostly read-only (think: a static business directory), Astro is worth considering -- check out our Astro development capabilities.

Layer 2: Database -- Supabase PostgreSQL
This is the most opinionated choice in the stack, and the one I'm most confident about. Supabase gives you PostgreSQL with all of its extensions -- and for directory/marketplace sites, three extensions matter enormously: pgvector, PostGIS, and PostgreSQL's built-in full-text search.
Across our directory projects, we're managing 253,000+ records in Supabase. That includes listings, user profiles, reviews, bookings, and subscription data. PostgreSQL handles this without breaking a sweat -- it's designed for datasets orders of magnitude larger.
The real insight is this: by keeping full-text search, vector embeddings, and geographic data in the same database, you avoid the architectural complexity of syncing data across multiple services. You don't need Elasticsearch for text search. You don't need Pinecone for vector search. You don't need a separate geo-service. One database. One source of truth.
-- A single query that combines text search, vector similarity, and geo proximity
SELECT
l.id,
l.name,
l.description,
ts_rank(l.search_vector, plainto_tsquery('english', 'family therapist')) AS text_rank,
1 - (l.embedding <=> $1::vector) AS semantic_similarity,
ST_Distance(
l.location::geography,
ST_MakePoint(-97.7431, 30.2672)::geography
) / 1609.34 AS distance_miles
FROM listings l
WHERE
l.search_vector @@ plainto_tsquery('english', 'family therapist')
AND ST_DWithin(
l.location::geography,
ST_MakePoint(-97.7431, 30.2672)::geography,
80467 -- 50 miles in meters
)
ORDER BY
(text_rank * 0.3) + (semantic_similarity * 0.5) + ((1 - distance_miles/50) * 0.2) DESC
LIMIT 20;
That's one query. Full-text ranking, semantic similarity scoring, and geographic distance filtering -- all happening in PostgreSQL. Try doing that across three separate services and keeping the results coherent.
For a deeper dive on database options for directories, check out our headless CMS and database comparison.
Supabase Row-Level Security
RLS deserves its own mention because it solves a problem that plagues marketplace backends: data access control at the database level. Instead of writing authorization checks in every API endpoint, you define policies on the database itself.
-- Therapists can only see their own client records
CREATE POLICY "therapists_own_clients" ON client_records
FOR SELECT USING (
auth.uid() = therapist_id
OR auth.jwt() ->> 'role' = 'admin'
);
Even if you have a bug in your API code that accidentally exposes a query, RLS prevents unauthorized data access. For marketplace sites handling sensitive user data, this is non-negotiable.
Layer 3: Authentication -- Supabase Auth
Since we're already in the Supabase ecosystem for the database, using Supabase Auth is a natural fit. But the real reason we use it for marketplaces is role-based access that integrates directly with RLS.
One of our marketplace projects runs role-based auth across three distinct user types: admins, service providers, and clients. Each role sees different data, has different permissions, and accesses different features. Another project runs a 4-tier membership system where each tier unlocks progressively more features.
The implementation stores the user's role in their JWT metadata, which means RLS policies can reference it without additional database queries:
// Assigning role during signup
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
data: {
role: 'therapist',
tier: 'professional'
}
}
});
Supabase Auth supports OAuth providers (Google, Apple, etc.), magic links, and email/password -- all out of the box. For B2C marketplaces, social login is practically required. We've seen signup conversion rates increase 30-40% when Google OAuth is available alongside email/password.
Layer 4: Payments -- Stripe Connect
Payment processing is where marketplace projects get genuinely complicated. There's a big difference between "accept a payment" and "accept a payment, take a platform commission, handle refunds, manage subscriptions across 30 countries, and deal with zero-decimal currencies."
Stripe Connect handles the marketplace payment flow -- the split between platform and service provider. One of our projects processes commissions on every transaction, automatically routing the platform fee and the provider's cut.
But the subscription side is where it gets interesting. We run a 4-tier subscription system with regional pricing across 30+ countries. This means maintaining separate Stripe Price objects for different currency regions.
The Zero-Decimal Currency Bug
This is a story I share because it saved us (and our client) real money. Stripe handles most currencies in their smallest unit -- so $10.00 USD is 1000 (cents). But some currencies like Japanese Yen (JPY) and Korean Won (KRW) don't have decimal units. ¥1000 is just 1000, not 100000.
If your code blindly multiplies by 100 to convert to the smallest unit, you'll charge Japanese users 100x the intended amount. We caught this in testing, but I've seen production marketplaces that didn't.
const ZERO_DECIMAL_CURRENCIES = [
'BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW',
'MGA', 'PYG', 'RWF', 'UGX', 'VND', 'VUV', 'XAF', 'XOF', 'XPF'
];
function formatAmountForStripe(amount: number, currency: string): number {
if (ZERO_DECIMAL_CURRENCIES.includes(currency.toUpperCase())) {
return Math.round(amount);
}
return Math.round(amount * 100);
}
Regional Trial Exclusions
Another gotcha: we had to exclude certain Southeast Asian countries from free trial offers because fraud rates made trials economically unviable in those regions. Stripe's API lets you set this up with customer tax location checks, but you have to know it's a problem first. This is the kind of thing you only learn by running a multi-country marketplace in production.
Layer 5: Search -- The Triple Search Pattern
This is probably the most valuable architectural pattern in this article. Most directory sites offer basic text search. Good ones add location filtering. We run all three search types simultaneously and blend the results.
Full-text search (PostgreSQL tsvector): Handles exact and stemmed keyword matching. When someone searches "plumber," it also matches "plumbing." Fast, well-understood, built into Postgres.
Semantic search (pgvector + Claude embeddings): Handles meaning-based queries. "Someone who can help me feel less anxious about my relationship" doesn't contain the word "therapist," but semantic search understands the intent. We generate embeddings for each listing using Claude's API and store them as vectors in pgvector.
Geographic search (PostGIS): Handles proximity queries. "Within 25 miles of downtown Chicago" becomes a spatial query that's indexed and fast.
The blending is where it gets interesting. We weight each search type differently depending on the query:
interface SearchWeights {
textWeight: number;
semanticWeight: number;
geoWeight: number;
}
function calculateWeights(query: string, hasLocation: boolean): SearchWeights {
const isNaturalLanguage = query.split(' ').length > 4;
if (hasLocation && isNaturalLanguage) {
return { textWeight: 0.2, semanticWeight: 0.5, geoWeight: 0.3 };
} else if (hasLocation) {
return { textWeight: 0.4, semanticWeight: 0.2, geoWeight: 0.4 };
} else if (isNaturalLanguage) {
return { textWeight: 0.2, semanticWeight: 0.7, geoWeight: 0.1 };
}
return { textWeight: 0.7, semanticWeight: 0.2, geoWeight: 0.1 };
}
Short keyword queries lean on full-text search. Longer natural language queries lean on semantic search. Queries with a location component increase the geo weight. One of our directory sites runs this triple pattern across 137K+ listings, and search results are noticeably better than competitors using basic text matching.
Layer 6: Media -- Supabase Storage + Next.js Image
Directory sites are image-heavy. Listing photos, profile pictures, logos -- it adds up. We use Supabase Storage for uploads and the Next.js <Image> component for optimized delivery.
The key configuration is setting up Supabase Storage buckets with appropriate access policies (public for listing photos, private for user documents) and then using Next.js Image optimization to serve WebP/AVIF formats at the right dimensions:
<Image
src={`${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/object/public/listings/${listing.image_path}`}
alt={listing.name}
width={800}
height={600}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
loading="lazy"
/>
Next.js handles format conversion, resizing, and caching automatically when deployed on Vercel. We've seen image payload reductions of 60-70% compared to serving original uploads directly.
Layer 7: Hosting -- Vercel
All of our production directory and marketplace sites run on Vercel. The reason is straightforward: Vercel is where Next.js runs best. ISR, Server Components, edge middleware, preview deployments -- everything works without configuration.
For directory sites specifically, the edge network matters. A user in Tokyo searching a directory should get cached listing pages from a nearby edge node, not from a server in Virginia. Vercel's edge caching makes this automatic for ISR pages.
Preview deployments are also huge for marketplace projects with multiple stakeholders. Every pull request gets its own URL. The client can review the new search UI on a real URL with real data before anything goes to production.
Vercel's Pro plan at $20/month per team member covers most directory projects. Larger sites (100K+ pages) may need the Enterprise plan for higher ISR limits and dedicated support.
Layer 8: Email -- Brevo API
Email in marketplace projects falls into two categories: transactional (booking confirmations, password resets, payment receipts) and marketing (newsletters, feature announcements, re-engagement).
We use Brevo (formerly Sendinblue) for both, called from Next.js API routes:
// app/api/send-booking-confirmation/route.ts
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const { to, bookingDetails } = await request.json();
const response = await fetch('https://api.brevo.com/v3/smtp/email', {
method: 'POST',
headers: {
'api-key': process.env.BREVO_API_KEY!,
'content-type': 'application/json',
},
body: JSON.stringify({
to: [{ email: to }],
templateId: 12, // Booking confirmation template
params: bookingDetails,
}),
});
return NextResponse.json({ success: response.ok });
}
Brevo's free tier handles 300 emails/day, which is enough for early-stage marketplaces. Their paid plans start at $9/month for 5,000 emails. Compared to SendGrid or Mailgun, we've found Brevo's deliverability rates comparable and the pricing more predictable for growing projects.
Layer 9: AI -- Claude API
AI isn't a gimmick in our directory stack -- it's a core infrastructure component that handles three distinct jobs.
Semantic search embeddings: Every listing gets an embedding generated by Claude that captures its meaning. This powers the semantic search layer described above.
Content enrichment: For directories with user-submitted listings, the quality varies wildly. We use Claude to normalize descriptions, extract structured data (hours, specialties, service areas), and generate SEO-friendly summaries.
Interactive features: One of our projects runs what we call an "Oracle Council" -- five distinct AI personas that users can consult for different types of guidance. Each persona has its own system prompt, personality, and area of expertise. It sounds whimsical, but it drives significant engagement and is one of the site's most popular features.
Claude API pricing as of 2025-2026: Claude 3.5 Sonnet costs $3 per million input tokens and $15 per million output tokens. For embedding generation on a 100K listing directory, the one-time cost is roughly $50-80. Ongoing costs for search queries and chatbot interactions typically run $100-300/month depending on traffic.
Layer 10: Monitoring -- Vercel Analytics + PostHog
You need two types of monitoring for directory sites: performance metrics and user behavior analytics.
Vercel Analytics gives you Web Vitals (LCP, CLS, INP), real user monitoring, and traffic data. It's built into the Vercel dashboard and requires zero configuration. For directory sites, we watch LCP closely on listing pages -- if it creeps above 2.5 seconds, we know something's wrong with our ISR configuration or image optimization.
PostHog handles product analytics: which search queries return zero results (so we know what content gaps to fill), which listing categories get the most views, where users drop off in the booking or signup flow. PostHog's free tier covers up to 1 million events per month, which handles most early-stage marketplaces.
The combination gives you both "is the site fast?" and "are users finding what they need?" -- two very different but equally important questions.
Full Stack Comparison Table
| Layer | Our Choice | Alternative | Why We Chose Ours |
|---|---|---|---|
| Frontend | Next.js 15 | Astro, Remix | ISR for 100K+ pages |
| Database | Supabase PostgreSQL | PlanetScale, Neon | pgvector + PostGIS in one DB |
| Auth | Supabase Auth | Clerk, Auth.js | Native RLS integration |
| Payments | Stripe Connect | Paddle, LemonSqueezy | Marketplace splits, multi-currency |
| Search | Triple pattern (in-DB) | Algolia, Elasticsearch | No external sync, lower cost |
| Media | Supabase Storage | Cloudinary, S3 | Same ecosystem, simpler billing |
| Hosting | Vercel | Netlify, AWS Amplify | Best Next.js ISR support |
| Brevo API | SendGrid, Resend | Price/deliverability ratio | |
| AI | Claude API | OpenAI, Gemini | Best reasoning for content tasks |
| Monitoring | Vercel + PostHog | Datadog, Mixpanel | Free tiers cover early growth |
What This Stack Costs in Production
Let's talk real numbers for a directory site with ~50K listings and moderate traffic (50K monthly visitors):
| Service | Plan | Monthly Cost |
|---|---|---|
| Vercel | Pro | $20 |
| Supabase | Pro | $25 |
| Stripe | Pay-as-you-go | 2.9% + 30¢ per transaction |
| Brevo | Starter | $9 |
| Claude API | Usage-based | ~$150 |
| PostHog | Free tier | $0 |
| Total fixed costs | ~$204/month |
That's remarkably affordable for a production marketplace platform. The Supabase Pro plan gives you 8GB of database space, 250GB of bandwidth, and 100GB of storage -- more than enough for a directory with 50K listings.
As you scale past 100K listings and into higher traffic, expect costs to increase to roughly $500-800/month. Still dramatically cheaper than the old approach of running dedicated servers, managed Elasticsearch clusters, and separate vector databases.
If you're planning a directory or marketplace project and want to understand pricing in more detail, check out our pricing page or get in touch for a project-specific estimate.
FAQ
What's the best database for a directory website in 2026?
PostgreSQL through Supabase is our top recommendation. The combination of pgvector for semantic search, PostGIS for geographic queries, and built-in full-text search means you can handle all three search dimensions without external services. With 253K+ records across our production projects, it handles directory-scale data without issue. Alternatives like PlanetScale (MySQL-based) lack PostGIS support, making geo-search significantly harder.
Can Next.js handle 100,000+ pages for a directory site?
Yes, but you need ISR (Incremental Static Regeneration). You don't generate all 100K pages at build time. Instead, you pre-generate your highest-traffic pages (maybe the top 1,000-5,000) and let ISR generate the rest on-demand. We've done this with 137K pages in production. The key is setting appropriate revalidation intervals -- we use 3600 seconds (1 hour) for listing pages and shorter intervals for category/search pages.
How does semantic search work in a directory website?
Each listing gets converted into a numerical vector (an "embedding") that captures its meaning using an AI model like Claude. When a user searches with natural language -- "someone who helps kids with ADHD" -- that query also gets converted to a vector. The database finds listings whose vectors are mathematically close to the query vector using pgvector's cosine similarity operator. This works even when the listing doesn't contain the exact words from the search query.
Is Stripe Connect necessary for a marketplace, or can I use regular Stripe?
If your marketplace involves payments between buyers and sellers (or clients and service providers), you need Stripe Connect. Regular Stripe only lets you accept payments to your own account. Connect handles the split -- taking a platform commission and routing the remainder to the service provider. It also handles 1099 reporting for US-based sellers, which is a compliance requirement you don't want to build yourself.
How much does it cost to build a directory website from scratch?
Using the stack outlined here, your ongoing infrastructure costs start around $200/month for a mid-sized directory. Development costs vary widely based on features, but a production-ready directory with search, user accounts, and listing management typically takes 8-16 weeks to build. A full marketplace with payments, bookings, and subscriptions adds another 4-8 weeks. You can explore our directory and marketplace development capabilities for more specifics.
Should I use Algolia or Elasticsearch instead of in-database search?
For most directory sites, no. The complexity of syncing data between your primary database and a separate search service creates bugs, adds latency, and increases costs. Algolia charges based on search operations -- at scale, this gets expensive fast (their pricing starts at $1/1,000 search requests on the Build plan). PostgreSQL's built-in search capabilities, especially combined with pgvector, handle directory-scale search well. The exception: if you need typo tolerance and faceted filtering with sub-10ms response times on millions of records, Algolia is worth the complexity.
What's the difference between building a directory vs. a marketplace?
A directory lists things and lets users find them. A marketplace adds transactions -- payments, bookings, commissions, and often two-sided interactions between providers and consumers. The tech stack is largely the same, but marketplaces add Stripe Connect (or equivalent), more complex auth roles, and transactional email flows. The database schema also gets more complex with orders, invoices, and payout tracking tables.
Can I add AI features to an existing directory site?
Absolutely. The AI layer in our stack is additive, not foundational. You can add semantic search by generating embeddings for your existing listings (a one-time batch job), storing them in a pgvector column, and adding a semantic search endpoint alongside your existing text search. Content enrichment and chatbot features can be added as independent API routes. The hardest part is usually the embedding generation for a large existing dataset -- budget a few hours of processing time and $50-100 in API costs for 100K listings.