Skip to main content
Dorloter - Pet Adoption and Lost & Found Platform
In Progress
SAAS

Case study

Dorloter - Pet Adoption and Lost & Found Platform

French web and mobile platform dedicated to domestic animals, built around three functions, adoption (shelter showcase, applications, full back-office), lost and found (geolocated reports with scored matching) and certified professional boarding facilities. A TypeScript monorepo, with a NestJS REST API as a modular monolith, two distinct web apps (public showcase and pro workspace), an Expo mobile app and shared packages. Data access in typed SQL, no ORM, on PostgreSQL and PostGIS. Token authentication with rotation, shelter authorisation through a fine-grained permission matrix. Self-hostable in Europe.

Duration
Since June 2024, MVP scope delivered in 4 months
Team
1 person
Status
In Progress
Share on TwitterShare on LinkedInShare on FacebookCopy link

By the numbers

3

Business domains

Adoption, lost and found, boarding

142

REST routes

All under /api/v1

36

Tables

PostgreSQL and PostGIS

3

Clients

Public web, pro, mobile

The context

Dorloter is a French platform dedicated to domestic animals, bringing together three services that are usually scattered: shelter adoption, lost and found reporting, and a directory of certified professional boarding facilities. The MVP targets cats and dogs, designed to extend to exotic pets without a rewrite.

It serves four kinds of actors, adopters, shelters, boarding facilities and administrators, through three distinct clients: a public showcase, a professional back-office and a mobile app. I designed and built it solo, in four months. Bringing three lines of business into one product is less a sum of features than a problem of boundaries, and three constraints follow from that.

Three lines of business, one product. Adoption, lost and found and boarding share neither actors, nor rules, nor pace of change, yet they share users, a database and an API. Each domain has to evolve without dragging the others along.

Matching that has to land right. Matching a lost animal against a found one crosses proximity, physical traits and date. Sorting in the application, after pulling the candidates out of the database, does not hold up.

A volunteer is not a global role. Rights inside a shelter depend on the team you belong to, not on the role carried by the authentication token. Conflating the two leads to a hierarchy of roles that always ends up buckling.

The reuniting chain

This is the function that justifies the data stack: everything else follows from it.

From report to introduction

individual

Report

A lost or found animal is declared, with photos.

platform

Geolocation

The point is stored as geographic data, not as two numbers.

database

Proximity search

The database itself narrows opposite-type candidates to a perimeter.

platform

Scoring

Several weighted criteria give each candidate a single score.

platform

Introduction

Above the threshold, the match is offered to both reporters.

A report stays active: every new declaration is checked against those already stored, in both directions. An animal found today can match a loss reported last week.

Three consequences shaped the architecture. Position is stored as geometry rather than a pair of coordinates, which lets the database do the work. Matching is a continuous search, not a one-off query: data freshness matters as much as the accuracy of the computation. And the scoring scale sits in a pure function with no database access, which makes it verifiable without any infrastructure. That last boundary is the one that pays off most, and I come back to it below.

The technical choices

API

NestJS, modular monolith

Business modules in a single deployment, where each reaches the others only through their exported services. The boundaries of microservices without their operations, for a team of one.

Data access

Kysely, no ORM

Query typing is checked at compile time, but nothing stands between the code and the SQL. The schema is described in TypeScript and migrations remain versioned files, applied at startup.

Geospatial search

Delegated to the database

Proximity and distance are computed where the data lives, on an indexed geographic column, instead of pulling candidates out to filter them in memory.

Shelter authorisation

Permissions derived from the team

Fine-grained permissions derived from team roles. A volunteer invited in keeps the weakest global role: their rights come from membership, not from their token.

Two contract decisions round out those choices. API responses follow fixed envelopes, with stable error codes and cursor pagination: that contract has not moved since day one, so the three clients stay aligned without regeneration. And the platform carries no non-essential cookies, no third-party resources and no trackers, which exempts it from a consent banner and makes the published policy match what the code actually does.

The architecture that follows

Overview
European hosting, self-hostableproximity computed in SQLpresigned URLsPublic showcaseReact 19 · ViteProfessional consoleshelters · boarding · adminMobile appExpo · React NativeVersioned REST APINestJS · modular monolith · single deploymentPostgreSQL and PostGISversioned migrations · geospatial indexesObject storagepresigned uploads · European region
Scroll the diagram horizontally.
All three clients consume the same contract: none has a privileged path to the database.

Two shared packages keep the clients aligned without merging them: a design system common to both web apps, and an API access layer that carries tokens and their rotation. The showcase and the pro workspace have opposite audiences and opposite UX needs, so they stay two applications on two domains, and the API remains the only security boundary.

What the test suite covers

The suite is deliberately narrow. It does not chase a coverage percentage, it targets the places where a mistake is invisible on re-reading and breaks no journey: a scoring scale drifting by a point, a table forgotten in an erasure, a hash accepting what it should reject.

Static

Completeness of GDPR erasure

The real risk is not that account deletion stops working, it is that a migration adds a table tied to a user and the erasure path forgets it. The test collects mandatory foreign keys to the accounts table, compares them against what the service actually purges, and fails on any gap. A newly tied table must be purged or declared as knowingly retained with its legal justification. The check is static: it runs with no database.

Unit

Matching scale

Since the scale is a pure function, each criterion is checked in isolation across its tiers, then their sum, then the case where everything diverges and the total must fall below the introduction threshold. No database, no fixtures, so no reason not to replay these cases on every run.

Unit

Password hashing

Reference vectors from the original implementation, Unicode normalisation of the password before hashing, encode-then-verify round trip, and rejection of a malformed hash without throwing. That last point matters: an uncaught error at verification turns corrupted data into an outage.

Unit

Mobile notification routing

Translating a notification into a destination inside the app, against hostile payloads: absent, carrying no usable identifier, carrying two competing identifiers, or holding unexpected types. A notification that opens the wrong screen is a bug no rendering test catches.

Twenty-one cases, fifty-two assertions, four files, under a second to run. That is little, and deliberately so: on an MVP built solo, a short suite that runs on every commit beats a broad one you end up disabling. The erasure test is the one I would write first on any product handling accounts.

The back-office that goes with it

A shelter is not run from a list of listings. The pro console covers the legal intake and outtake registry, adoption contracts, foster homes, medical follow-up, volunteers, events, stock and communication campaigns. It is the least visible part of the product and the largest.

Boarding facilities, meanwhile, go through a human check: company number and prefecture licence are verified before publication. It is the only point in the platform where a human deliberately blocks the flow, because a directory of certified professionals that does not verify certifications is worthless.

Where the product stands

The MVP scope is delivered: a 142-route API, three clients consuming the same contract, geospatial matching delegated to the database, and GDPR compliance written into the code rather than a page. Identified work remains, including browser notifications and an exhaustive API description. The design targets cats and dogs without closing the door on exotic pets: extending is a matter of data, not of a rewrite.

The interface

Dorloter public home page
Adoption catalogue with its filters by species and age
Map of lost and found reports, with the side list of alerts
Shelter back-office dashboard, with indicators and recent applications

The public site, entry point to the product's three functions

The adoption catalogue, filterable by species, age and compatibility

Geolocated reports, lost in red and found in green on the same map

The shelter back-office, from the intake register to post-adoption follow-up

1 / 4

Lessons LearnedLessons Learned

Technical

Boundaries beat discipline

Technical

Splitting an API into modules that only talk through their public services costs a few indirections, and keeps the product changeable when one domain grows faster than the others.

Technical

Separate what the database does from what you can test

Technical

Proximity search belongs to the geospatial engine. Score computation is a pure function, verifiable without a database or fixtures. The boundary between the two is what makes matching testable.

Technical

One less ORM, more SQL

Technical

With a large share of geospatial queries, a layer translating objects into SQL costs more than it returns. Kysely gives compile-time typing without standing between the code and the query.

Design

A token's role does not say what you may do

Design

A volunteer invited into a shelter carries the weakest global role. Resolving authorisation through team membership, rather than the token's role, avoids a hierarchy of global roles that would eventually have buckled.

Navigation

Articles

Projects

Actions