The Hard Part of Building a Dog-Matching App Wasn't the Code
TL;DR: I built Fennaro, a dog-matching app for owners of big and working breeds, solo and end to end. I assumed the matching algorithm or the maps would be the hard part. They were not. The real problems were human: people are in denial about their own dogs, exact locations are dangerous to store, app stores get in the way of a launch, and a local network product is worthless without density. The biggest single fix was deleting compatibility code I had already shipped.
I spent months building a dog-matching app this year. Going in, I thought the challenge would be technical. Geo queries, a compatibility score, real-time chat, moderation. I am a developer, so of course I framed it as a code problem.
The code was the easy part. Postgres with PostGIS handles the geography. Drizzle handles the schema. Hono and oRPC give me a typesafe API. None of that kept me up at night. What kept me up at night was everything the code was standing in for: how dogs actually get along, how to keep people safe, and how to get two strangers with dogs into the same park at the same time. This is the honest version of what that taught me.
The real problem was people, not dogs
The pitch sounds simple. Match dogs, arrange a walk. But the problem I was actually solving is on the Fennaro homepage in one line: it is for the dogs people cross the street to avoid.
Owners of big breeds, a Cane Corso, a Rottweiler, a Malinois, live with a specific kind of rejection. Their dog can be calm and friendly and still get treated like a threat. Small-dog owners scoop their dog up and walk the other way. In Poland this is not just social, it is legal. Breed-specific legislation puts extra permits and muzzling rules on certain breeds regardless of the individual dog's temperament. So a big, gentle dog ends up isolated, and its owner runs out of places to find a normal, calm walk.
That is the real problem. It is not "there is no app for this". It is that these owners have been quietly pushed out of dog social life, and the app is only useful if it undoes that instead of repeating it.
Here is the part that surprised me. When I dug into the problem, the hardest people to design for were not the ones being rejected. They were the ones in denial. Plenty of owners genuinely believe their reactive dog is "just friendly" and would be fine at a chaotic dog park. If I built a product that trusted what owners say about their own dogs, I would be setting up bad meetups and someone would get bitten. The product could not be a self-report questionnaire with a happy ending. It had to be honest about dogs in a way their owners sometimes are not.
That reframed the whole thing. I stopped calling the meetups "playdates" and started calling them what they should be: calm walks. Not a free-for-all in a field. Two owners agreeing on a quiet place to say hi. The language change was small. The product change behind it was not.
Lesson 1: Compatibility is pairwise, so I deleted my own filters
My first version did the obvious engineering thing. I built hard filters. Sizes had to be within one step. Energy levels had to be close. Certain temperaments were banned from meeting certain others: a reactive dog should never be shown an anxious one. If two dogs failed a rule, they never saw each other. Clean, safe, done.
It was wrong, and a walk with real dog owners made that obvious. Their insight was one sentence, and it broke my design: a dog is not globally "social". It depends entirely on the other dog. The same calm giant that terrifies a nervous terrier is the perfect steady companion for another big dog. "Social" is not an attribute you can store on a profile. It only exists between two specific dogs.
That is what pairwise compatibility means, and it is the opposite of a filter. My hard rules were hiding good matches to prevent bad ones, and in a small city where there might be six big dogs total, hiding matches is fatal. An empty feed is a dead product.
So I deleted the filters. Not softened, deleted. The feed now shows every dog within range and ranks them instead of hiding them. Under the hood, compatibility is scored from zero to ten for each specific pair, on the fly: size, energy, play style, and shared temperament traits. That score sorts the feed and turns into a plain label, a great match, a good match, or worth a look. Where there is a real concern, a big size gap, a risk flag, the app does not silently drop the dog. It shows a small honest warning that the owner has to acknowledge before they can even like it.
The lesson that stuck: my instinct as a developer was to encode safety as exclusion. The right answer was to encode it as information and let two humans make the call. Deleting that filtering code was the best decision in the whole project, and it was still code I was proud of when I wrote it.
Lesson 2: Location privacy without ever storing a home address
A "dogs near you" app is a stalking tool if you build it carelessly. The whole product depends on location, and the users are disproportionately women walking large dogs, often alone, in parks. If my database ever leaked and it contained exact coordinates, I would have handed someone a map to strangers' front doors. That was not acceptable, so the rule I set was simple and absolute: never store an exact location. Not even encrypted. Just never have it.
The naive fixes do not hold up. Rounding coordinates leaks the corner of the grid you sit in. Storing a real point and only showing an approximate one still means the real point is in the database waiting to leak. I needed the stored value itself to be safe.
What I landed on is deterministic, secret-seeded fuzzing, closer in spirit to geohashing than to rounding. When a user sets their area, I take their real point and shift it by a random offset inside a roughly 500 metre disc. The direction and distance of that shift come from an HMAC keyed by a server secret plus a coarse, roughly one kilometre grid cell. The only location I ever write to the database is that shifted point.
Two properties fall out of that, and both matter:
- A database dump does not expose anyone's exact home coordinates. Without the server secret you cannot reverse the offset, so what leaks is only an intentionally shifted point a few hundred metres from a home, not the home itself.
- You cannot average it away. Because the offset is deterministic for a given user in a given cell, probing the app repeatedly always returns the same fuzzed point. There is no jitter to sample and cancel out.
There is a nice safety detail too. When someone crosses a cell boundary, say they move house to get away from someone, they get a completely fresh, unlinkable offset. The old fuzzed location cannot be tied to the new one. Every match query, including the distance to the person doing the search, runs on these fuzzed points only, and distances are shown rounded to the nearest kilometre with a one kilometre floor. Nobody ever sees "220 metres away".
The honest footnote: this reduces precision on purpose, and I accept that. A product that respects location has to be a little less exact than one that does not care.
Lesson 3: I shipped a PWA because the app store was the bottleneck
I planned to ship a normal Android app. Then I hit the reality of Google Play's closed testing rules: you need a group of testers running the app continuously for two weeks before you are even allowed to publish. For a pre-launch product with no users yet, that is a chicken and egg problem. I could not get testers because there was nothing to install, and I could not publish the thing to install because I had no testers.
So the app ships as an installable progressive web app. The same build runs on an iPhone added to the Home Screen from Safari, on an Android phone that offers to install it, and right in the browser on a laptop. One line on the homepage now does a lot of quiet work: it runs in your browser, on your phone or your computer. That sentence exists because the PWA made it true.
This was not a shortcut, it created its own work. I wrote the service worker by hand and made one deliberate choice: it caches the app shell for offline loading and never caches a single API response. On a shared or borrowed phone, I did not want anyone's messages or matches sitting in a cache after they close the tab. The marketing site and the app also live on separate domains, fennaro.com and app.fennaro.com, so the public, indexable pages and the logged-in product never share an origin or leak into each other.
The takeaway: the platform's rules are part of your architecture whether you like it or not. The app store gate did not just delay me, it changed what I built, and the change was an improvement.
Lesson 4: A local network product is dead without density
This is the lesson I understood last and felt hardest. Fennaro is a local network product. Its entire value is other real dogs near you, right now. That means it does not launch like a normal app.
A normal app works for the first user. A notes app is useful with one person. Fennaro is useless with one person. The first owner in a city opens it, sees an empty map, and leaves, and they were exactly the person I needed to keep. Every early user is both my customer and my supply, and if the two sides of that are not in the same neighbourhood at the same time, nothing happens.
That reframes launch completely. It is not a marketing problem, it is a density problem. Spreading thin across a whole country would guarantee empty feeds everywhere. So the plan is the opposite: start city by city, get enough big-dog owners in one place that the map is not empty, then move to the next. Match radius even has to flex with how many dogs are actually around.
The other thing density forced me to confront is the obvious objection: why not just a Facebook group? Groups exist. They are also a wall of posts with no way to know if two dogs actually fit, no privacy on where you live, and no structure to a meetup. The answer could not be "mine is an app". It had to be the things a group genuinely cannot do: match two specific dogs, protect your location, and turn a match into a calm, planned walk. If the product did not clearly beat a free Facebook group, it had no reason to exist.
What I would tell a developer starting this
If you are a developer about to build something for real people, not a demo, here is what I wish I had internalised earlier.
- The hard part is almost never the code. It is understanding the humans the code serves, and they will not describe their problem accurately. The big-dog owners did not ask for a matching algorithm. They asked, in effect, to stop being treated like a threat.
- Be willing to delete your best work. My proudest early feature, the compatibility filter, was the thing most in the way. Falling out of love with your own code on schedule is a skill.
- Decide your non-negotiables before you write a line. "Never store an exact location" shaped the entire data model. Bolting privacy on afterwards would have been impossible.
- The platform is part of your architecture. App store rules, browser capabilities, and where your domains live are design constraints, not paperwork.
Fennaro is launching in Poland now, big breeds especially welcome, because those are the owners who needed it most. If you want to see it, the homepage is Fennaro, and you can open the real thing at app.fennaro.com. It runs in your browser, no install required.
I still love the engineering. But this project made the lesson concrete in a way no tutorial could: I got paid back not for the code I wrote, but for finally understanding the problem well enough to know which code to throw away. That is the same way I try to approach every product I build, and if you have something real that needs building end to end, let's talk.
Sources: Breed-specific legislation, Wikipedia, Progressive web apps, MDN, Learn PWA, web.dev, Geohash, Wikipedia.
