How I Built KittenPlein
I built KittenPlein, a Dutch kitten marketplace because the existing experience for finding a kitten in the Netherlands felt too noisy, too generic and too dependent on a few photos and a price. The product idea was simple: make it easier for buyers to compare kittens, nests and breeders with more context, and give responsible sellers a cleaner place to present their availability.
The engineering problem was more interesting than a simple listings website. A useful marketplace needs search, trust signals, moderation, account flows, payments, private files, emails, analytics and enough SEO structure to be discoverable without feeling like a pile of generated landing pages.
KittenPlein became a focused full stack project built with Next.js App Router, React, TypeScript, PostgreSQL, Drizzle ORM, Auth.js, Mollie, Resend and a self hosted deployment. This post explains why I made those choices, how the product is structured, and what I learned from building a real vertical marketplace rather than another portfolio demo.
Why KittenPlein Exists
Most marketplaces treat every category almost the same. That works for furniture or second hand electronics, but it is awkward for pets. When someone wants to kitten kopen in Nederland, they are not only comparing price. They are checking age, breed, health information, socialisation, documents, location, whether the mother cat can be seen and whether the seller communicates responsibly.
That shaped the product from the beginning. KittenPlein needed to answer three questions quickly:
- Can buyers browse available kittens by breed, location and practical trust signals?
- Can breeders and private sellers create complete listings without friction?
- Can I moderate the marketplace before content becomes public?
The first version could have been a thin CRUD app. I intentionally did not stop there, because the value of a marketplace is in the system around the listing. The listing is only the object. The product is comparison, trust and follow up.
That is why the live site includes pages for veilig kitten kopen, geverifieerde fokkers, breed specific pages such as Ragdoll kittens te koop, and seller focused pages like kittens verkopen via KittenPlein.
The Stack I Chose
I used Next.js because the product needed both application behaviour and search friendly pages. The App Router maps well to this split: public marketplace pages can be server rendered and cached, while account, admin and payment flows can use Server Actions and route handlers.
The core stack is intentionally boring in the best way:
- Next.js App Router for routing, metadata, server rendered pages and API routes
- React and TypeScript for the interface and type safe product logic
- PostgreSQL with Drizzle ORM for relational marketplace data
- Auth.js for credentials and Google sign in
- Mollie for Dutch friendly payments, including iDEAL and recurring subscriptions
- Resend and React Email for transactional emails
- Tailwind CSS for fast, consistent UI work
- Biome for linting and formatting
- Docker Compose on a VPS for deployment with PostgreSQL and Nginx
The result is a system that is still easy to reason about locally. Listings, users, payments, conversations, reports and analytics are all visible in one relational model. That matters because marketplace bugs often happen between features, not inside isolated components.
Designing the Data Model Around Trust
The most important decision was to make trust part of the schema instead of burying it in free text. A kitten listing has normal marketplace fields like title, description, price, city and images, but it also has structured fields for the things buyers actually care about.
Here is a simplified version of the listing model:
export const listings = pgTable("listings", {
id: serial("id").primaryKey(),
userId: integer("user_id").references(() => users.id),
title: text("title").notNull(),
description: text("description").notNull(),
breed: text("breed").notNull(),
breedSlug: text("breed_slug").notNull(),
priceCents: integer("price_cents").notNull(),
province: text("province").notNull(),
city: text("city").notNull(),
isVaccinated: boolean("is_vaccinated").notNull().default(false),
isChipped: boolean("is_chipped").notNull().default(false),
hasPassport: boolean("has_passport").notNull().default(false),
hasPedigree: boolean("has_pedigree").notNull().default(false),
parentsVisible: boolean("parents_visible").notNull().default(false),
visitsAllowed: boolean("visits_allowed").notNull().default(true),
status: listingStatusEnum("status").notNull().default("pending"),
});This structure lets the UI do more than display paragraphs. Buyers can filter on health checks, vaccination, chip status, pedigree, visible parents, visits and availability. The search result page becomes a comparison tool instead of a feed.
It also gives the admin area better queues. A listing can be pending, approved, rejected or paused. Health documents have their own status. Breeder verification has its own status. That separation keeps moderation explicit and makes the system easier to extend.
Building Search Pages That Are Useful And Indexable
SEO was not an afterthought. A vertical marketplace lives or dies by whether people can find the right entry point. Someone searching for a Maine Coon kitten has different intent from someone searching for a general kitten checklist.
KittenPlein has several layers of public discovery:
/kittensfor the national marketplace overview/kittens/[breed]for breed pages/kittens-te-koop/[city]and province routes for location intent/fokkersand/fokker/[slug]for breeder discovery/kenniscentrum/[slug]for educational content
The breed pages are not empty SEO shells. They combine real listing data, breed guidance, related buying guides, internal links and structured metadata. That is important because search pages should help users make a decision, not only capture traffic.
In the listing query layer, the same filter model powers the UI and the routes:
export interface ListingFilters {
breed?: string;
province?: string;
city?: string;
query?: string;
minPrice?: number;
maxPrice?: number;
verifiedOnly?: boolean;
healthCheckedOnly?: boolean;
vaccinatedOnly?: boolean;
chippedOnly?: boolean;
pedigreeOnly?: boolean;
parentsVisibleOnly?: boolean;
visitsAllowedOnly?: boolean;
availableNow?: boolean;
sort?: "newest" | "price-asc" | "price-desc";
}This is the kind of small design choice that pays off later. The route does not need a separate mental model from the filter bar. The page metadata, canonical behaviour, pagination and listing grid all derive from the same search parameters.
From a portfolio perspective, this is one of the parts I like most. It shows that technical SEO is not only metadata. It is information architecture, internal linking, content freshness, route design and crawl control working together.
Marketplace Moderation Comes Before Growth
I did not want every submitted listing to go live immediately. For this category, moderation is part of the product promise. New listings start as pending unless the user is allowed to auto approve. Admins can approve or reject listings, review breeder verification, inspect health documents and handle reports.
That led to a dedicated admin cockpit rather than a hidden script. The admin area includes moderation queues, users, reports, payments, audit logs, operations and analytics. This is deliberately operational. If a marketplace succeeds, the backoffice becomes just as important as the public interface.
The moderation model also affects public queries. Public listing selection filters out unavailable or restricted accounts, so a suspended or banned seller does not keep surfacing in search. This is easy to miss in marketplace builds. User state and listing state have to be evaluated together.
I also added reporting flows for listings and conversations. That means the product can respond to suspected scams, misleading information, duplicate listings, welfare concerns and inappropriate messages. It is not glamorous work, but it is exactly the kind of feature that makes a marketplace safer over time.
Payments And Monetization
The monetization model is intentionally simple:
- A free account can have one active listing
- Pro unlocks more active listings, a breeder profile and priority placement
- Featured boosts give a listing extra visibility for a limited period
- Homepage featured spots create a scarce premium placement
Mollie was the right fit because the product is Dutch and iDEAL matters. The webhook implementation is where most of the real care lives. The app verifies the webhook secret, fetches the payment from Mollie instead of trusting the request body, validates metadata and amount, stores invoices, sends emails and records product analytics.
One detail I like is the use of a transaction and advisory lock around webhook processing:
await db.transaction(async (tx) => {
await tx.execute(
sql`select pg_advisory_xact_lock(hashtext(${payment.id}))`,
);
const [existingPayment] = await tx
.select({ id: payments.id })
.from(payments)
.where(eq(payments.molliePaymentId, payment.id))
.limit(1);
if (existingPayment) {
return;
}
// Create invoice, update plan or listing visibility,
// then send the transactional email after the database work.
});Payment providers retry webhooks. Networks fail. Users refresh checkout pages. This kind of defensive handling is what turns a demo checkout into a production feature.
The plan enforcement job is another example. When Pro expires, excess listings are paused and can be restored when Pro is renewed. That keeps the commercial rules honest without deleting seller data.
Private Uploads And Image Handling
Listings need images, but uploads come with tradeoffs. I chose local private storage behind controlled routes instead of treating every file as a public asset. The upload code validates file size and extension before writing to disk. Listing images, parent images and health documents are handled differently because they have different privacy requirements.
Public listing images can be served through /api/images, while breeder verification documents and health documents are only available through account or admin routes with permission checks. This distinction matters. A health document can support trust without becoming public content.
There is also a practical deployment reason behind the storage setup. The production Docker volume for uploads survives app rebuilds and restarts. That keeps deployment simple while avoiding the classic mistake of storing runtime user uploads inside an image layer that disappears on redeploy.
Product Analytics For A Real Feedback Loop
I wanted KittenPlein to measure marketplace quality, not only page views. The app records product events such as listing views, chat starts, messages, first replies, listing approvals, Pro purchases and featured boost purchases.
That makes it possible to answer better questions:
- Which listings get views but no chats?
- Do trust signals increase contact rate?
- Which sellers reply quickly?
- Which breeds have supply but low approval rate?
- Which free users are likely Pro candidates?
The admin analytics code combines product events with listings, conversations, favorites and users. This is the difference between generic analytics and product analytics. Google Analytics can tell me traffic is up. The internal event model can tell me whether the marketplace is becoming more useful.
The UX Principle: Slow The User Down Just Enough
For many products, reducing friction is the main goal. For KittenPlein, the goal is more nuanced. Buyers should be able to browse quickly, but the interface should encourage careful comparison before contact. Sellers should be able to place an advert without frustration, but they should also provide enough information to make the listing trustworthy.
That shows up in small pieces of UI:
- Search filters include trust fields, not only breed and price
- Listing cards show practical signals instead of only decorative badges
- Breed pages link to buying guides before reservation
- The placement flow asks sellers to confirm welfare and platform compliance
- Account restrictions can block placing listings, messaging or buying visibility
The product is trying to create a calmer marketplace. Not slower in the annoying sense, but calmer in the decision making sense.
CI, Deployment And Operations
KittenPlein runs as a real deployed application, so I built the boring parts too. The repo includes a Dockerfile, Docker Compose setup, Nginx config, database migrations, backup scripts, cron routes, smoke tests and CI documentation.
The CI pipeline installs dependencies, runs Biome, starts PostgreSQL, applies Drizzle migrations, builds the app and runs smoke tests against public routes like the homepage, kitten search, pricing, FAQ, knowledge center and robots.txt.
That coverage is intentionally practical. It does not pretend to prove everything. It proves that the app can build, migrate and serve the routes that matter most to discovery and conversion.
For a project like this, operational maturity is part of the feature set. A marketplace that cannot deploy safely cannot be moderated safely. A paid product without invoices, backups and webhook logging is not finished.
Tradeoffs And Things I Would Revisit
The current architecture is pragmatic, but it has tradeoffs.
Local uploads are simple and cost effective, but object storage would be a natural next step as traffic and upload volume grow. A VPS deployment keeps control high, but a managed database and external storage would reduce operational responsibility. The admin analytics are useful, but they could eventually move into more formal dashboards with cohort tracking and alerting.
The SEO surface is already broad, so the ongoing challenge is quality control. Breed, city and guide pages only help if they stay useful, accurate and connected to real inventory. Programmatic SEO works best when it is grounded in product data and editorial care.
I would also keep investing in trust. More structured verification, clearer seller response metrics and better buyer education would all make the marketplace stronger. In this category, growth without trust would be the wrong win.
Summary
KittenPlein started as a simple idea, build a better Dutch marketplace for kittens, but the useful version required a full product system. The interesting engineering was not any single feature. It was the way search, SEO, listings, moderation, payments, uploads, emails, analytics and operations had to support the same goal.
For me, this project is a good example of how I like to build: start with the user problem, model the domain carefully, keep the stack understandable and then add the operational pieces that make the product real.
You can explore the live marketplace at kittenplein.nl, browse available kittens in the Netherlands, or read how the platform approaches safe kitten buying.