HomeCase studiesA Multi-Category Local Marketplace and Classifieds Platform

Case study · Stockholm, Sweden

A Multi-Category Local Marketplace and Classifieds Platform

Online classifieds and local commerce

Classifieds platforms fail in predictable ways: search that cannot tell a vehicle from a sofa, a single listing fee that treats a private seller and a full-time business identically, and a commercial pipeline that lives in a spreadsheet nobody updates. Our team built a marketplace that solves all three — a category-driven dynamic attribute system so every category defines its own structured fields, a tiered seller account model billed across web and both mobile app stores, and an automated synchronisation layer that keeps a hosted CRM in step with every commercially meaningful product event. Delivered as a large cross-platform mobile application and a public web marketplace over a Laravel API and administrative back office.

Industry
Online classifieds and local commerce
Solution
A multi-sided marketplace platform: a token-authenticated REST API, a server-rendered public web marketplace, an administrative back office, and a feature-rich cross-platform mobile application.
Platforms
Cross-platform · Web & API
Stack
Laravel · PHP · Blade · React Native · React · MySQL
Location
Stockholm, Sweden · delivered remotely
Role
Engineering, with the team behind for mobile and QA

Delivered remotely for a business based in Stockholm, Sweden. Names withheld by agreement.

Project Overview

Industry: Online classifieds and local commerce.

Type of solution: A multi-sided marketplace platform: a token-authenticated REST API, a server-rendered public web marketplace, an administrative back office, and a feature-rich cross-platform mobile application.

Business context: A general marketplace has to serve two very different sellers from one product. A private individual lists a single item, sells it, and may not return for a year. A small business maintains a permanent presence, lists continuously, wants to be found on a map, and will pay for visibility. Treating them identically produces a product that under-serves the business and over-complicates things for the individual. At the same time, buyers arrive with wildly different expectations depending on category — the questions you ask about a vehicle have nothing in common with the questions you ask about a service.

General users: Guest browsers, individual sellers, business sellers across multiple account tiers, advertisers, back-office moderators and administrators.

General purpose: To make a multi-category marketplace genuinely searchable, to monetise sellers according to how they actually use the platform, and to keep the commercial side of the business synchronised with the product automatically rather than manually.

The Business Challenge

One search box cannot serve every category. A marketplace spanning vehicles, property, services and general goods needs structured, category-specific attributes to be filterable at all. Hard-coding fields per category is unmaintainable; omitting them makes search useless.

Two kinds of seller, one product. A private individual and a full-time business have different needs, different volumes and different willingness to pay. A single flat model either under-serves the business or puts a paywall in front of a one-off seller.

Billing has to work in three places at once. Sellers subscribe on the web with a card, and on iOS and Android through the app stores. Each rail has its own identifiers, renewal semantics and notification formats — and all three must resolve to one account tier and one entitlement state.

Payment claims cannot be trusted from the client. Both mobile stores require the server to verify purchases independently, and both send asynchronous notifications that can arrive duplicated, delayed or out of order.

Discovery is geographic. Local commerce is local. Browsing has to work on a map — showing sellers and listings across an area, filtered by what each account tier is entitled to show and by what state each listing is in — without becoming an expensive query on every map interaction.

Address lookup is a metered cost. Turning addresses into coordinates is chargeable per call and slow. At marketplace volume, doing it naively is both a bill and a latency problem.

Moderation must not break live listings. Listings need review before going live, and edits to already-live listings need review too — without the live listing changing while the edit sits in a queue.

The commercial pipeline drifts from the product. Upgrades, renewals, advertising purchases and churn all happen in the product, but the sales team works in a CRM. Manual reconciliation means the CRM is always wrong.

The mobile app is the product. Most browsing and most selling happens on a phone, including from people who have not registered yet.

Our Approach

Make the category system data, not code. We built a dynamic attribute model where each category defines its own fields, each with a type drawn from a rich set — text, numeric, decimal, currency, date, date range, year, colour, single and multi-select, and structured link fields — along with its own validation. Adding a category with a new set of attributes is a configuration task, not a development task, and search filters follow automatically from the same definitions.

Tier the account model around real usage. Seller accounts sit in a tiered structure with different listing allowances, media limits and discovery visibility, backed by settings-driven limits rather than constants, so the commercial model can be tuned without a release. Tier changes are automated from subscription state — including the reconciliation logic a downgrade requires.

Verify every purchase server-side. Both store purchase flows are verified against the provider's own servers before entitlement is granted, including token-signed requests to the store server API with automatic environment fallback, so a client-supplied receipt is never taken at face value.

Persist provider notifications before interpreting them. Every store and gateway notification is written to its own record in raw form first, and only then decoded and applied. Store notifications are deduplicated on a deterministic key derived from the provider's own payload, using an atomic first-or-create so that duplicate concurrent deliveries cannot both take effect. Months later, when a billing question arises, the original payload is still there to answer it.

Project the map instead of querying it. Map discovery reads from a purpose-built pin projection maintained alongside listings and seller profiles, carrying its own type, tier and status information. Map interaction never touches the main listing tables, and bounding-box narrowing happens before any distance computation.

Cache the metered work. Address lookups run through a dedicated caching service with its own configurable cache store, hash-based keys and a multi-day retention window, with targeted invalidation scoped to a single seller, a single listing or the pin set — so a change invalidates what it affects and nothing more.

Stage edits rather than applying them. Edits to a live listing are held as pending values alongside the live ones and promoted only on approval, so a listing under review continues to serve buyers unchanged.

Decouple the CRM with an outbox. Product code never calls the CRM inline. It marks the record as pending synchronisation, and a bank of scheduled sweepers performs the work out of band. A CRM outage becomes a backlog that drains, not a failed user action. Identity conflicts are recovered automatically: when the CRM rejects a record because it already exists, the integration extracts the existing identifier and relinks rather than failing.

Treat guests as users. The mobile application ships a complete guest experience with its own navigation tree and screens, so the marketplace can be browsed, searched and explored on a map before anyone is asked to register.

The Solution

Category-driven dynamic attributes. A three-level category hierarchy where each level can define its own structured fields across thirteen input types, with per-field validation — turning a general marketplace into a set of properly structured, filterable verticals.

Listing lifecycle with moderation. Listings move through an explicit status model covering submission, review, approval, rejection, scheduled publication, live, sold, expiring and expired, with a separate approval path for edits to live listings.

Scheduled publishing, expiry and renewal. Sellers can schedule a listing to go live at a future point; listings expire on tier-dependent timelines, with warning notifications ahead of expiry and a renewal path.

Map-based discovery. Sellers and listings appear as map pins with clustering, radius search around a point, and directions — with visibility governed by account tier and listing state.

Tiered seller accounts. A tiered account model with differentiated listing allowances, media limits and discovery presence, driven by configuration rather than hard-coded rules.

Subscription billing across three rails. Card billing on the web plus native in-app purchases on both mobile platforms, all converging on a single entitlement state, with server-side verification, provider notification handling, plan history and automated tier transitions.

Paid visibility. Duration-based listing boosts and a separate promotion mechanism, each with their own scheduling, expiry and demotion handling, purchasable through the same billing rails.

Self-serve advertising. Advertisers can request placements across multiple formats with duration-based options, moving through an approval and payment flow, with impression, click and conversion counters and exportable performance reporting.

Buyer–seller messaging. Listing-scoped one-to-one conversations with read state, unread counts and per-user blocking in both directions, plus automatic notification to active conversations when a listing is marked sold.

Push notifications and deep linking. Device token registration with duplicate cleanup and expiry maintenance, in-app notification records, and deep-link routing that takes a tapped notification to the right screen.

Favourites, following and seller profiles. Buyers can favourite listings and sellers, follow sellers and be notified of new listings, with a seller popularity model surfacing active sellers.

Full guest experience. Browsing, category navigation, search, map discovery and listing detail all available before registration, on both web and mobile.

Localisation-aware content model. Content entities carry localised variants resolved per user preference.

Administrative back office. Moderation queues, user and seller management, category and attribute configuration, plans, boosts, promotions, advertising, invoicing, CMS, FAQs and settings, with server-side paginated tables and scoped sub-administrator access.

Reporting and search visibility. Spreadsheet export of listing and advertising analytics, scheduled analytics digests, and automated sitemap generation keeping a large and changing catalogue discoverable.

Key Features

Dynamic per-category attribute system Each category defines its own structured fields across thirteen input types with independent validation, so new verticals are configured rather than coded, and filtering derives from the same definitions.

Unified entitlement across three billing rails Web card subscriptions and native in-app purchases on both mobile platforms resolve to one account tier and one entitlement state, with server-side verification and automated upgrade and downgrade.

Idempotent, auditable payment notification handling Provider notifications are persisted raw before processing and deduplicated on a deterministic key using an atomic first-or-create, so duplicate deliveries cannot double-apply and the original payload remains available for later dispute resolution.

Map-based discovery over a dedicated pin projection Seller and listing pins are served from a purpose-built projection with clustering and radius search, keeping map interaction off the main listing tables.

Cached geocoding with targeted invalidation A dedicated caching service with its own store and multi-day retention removes repeated metered address lookups, with invalidation scoped to the specific seller, listing or pin set affected.

Staged edit approval on live listings Changes to an approved listing are held as pending values and promoted only on approval, so a listing under review keeps serving buyers unchanged.

Outbox-based CRM synchronisation with conflict recovery Commercial events are flagged for synchronisation and swept out of band, so a CRM outage degrades to a backlog rather than a failed user action, with automatic relinking when the CRM reports an existing record.

Concurrency-safe seller pin reconciliation Profile pin creation runs inside a transaction with pessimistic row locks, merging duplicates and re-parenting their associations — a real fix for a real concurrency condition.

Self-serve advertising with performance tracking Multiple placement formats with duration-based options, an approval and payment flow, and impression, click and conversion tracking with exportable reporting.

First-class guest experience on mobile A complete parallel navigation tree and screen set, so the marketplace is fully browsable before registration.

Technical Architecture

Mobile client. A large cross-platform application built around a persisted state store with saga-based side effects, composing stack, bottom-tab, drawer and swipeable-tab navigators into two complete trees — one authenticated, one for guests. Device capability usage is extensive: maps with clustering and directions, geolocation and geospatial computation, camera and gallery capture with cropping, in-app purchase flows, push notification handling with deep-link routing, embedded web content and HTML rendering, and native locale detection. Networking runs through a retrying HTTP client against the REST API.

Web marketplace. A server-rendered public marketplace and administrative back office sharing the application's models and business rules, with a modern asset pipeline and server-side paginated tables for administrative volume.

API layer. A token-authenticated REST API with controllers partitioned by audience — consumer API, public web, and administrative — over shared base controllers, with role middleware separating administrator, scoped sub-administrator and combined access.

Billing and verification layer. Independent handlers per rail: a card gateway webhook processor, and store notification processors for both mobile platforms, each persisting the raw payload before interpretation. A dedicated verification service performs token-signed requests to the store server API with automatic environment fallback and manual cryptographic signature conversion, so no purchase is granted on client assertion alone.

Discovery layer. A denormalised pin projection maintained alongside listings and profiles, with type, tier and status columns supporting map queries, plus bounding-box narrowing ahead of distance computation and a cached geocoding service in front of the metered address provider.

Integration layer. An outbox pattern over the database: product code marks records pending, and scheduled sweepers perform CRM synchronisation across contacts, companies, deals, line items and invoices, with automatic recovery from identity conflicts and an override switch for suppressing synchronisation.

Scheduled operations layer. A large bank of scheduled console commands covering expiry and demotion, notification chains, subscription cancellation reconciliation, access token refresh, token registry maintenance, analytics digests, sitemap regeneration and integration sweeps.

Data layer. A relational database evolved through a long migration history, with model-level query caching over catalogue and listing entities and a distributed cache for application and geocoding data.

Flow: Mobile app and web marketplace → Token-authenticated API and server-rendered controllers → Listing, discovery and billing logic → Relational database with model-level caching → Pin projection and cached geocoding for map discovery → Store and gateway verification with raw payload retention → Scheduled sweepers → Hosted CRM, push notifications and email

Technology Stack

Category Technology
Backend language PHP 8.2
Backend framework Laravel 12
Database MySQL with Doctrine DBAL, extensive migration history
Cache Redis, with a dedicated store for geocoding data
API authentication Laravel Passport (OAuth2) and Sanctum
Web authentication Session authentication with role middleware; social sign-in via Socialite and Sign in with Apple
Query caching Model-level caching layer
Card payments Stripe, plus an additional card gateway SDK
Mobile billing Apple App Store Server API / StoreKit 2 and Google Play Billing with real-time developer notifications
Token signing JWT signing for store server API authentication
Maps and geocoding Google Maps and Geocoding, behind a caching service
CRM Hosted CRM platform SDK, synchronised through a database-backed outbox
Email SendGrid
Reporting Spreadsheet export, server-side paginated administrative tables
Image processing Server-side image processing library
Monitoring Error monitoring service with log viewer
Web delivery Blade with a modern asset pipeline, Progressive Web App support
Scheduling Scheduled console commands for expiry, notifications, integrations and maintenance
Deployment Capistrano deployment driven from CI
Mobile framework React Native 0.84 with React 19
Mobile state Redux with saga middleware and persistence
Mobile navigation React Navigation 7 (stack, bottom tabs, drawer, material top tabs), with separate authenticated and guest trees
Mobile maps Google Maps with clustering, directions, geolocation and geospatial helpers
Mobile billing Native in-app purchase integration for both platforms
Mobile services Firebase messaging, authentication, analytics and crash reporting
Mobile media Image capture and cropping, cached fast image rendering, masonry and carousel layouts
Mobile localisation Internationalisation framework with native locale detection

Technical Challenges & Solutions

Challenge Our Approach
Every category needs different structured fields, and hard-coding them per category does not scale A dynamic attribute model where each category defines its own fields across thirteen input types with independent validation, so new verticals are configured rather than coded, and search filters derive from the same definitions.
Three independent billing rails — web card, and both mobile app stores — must produce one account tier Independent handlers per rail converging on a single entitlement state, with server-side verification against each provider, plan history retained, and account tier transitions automated from subscription state including downgrade reconciliation.
Store notifications arrive duplicated, delayed and out of order, and cannot be trusted from the client Raw payloads persisted before interpretation; store notifications deduplicated on a deterministic key derived from the provider payload using an atomic first-or-create so concurrent duplicates cannot both apply; purchases verified against the provider's servers with token-signed requests and automatic environment fallback.
Map browsing across an area, filtered by tier and listing state, is an expensive query on every interaction A denormalised pin projection carrying its own type, tier and status information, maintained alongside listings and profiles, with bounding-box narrowing before any distance computation — so map interaction never touches the main listing tables.
Address-to-coordinate lookup is metered and slow A dedicated caching service with its own configurable cache store, hash-based keys and multi-day retention, with invalidation targeted to the specific seller, listing or pin set affected rather than flushed wholesale.
Listings need review, but an edit must not take a live listing down while it waits Pending values staged alongside live values and promoted only on approval, so a listing under review continues serving buyers unchanged.
The commercial pipeline in the CRM constantly drifts from what is happening in the product A database-backed outbox: product code flags records pending and scheduled sweepers perform synchronisation out of band across contacts, companies, deals, line items and invoices — so an integration outage becomes a backlog that drains rather than a failed user action, with automatic relinking on identity conflicts.
Concurrent profile updates could produce duplicate map pins and orphaned associations Pin reconciliation runs inside a transaction with pessimistic row locks on both the account and its existing pins, merging duplicates and re-parenting their listing associations in one atomic operation.
A marketplace has to be browsable before anyone will register A complete guest experience in the mobile application with its own navigation tree, screens and API surface, mirrored on the public web marketplace.

Security & Reliability

Authentication. Token-based authentication for the mobile API and session authentication with role middleware for the web and back office, alongside email verification, registration gating and social sign-in across three providers.

Layered authorisation. Controllers partitioned by audience — consumer API, public web and administrative — with separate middleware for full administrative access, scoped sub-administrator access and combined access.

Server-side payment verification. Purchases on both mobile platforms are verified against the provider's own servers with token-signed requests before any entitlement is granted, rather than trusting client-supplied receipts.

Idempotent notification processing. Store notifications are deduplicated on a deterministic key using an atomic first-or-create, so a duplicate delivery is recorded and discarded rather than applied twice.

Auditable payment history. Every provider notification is persisted in raw form before interpretation, alongside subscription records keyed to the provider's own transaction identifier, plan history and invoice records — so a billing question months later can be answered from stored evidence rather than inference.

Moderation controls. Listings are reviewed before publication, edits to live listings are reviewed separately, rejections are recorded, and archival and soft deletion are used throughout so removal is reversible.

User safety. Per-user blocking in messaging applies in both directions, with blocked relationships enforced at the query level.

Failure isolation. External integrations run out of band through scheduled sweepers, so a third-party outage produces a backlog rather than user-facing failures.

Production monitoring. Error monitoring with log inspection tooling, and crash reporting on the mobile client.

Scalability & Performance

Denormalised discovery. The map-pin projection removes the join and filter cost of map browsing from the listing tables entirely — the single highest-traffic interaction in a local marketplace.

Cached metered lookups. Geocoding results are cached in a dedicated store with multi-day retention, so repeated address resolution costs nothing in latency or spend, with invalidation scoped precisely to what changed.

Model-level query caching. Catalogue, category, attribute and plan data is read on nearly every request and changes rarely, making it an ideal caching target.

Geometric narrowing before computation. Radius search narrows by bounding box at the database level before any distance calculation runs.

Batch work off the request path. Expiry, demotion, notification chains, integration synchronisation, analytics digests and sitemap regeneration all run as scheduled work, so none of it affects user-facing response times.

Chunked generation. Sitemap generation processes the catalogue in chunks rather than loading it whole, keeping memory bounded as the listing count grows.

Paginated everywhere. API responses and administrative tables page server-side, keeping result sets bounded regardless of catalogue size.

Efficient mobile rendering. Cached image components, virtualised and masonry list rendering, marker clustering on the map, and persisted client state reduce both network traffic and re-render cost on device.

Stateless application tier. Token authentication and externalised cache and storage keep application instances free of local state.

Business Outcomes

  • A general marketplace became a set of structured verticals, because each category defines its own attributes and filters without any code change.
  • New categories are a configuration task, so the catalogue can expand at the pace of the business rather than the release cycle.
  • Sellers are monetised according to how they actually use the platform, through a tiered model whose limits are configuration rather than constants.
  • Subscriptions work identically wherever they were bought, with web and both app stores resolving to one entitlement state and automated tier transitions.
  • Billing questions can be answered from evidence, because every provider notification is retained in raw form alongside a full subscription and invoice history.
  • Duplicate provider notifications cannot double-charge entitlement, because ingestion is idempotent by design.
  • Discovery works the way local commerce works — on a map, with clustering, radius search and tier-aware visibility, without a query cost on every pan.
  • Moderation no longer penalises active sellers, because an edit under review does not take a live listing down.
  • The commercial pipeline stays in step with the product automatically, so the sales side works from current data instead of manual reconciliation.
  • The marketplace is explorable before registration, on both mobile and web, removing the wall that suppresses first-visit engagement.

Why it worked

Marketplaces look simple from the outside — a list of things, a search box, a way to message the seller. What makes them hard is everything the list implies. A category system that stays maintainable past the tenth category. A monetisation model that is a configuration surface rather than a set of constants. Billing that behaves the same whether the seller paid on a website or through an app store, and that survives providers sending the same notification twice. Discovery that works geographically without becoming the most expensive query in the system. And a commercial pipeline that reflects reality without anyone maintaining it by hand.

Our team builds for those problems specifically. We made the category system data rather than code, because a marketplace that needs a release to add a vertical will stop adding verticals. We verified every purchase against the provider's own servers and made notification ingestion idempotent, because entitlement granted on an unverified claim or applied twice is a support problem that never fully goes away. We projected map discovery into its own structure and put a cache in front of every metered lookup, because the interaction users perform most often is the one that must be cheapest. And we decoupled the CRM behind a database-backed outbox, so an integration the business depends on can fail without a seller ever noticing.

Our teams work across Laravel and modern PHP, large-scale API design, multi-provider payment and subscription integration, geospatial discovery, CRM and third-party system integration, and complex multi-tier cross-platform mobile applications — with the judgement to know which parts of a marketplace must be modelled precisely and which can be simplified.

Final Summary

A general classifieds marketplace has to be several products at once. It has to make vehicles and services and household goods equally searchable, which means structured attributes per category rather than one search box. It has to serve a private seller listing a single item and a business maintaining a permanent storefront, which means a tiered account model rather than a flat fee. And it has to bill all of that across a website and two mobile app stores, each with its own identifiers, renewal semantics and asynchronous notifications.

Our team built a platform that does all three. Categories define their own structured fields across thirteen input types, so filtering is meaningful in every vertical and new verticals are configured rather than coded. Seller accounts sit in a tiered model whose allowances and visibility rules are settings, not constants, billed across web and both mobile stores with server-side verification, idempotent notification ingestion and full raw payload retention — so entitlement is never granted on an unverified claim, a duplicate delivery cannot apply twice, and a billing question months later is answered from stored evidence. Discovery runs on a map served from a dedicated pin projection with clustering and radius search, in front of a cached geocoding service that removes repeated metered lookups.

Around that sit the things a working marketplace requires: staged edit approval so a listing under review keeps serving buyers, listing-scoped messaging with two-way blocking, tier-aware expiry and renewal chains, a self-serve advertising module with performance tracking, and a complete guest experience so the marketplace can be explored before anyone is asked to register. Commercial events reach the company's CRM through a database-backed outbox, so the sales side stays current without anyone maintaining it and without an integration outage ever reaching a seller. Delivered as a large cross-platform mobile application and a public web marketplace over a Laravel API and administrative back office.

01 — Questions

asked about this kind of project

How do you build a marketplace that spans completely different categories?

Make the category system data rather than code. Each category defines its own structured attributes with their own types and validation, and search filters derive from those same definitions. The alternative — hard-coding fields per category — works for the first three categories and becomes unmaintainable somewhere around the tenth. The test is simple: if adding a vertical needs a release, the business will stop adding verticals.

How do you handle subscriptions across a website and both mobile app stores?

Treat each rail as an independent ingestion path that converges on one entitlement state. Each has its own transaction identifiers, renewal semantics and notification formats, so trying to force them into a shared abstraction too early creates more problems than it solves. What must be shared is the outcome: one account tier, one expiry, one plan history — with transitions automated from subscription state rather than performed manually.

Why does server-side purchase verification matter?

Because a purchase claim from a client is an assertion, not a fact. Both mobile stores provide server APIs precisely so the backend can confirm a transaction independently before granting entitlement. Skipping that step means entitlement can be granted on a forged or replayed claim, and the problem usually surfaces as unexplained revenue leakage long after it starts.

How do you make payment notification handling reliable?

Two rules. Persist the raw provider payload before you interpret it, because a billing dispute six months later is answered by evidence, not by inference from derived records. And make ingestion idempotent using a deterministic key from the provider's own payload with an atomic first-or-create, because providers retry, and two concurrent deliveries of the same event must not both take effect.

How do you make map-based discovery fast?

Do not run it against your main tables. Maintain a projection specifically for map pins, carrying the type, tier and status information the map needs to filter on, and update it alongside the entities it represents. Narrow geographically with a bounding box at the database level before computing distances. Map panning is the single most repeated interaction in a local marketplace, so it has to be the cheapest.

How do you keep third-party integration costs under control?

Cache anything metered, and invalidate precisely. Address geocoding is the classic case: the same addresses resolve repeatedly, results are stable for long periods, and every call is billable. A dedicated cache store with a multi-day retention window and targeted invalidation — scoped to the specific record that changed rather than a wholesale flush — removes most of the spend and most of the latency at once.

Should a CRM integration be called inline or queued?

Neither, if you want it to survive. Flag the record as pending in your own database and let a scheduled sweeper perform the synchronisation. That is an outbox, and it means an outage at the provider becomes a backlog that drains rather than a user-facing failure. It also gives you a natural retry surface and a visible record of what has and has not synchronised — which is exactly what you want the first time the integration breaks.

How do you moderate listings without penalising active sellers?

Stage the edit, do not apply it. When a seller changes an approved listing, hold the new values alongside the live ones and promote them only on approval. Taking a live listing down while an edit sits in a review queue punishes the sellers who maintain their listings most carefully, which is precisely the behaviour you want to encourage.

03 — Similar project?

describe what is different about yours

Build yours.

Tell me what exists, what you need and the deadline. Fixed price for a defined scope, or hourly from $10 with a written estimate first.

Ahmedabad, India · IST (UTC+5:30) · --:-- IST · Mon–Fri 09:00–18:00 IST · US & EU overlap daily

No newsletter, no CRM. Just a reply.