pdelabs logo
    sun

    demoda

    A clothing marketplace where the hard part was never the catalogue.
    whale-tale
    RoleStaff augmentation — product, mobile, backend and platform
    PlatformsiOS · Android · Web
    Timeline2022 – 2024
    mobilewebapisearchpaymentsstaff augmentation
    demoda is a marketplace for clothing and accessories that lets small brands, second-hand stores, fashion designers and individuals sell their products. It covers the entire sales process — discovery, buyer-to-seller messaging, online payments, shipping and order tracking.

    We joined the team as an embedded engineering group and worked across the whole stack: the Flutter app, the GraphQL API, the Python services behind it and the AWS infrastructure it all runs on.
    App preview
    App preview

    The problem

    An order is a negotiation, not a checkout

    On a marketplace, buying is not one transaction — it is a multi-day exchange between two strangers, either of whom can simply stop replying. Money sits in the middle the whole time. The system has to know what happens when a seller never confirms, when a package is never shipped, when a buyer never collects.

    Search runs on other people's data

    Nobody normalises a marketplace catalogue. Sellers write their own titles and descriptions from their phones — inconsistent casing, misspellings, brand names spelled six ways, Spanish and English mixed in the same listing. Matching a query against that is not a SQL LIKE.

    Sellers are individuals, not merchants

    The seller side had to work for someone photographing a jacket on their bed, not for a warehouse with an ERP. That shapes onboarding, payment setup, image handling and every empty state in the app.

    What we built

    1. 01

      An order state machine with a timeout on every edge

      The order lifecycle is modelled explicitly rather than inferred from a status column, and every state where the system waits on a human has its own timeout handler. Nothing waits forever.
      • Payment, pickup, shipping and waiting-for-shipment each have a dedicated timeout path
      • Unhappy paths are first-class: seller rejects, buyer reports a problem, order is never collected
      • Totals are calculated server-side, so the client can never be the source of truth for money
      • Every transition is a handler that can be tested in isolation
    2. 02

      Split payments, with each seller paid into their own account

      Sellers connect their own MercadoPago account through an OAuth flow, so funds settle to them directly rather than pooling in a platform wallet that has to be reconciled later.
      • OAuth onboarding per store, with automatic token refresh
      • Payment preferences created server-side and reconciled from MercadoPago webhooks
      • Webhook events drive order state transitions, not the app
      • A separate bank-transfer withdrawal flow for seller balances
    3. 03

      Search on OpenSearch, not on the database

      Product search runs against a purpose-built OpenSearch index with its own loader and a query builder that carries its own test suite — because relevance is a thing you regress, and you only notice if you test it.
      • Dedicated indexing pipeline, decoupled from the transactional database
      • Query builder unit-tested against real query shapes
      • Machine learning in the pipeline for relevance beyond keyword matching
    4. 04

      Hasura as the API surface, with the schema in version control

      The read and write API is GraphQL over PostgreSQL via Hasura. What matters is that none of it lives only in a console: permissions, relationships and actions are committed metadata, reviewed in pull requests like any other code.
      • Migrations and Hasura metadata committed alongside the application code
      • Row-level permissions expressed as data, not scattered through handlers
      • Custom business logic behind Hasura Actions, served by Python services
      • The Flutter client generates its types from the live schema, so a breaking change fails the build
    5. 05

      One Flutter codebase across iOS, Android and web

      The app is organised feature-first — roughly thirty self-contained contexts, each owning its screens, state and queries — which is what makes a codebase this size survive more than one team rotation.
      • Riverpod for state, Freezed for immutable models, go_router for navigation
      • GraphQL types generated from the schema rather than hand-written
      • Sentry, Firebase Analytics and Remote Config wired in from the start
    6. 06

      Per-branch environments from infrastructure as code

      The AWS footprint is defined in CloudFormation stacks, and every deploy label gets its own API endpoint. Reviewing a change means opening the environment it created, not reading a diff and hoping.
      • Separate stacks for core services, shared resources and the image pipeline
      • Image processing with HEIC support, delivered from a dedicated edge stack
      • Secrets held in AWS Secrets Manager, never in the repository

    The stack

    Mobile & web
    FlutterDartRiverpodFreezedgo_routerGraphQL codegen
    API
    HasuraGraphQLPostgreSQL
    Services
    PythonFastAPIAWS LambdaPants
    Search
    OpenSearchTensorFlowTransformers
    Payments
    MercadoPago
    Platform
    AWS CloudFormationS3FirebaseSentry
    As co-founder of demoda, I am extremely impressed with the work pdelabs did helping in the development of our marketplace. They solved technically challenging problems such as real time messaging, complex order state machines and much more. Their commitment to understanding our vision and their ability to translate it into a functional and user-friendly platform was remarkable.

    - Nicolas Ferro, co-founder

    Have something like this to build?

    Schedule a call now

    We love to take on new challenges, tell us yours.

    Schedule a call

    Or
    if your prefer taking it offline, write us
    via email
    We will get back to you in less than 24 hs.