Sniffari
Dog-community platform: matching, events, services and Q&A, live at sniffaridogs.com

At a glance
- Platform
- Web (desktop and mobile), plus a public REST API
- Timeline
- 2026 (production delivery 14 June; 7 change rounds through 29 June)
- Role
- Solo full-stack engineer on a client-supplied front-end prototype
- Backend scope
- 17 API modules, 19 Prisma models, 6 migrations
- Front-end rewiring
- 74 files changed, 16 files added, about 4,900 lines added
- Tests
- 241 backend API tests (Jest + Supertest), 16 Vitest unit tests, 3 Playwright end-to-end smoke tests
Overview
Sniffari is a community web platform for dog owners and dog lovers: dog profiles, swipe-style playdate matching, events, a no-fee dog-sitting and walking exchange, private messaging, community Q&A, reviews, a care calendar and a blog. The client handed over a Next.js front end that ran only on a static mock-data file. I built the entire backend, connected every screen to it, and took the product to production at sniffaridogs.com.
The challenge
The hand-off looked like an app but made no network calls at all: data came from a single static file, sign-up and login did nothing, matching could not work because dogs were created with blank name, breed and gender, and the site had never been deployed. It also shipped with placeholder copy, lorem-ipsum legal pages and a wrong domain. The client needed a working product on their own VPS with real accounts, and every existing screen had to keep its design while being rewired to live data. Later rounds added requirements that touched both sides: sign-ups limited to launch countries, email verification, and an admin view.
My role
I worked alone across the whole stack, starting with the Node.js/Express/TypeScript API, which I designed and wrote from scratch (17 feature modules, a Prisma schema of 19 models evolved through 6 migrations, OpenAPI docs). On the front end, I wrote the API client and service layer, rewired the existing Next.js screens to it (74 existing files changed, 16 new files), and redesigned the sign-up/login flow and the home hero. I also wrote the test suites (backend API, front-end unit and end-to-end) and a GitHub Actions workflow, ran a code audit followed by a hardening pass, and deployed and operated the stack on the client's VPS with nginx, PM2 and Let's Encrypt. The work shipped as an initial delivery followed by seven tracked change rounds.
Architecture
Clients
Sniffari web app
Next.js 15 (App Router), React 19, Redux Toolkit, Tailwind CSS 4
The client's existing UI, rewired from a static mock file to live API data, with an auth-guard hook on private pages.
API client and service layer
lib/api.js, lib/services.js
One fetch wrapper that attaches the JWT and handles 401s centrally, plus a service function for every backend feature.
Edge/Delivery
Reverse proxy and TLS
nginx, Let's Encrypt
Serves the site on the main domain and the API on its own subdomain, with HTTP redirected to HTTPS and auto-renewing certificates.
Services
REST API
Node.js, Express, TypeScript, Zod
17 route/handler modules (auth, users, dogs, playdate, events, messages, exchange, appointments, community, reviews, blog, notifications, contact, newsletter, reports, upload, admin) with Zod request validation.
Authentication
JWT, bcryptjs, google-auth-library
Email/password and Google sign-in, 18+ registration, 6-digit email-verification and password-reset codes with expiry and attempt caps (plus a resend cooldown on verification), and role-gated admin routes.
Regional sign-up gating
fast-geoip (offline), declared-city check
Sign-ups are limited to an environment-configured list of launch countries using the declared city plus an offline IP lookup; sign-in is never blocked.
Traffic controls and ops endpoints
express-rate-limit, helmet, CORS allowlist
Strict limits on credential and code endpoints, moderate limits on mail-sending endpoints, a global throttle, and /health, /ready (database-checked) and /geo/availability endpoints.
Media
Image uploads
Multer, local disk, express.static
Profile and dog photos are stored as UUID-named files on the server's disk and served by the API with a cross-origin resource policy so the front end can embed them.
Data
Relational database
PostgreSQL, Prisma ORM
19 models (users, dogs, swipes, events and participants, messages, exchange posts, service requests, appointments, reviews, Q&A, notifications and more) managed through 6 migrations.
Third-party
Email, monitoring and analytics
Nodemailer (SMTP), Sentry, PostHog
Environment-gated integrations that do nothing when unconfigured: transactional email for verification, resets and contact/report notices, error reporting on both apps, and front-end product analytics.
Infrastructure
Hosting and process management
Ubuntu VPS, PM2
The API and the Next.js server run as a non-root user under PM2, restart automatically on reboot, and are reachable from outside only through nginx.
CI workflow
GitHub Actions
Defines an npm audit gate for both apps, backend typecheck and Jest against a Postgres 16 service, and a front-end lint, Vitest, build and Playwright run.
Key decisions
- 01
Build the backend around the existing screens
Rather than asking for a front-end rewrite, I modeled the API and Prisma schema on what the delivered screens already showed, then replaced the mock data screen by screen through a single API client. The design stayed as the client approved it; the cost was fitting some backend shapes to UI choices I would not have made from scratch.
- 02
Keep multi-step writes in one transaction
Accepting a service request updates its status, creates the booking on both the requester's and the provider's calendars, and sends a notification; sending a message also writes a notification. Both run inside Prisma transactions, so a failure part-way cannot leave a request accepted without its bookings, or a message without its notification. It adds some code in exchange for consistent data.
- 03
Match on exact dog pairs
Mutual matches are computed from exact (my dog, other dog) like pairs, not from a flat list of liked dogs. Owners with several dogs only see a match when that specific pair liked each other. The query does more work per request but no longer reports false cross-paired matches.
- 04
Region gating that never locks out real users
Sign-ups are limited to launch countries using the declared city plus an offline IP lookup; existing users can always sign in, and the country list is an environment variable.
- 05
Optional integrations degrade to no-ops
SMTP, Sentry, PostHog and Google sign-in are all switched on by environment variables. With no keys set they do nothing and form data is still saved, so nobody gets locked out. That kept local, test and early production environments working before the client's accounts were ready, at the cost of having to verify each integration by hand once it was switched on.
- 06
Fix the scaling issue at its source
The inherited UI sized type and spacing in viewport units, so everything looked zoomed-in on large monitors. I converted 2,788 font and spacing values across 70 files to fixed pixels and left the real responsive layout rules alone, which fixed the problem across the whole site instead of page by page.
Results
- Live in production at sniffaridogs.com, with the API on its own HTTPS subdomain and public OpenAPI/Swagger documentation.
- A front end that made no network calls became a working product: real accounts, Google sign-in, email verification, playdate matching, event joins with capacity limits, messaging with unread counts, a service exchange with bookings for both parties, Q&A, reviews and a read-only admin page.
- Automated API, unit and end-to-end test suites run in a GitHub Actions workflow with an npm audit gate.
- Hardening pass after a structured code audit: rate limiting, database indexes on hot paths, process-level crash handlers with Sentry, and dependency upgrades on both apps.
- Seven tracked change rounds shipped after the initial delivery, including a two-panel sign-up/login redesign, a picture-first home hero, regional availability notices and email verification.
Stack
- Frontend
- Next.js 15React 19Tailwind CSS 4Redux ToolkitMUISwiper
- Backend
- Node.jsExpressTypeScriptZodJWTbcryptjsGoogle Sign-InMulterNodemailerexpress-rate-limithelmetfast-geoip
- Data
- PostgreSQLPrisma
- Observability and analytics
- SentryPostHogSwagger/OpenAPI
- Testing and CI
- JestSupertestVitestPlaywrightGitHub Actions
- Infrastructure
- Ubuntu VPSnginxPM2Let's Encrypt