The beginning of Dogwalk — when I decided to create a pet marketplace
Dogwalk·

The beginning of Dogwalk — when I decided to create a pet marketplace

It all started with a silly problem. I needed someone to walk my dog on busy days, and there was nothing decent on the market.

The apps I found were clunky to use, full of abusive fees, and the experience felt like it was designed by someone who had never owned a pet in their life. It was that afternoon in April, sitting at my computer, that I thought: “Why not make it myself?”

The spark

April 24, 2026. That day is marked because it’s when I opened the editor and started sketching the first idea. No Figma, no design system, nothing. It was just me, a terminal, and a conviction: I could do better.

# The first Dogwalk sketch (yes, it was in Python)
from enum import Enum
from datetime import datetime
from typing import Optional

class StatusPasseio(Enum):
  PENDENTE = "pendente"
  CONFIRMADO = "confirmado"
  EM_ANDAMENTO = "em_andamento"
  CONCLUIDO = "concluido"
  CANCELADO = "cancelado"

class Passeio:
  def __init__(self, tutor, passeador, pet):
  self.tutor = tutor
  self.passeador = passeador
  self.pet = pet
  self.status = StatusPasseio.PENDENTE
  self.inicio: Optional[datetime] = None
  self.fim: Optional[datetime] = None

  def confirmar(self):
  self.status = StatusPasseio.CONFIRMADO
  print(f" Walk for {self.pet.nome} confirmed with {self.passeador.nome}!")

  def iniciar(self):
  if self.status != StatusPasseio.CONFIRMADO:
  raise ValueError("Can only start a confirmed walk")
  self.inicio = datetime.now()
  self.status = StatusPasseio.EM_ANDAMENTO

  def concluir(self):
  self.fim = datetime.now()
  self.status = StatusPasseio.CONCLUIDO
  duracao = (self.fim - self.inicio).total_seconds() / 60
  print(f" Walk completed in {duracao:.0f} minutes")

This code never went to production, obviously. But it represents the moment I stopped complaining and started building. The StatusPasseio with Enum was the first design decision that’s held up to today — the booking state machine still follows these same 5 states.

Why a marketplace?

The decision to build a marketplace — not a scheduling app — came from a simple observation: the problem wasn’t just mine. Friends complained about the same lack of options. Walkers complained about the lack of clients.

It was a classic coordination problem that a platform could solve.

Mapping the market

Before writing actual code, I made a spreadsheet of competitors. The landscape was bleak:

Competitor Fee UX Coverage Mobile App
DogHero 20% Average SP/RJ only
PetBacker 18-25% Bad International
Cão Leve Fixed R$15 Terrible SP only (WebView)
Guru dos Pets 15% Good 3 capitals (slow)
Dogwalk (idea) 10% * Excellent* National * PWA*

The table made it clear: high fees and poor UX were the norm. I could compete just by doing the basics well — fair fee, app that doesn’t crash, support that answers.

The first real lines of code

After the Python prototype came the real stack. React 19 + Vite 8 on the frontend, FastAPI + PostgreSQL on the backend. The first endpoint I wrote was the walker search:

# backend/app/routers/search.py — first version
from fastapi import APIRouter, Query
from typing import Optional
from app.database import get_db

router = APIRouter(prefix="/search", tags=["search"])

@router.get("/walkers")
async def search_walkers(
  city: Optional[str] = Query(None),
  min_rating: Optional[float] = Query(None, ge=0, le=5),
  limit: int = Query(20, ge=1, le=100),
  offset: int = Query(0, ge=0),
):
  async with get_db() as db:
  query = "SELECT * FROM profiles WHERE role = 'passeador'"
  params = []

  if city:
  query += " AND city = $1"
  params.append(city)
  if min_rating:
  query += " AND rating >= $2"
  params.append(min_rating)

  query += " ORDER BY rating DESC NULLS LAST LIMIT $3 OFFSET $4"
  params.extend([limit, offset])

  rows = await db.fetch(query, *params)
  return {"walkers": [dict(r) for r in rows], "total": len(rows)}

The NULLS LAST was a detail I learned the hard way — without it, walkers without ratings would appear at the top of search results.

Design decisions that shaped the product

From the start I decided search would be map-based, like iFood/Uber. The user opens the app, sees the map with colored markers by service type, and interacts via bottom sheet.

// src/components/search/SearchMapHub.tsx — initial version
interface WalkerMarker {
  id: string;
  lat: number;
  lng: number;
  name: string;
  rating: number;
  services: ('walk' | 'boarding' | 'transport')[];
}

function WalkerMapMarkers({ walkers }: { walkers: WalkerMarker[] }) {
  const iconColors: Record<string, string> = {
  walk: '#f59e0b',  // amber → walk
  boarding: '#3b82f6', // blue → boarding
  transport: '#7c3aed', // purple → transport
  };

  return (
  <>
  {walkers.map(w => (
  <Marker
  key={w.id}
  position={{ lat: w.lat, lng: w.lng }}
  icon={{
  path: google.maps.SymbolPath.CIRCLE,
  scale: 8,
  fillColor: iconColors[w.services[0]],
  fillOpacity: 0.9,
  strokeWeight: 2,
  strokeColor: '#ffffff',
  }}
  />
  ))}
  </>
  );
}

2. Dual profile (tutor/walker)

A user can be both tutor and walker at the same time. It was a controversial decision within the team — some wanted separate accounts. But in practice, many walkers also have pets and use the app as tutors. The profiles model with switch solved it:

// src/hooks/useProfile.ts
interface Profile {
  id: string;
  name: string;
  role: 'tutor' | 'walker' | 'transporter';
  is_active: boolean;
}

function useProfile() {
  const { data: profiles, mutate } = useSWR('/profiles/me');

  const switchProfile = async (profileId: string) => {
  await api.post('/profiles/switch', { profile_id: profileId });
  mutate(); // revalidate data
  };

  return { profiles, activeProfile: profiles?.active_profile_id, switchProfile };
}

The early days

I spent the rest of the week researching competitors, jotting down features, and drawing flows in a physical notebook. No fancy tools — pen and paper solve 80% of design problems before you write a single line of code.

Dogwalk was born from a real need, not a commissioned market research. Maybe that’s why it made so much sense from the start.

What came after

In a few days, the prototype turned into an MVP with:

Feature Day 1 status Status today
Tutor registration Social login + email
Walker registration + verification + ID
City search SP only multi-city
Scheduling + Stripe Connect
Live GPS WebSocket
Chat WebSocket
Reviews after each walk
Finances statement + withdrawal

What started with 53 lines of Python on a Saturday afternoon turned into 35+ API endpoints, 3 WebSocket channels, Stripe Connect integration, and hundreds of active users.


Useful commands

~/lifelog — bash
$cat about.txt
╔══════════════════════════════════════╗
║  Samuel Medeiros                    ║
║  Senior Software Engineer           ║
║  Stack: Python · TypeScript · Rust  ║
║  Projetos: Arachne, Dogwalk,        ║
║            Capivara, TatuEngine      ║
╚══════════════════════════════════════╝
      
$