01Full-Stack Web Application

Souls

Animal Registration & Care Platform

A multi-portal platform for registering animals, managing medical records, tracking ownership transfers, and coordinating adoptions and lost-and-found reports.

Multi-PortalQueue-Driven NotificationsGoogle OAuthFile Uploads to CDNRole-Based Access
01 // Overview

About the Project

What it is

Souls is a four-portal web platform for animal registration and care: a public website, a member (guardian) dashboard, a workspace dashboard for vets and shelters, and an admin panel backed by a single NestJS API.

Who it's for

Animal owners registering pets, veterinary clinics and shelters managing records, and platform administrators overseeing transfers, adoptions, and lost-and-found cases.

Why it exists

To give every registered animal a persistent digital identity — covering medical history, ownership chain, vaccinations, and adoption or loss events — accessible across all stakeholder types through role-separated interfaces.

02 // My Role

What I Built

Backend Developer

Designed and built the NestJS API: 18 feature modules, Prisma schemas across 12 files, JWT + session auth with Google OAuth, BullMQ queue workers, and DigitalOcean Spaces file storage.

Database Designer

Modelled the MongoDB schema using Prisma with split schema files covering animals, medical records, vaccinations, transfers, adoptions, lost-and-found reports, and a multi-channel notification system.

Frontend Developer

Built four separate React apps: the TanStack Router public website, and three Vite + React Router dashboards (member, workspace, admin) each with Tailwind, Radix UI, and React Query.

Infrastructure

Configured CORS for four deployed Vercel frontends, PM2 ecosystem for production NestJS, DigitalOcean Spaces CDN for uploaded files, and Redis with graceful degradation when unavailable.

03 // Key Capabilities

What It Can Do

•

Animal registration with unique UID, photo gallery, breed/type/color classification, tag types, and behavior profiles

•

Medical record system supporting check-ups, vaccinations, surgeries, lab tests, dental visits, and structured medication schedules with prescription details

•

Ownership transfer workflow tracking full chain of custody with type (ownership, custody, adoption, surrender) and document uploads

•

Multi-channel notification delivery via BullMQ queues with per-user preference resolution, quiet-hours scheduling, and idempotency-keyed delivery tracking

04 // Under the Hood

Engineering Deep Dive

Click any card to read the full technical detail.

05 // Engineering Decisions

Challenges & Trade-offs

The real decisions behind the architecture.

⚠ Challenge

Reference data (animal types, breeds, colors, tag types) needed fast validation on every animal write without a DB query per field.

→ Decision

Cache all active reference IDs into Redis sets and hashes at startup with in-memory fallback Sets/Maps. Validation is a single Redis SISMEMBER or HGET call.

⇄ Trade-off

Cache can drift after admin updates. A refreshAllCache() method exists to force a reload, but automated invalidation is not wired up — a stale write window exists until manual or restart-triggered refresh.

⚠ Challenge

Notification delivery needs retry logic, scheduling, quiet-hours delay, and delivery tracking without blocking the HTTP request.

→ Decision

Use BullMQ with exponential backoff (5 attempts, 3s base delay) per delivery record. Delivery records hold idempotency keys and processing leases. A 60-second reconcile loop re-enqueues stuck deliveries.

⇄ Trade-off

Requires Redis in production. The code enforces this: Redis unavailability throws a FATAL error in production but degrades gracefully in development, disabling queue features.

⚠ Challenge

Four frontends need CORS access to a single API while preventing arbitrary origins.

→ Decision

CORS is configured with an explicit allowlist of four Vercel deployment URLs plus four LAN IPs for local development. Credentials are enabled.

⇄ Trade-off

New frontend deployments require a backend code change to add the origin. There is no runtime config or environment-variable-driven allowlist.

⚠ Challenge

Redis may not be available in development environments, but the notification and animal-create queues depend on it.

→ Solution

RedisConnectionService uses lazyConnect and catches the initial connection failure. isAvailable is set to false and propagated to all queue/worker services via guard checks in onModuleInit. Features degrade silently in dev; in production a FATAL error is thrown.

✓ Result

The app boots and serves all non-queue endpoints in dev without Redis. Queue-dependent features are simply no-ops. In production, a missing Redis blocks startup rather than causing silent data loss.

⚠ Challenge

Animal creation involves multiple file uploads (photo + gallery + documents), storage to CDN, and several DB writes that must be atomic or rolled back.

→ Solution

An animal-create BullMQ queue job carries pre-uploaded filenames alongside the full DTO. The worker handles DB writes with the uploaded CDN URLs. Temp files on disk are deleted in a finally block regardless of upload success or failure.

✓ Result

File uploads are separated from DB writes; upload failures clean up temp files without leaving orphaned DB records.

⚠ Challenge

Notification quiet-hours logic must handle overnight ranges (e.g. 22:00–06:00) and respect per-notification priority (CRITICAL bypasses quiet hours).

→ Solution

The preference resolver calculates minutesSinceMidnight in the system timezone, checks isOvernight = start > end, and computes the delay-until datetime. CRITICAL priority notifications skip the quiet-hours check entirely.

✓ Result

Quiet-hours logic is self-contained in a single service and handles both same-day and overnight window ranges.

06 // Architecture

System Design

NestJS monolith with 18 feature modules under src/modules, shared core services (Prisma, BullMQ/Redis, Storage, Email, Stripe) under src/core/services, and shared guards/pipes/filters/decorators under src/common
MongoDB via Prisma ORM with the schema split across 12 domain-specific .prisma files (animal, user, medical_record, notification, transfer, adoption, lost_and_found, deceased, shared, system)
BullMQ queue layer backed by ioredis — three per-channel queues (EMAIL, SMS, PUSH) for notification delivery and a separate animal-create queue for async animal registration
Four separate React frontends (public website on TanStack Router v1; member, workspace, and admin dashboards on Vite + React Router v6) each deployed independently to Vercel
DigitalOcean Spaces (S3-compatible) for CDN-served file storage, with folder-scoped keys generated from a nanoid suffix + slugified filename
07 // Performance

What Makes It Fast

Parallel Prisma calls using Promise.all throughout

admin KPI endpoint runs 10 concurrent count queries in a single await

Reference

Reference data validation via in-memory Set/Map lookups or single Redis commands instead of DB queries per field on every animal write

BullMQ

BullMQ workers use concurrency: 3 for email and concurrency: 1 for SMS and push to avoid overwhelming external providers

DigitalOcean

DigitalOcean Spaces CDN serves uploaded files; storage keys use a nanoid + timestamp suffix to avoid cache collisions

08 // Security

Security Model

JWT access tokens (7-day expiry) and refresh tokens (30-day expiry) with separate secrets; a third reset token secret (15-min expiry) for password reset flows

JWT access tokens (7-day expiry) and refresh tokens (30-day expiry) with separate secrets; a third reset token secret (15-min expiry) for password reset flows

Session sliding-window expiry

Session sliding-window expiry: the AuthGuard checks lastActive against a per-user configurable sessionTimeout and rejects expired sessions before the JWT itself expires

Passwords hashed with bcrypt using a configurable salt rounds from environment; Google OAuth users get a nanoid(32) random password they never know

Passwords hashed with bcrypt using a configurable salt rounds from environment; Google OAuth users get a nanoid(32) random password they never know

CSRF protection via csrf-csrf and Helmet for HTTP security headers; throttling via @nestjs/throttler; raw body middleware scoped specifically to the /webhook path for Stripe signature verification

CSRF protection via csrf-csrf and Helmet for HTTP security headers; throttling via @nestjs/throttler; raw body middleware scoped specifically to the /webhook path for Stripe signature verification

09 // Deployment

How It Ships

Backend: PM2 ecosystem.config.js runs the compiled dist/main.js as a single process named nestjs_starter_pack in production mode
Four frontends deployed to Vercel (souls-admin-portal, souls-guardian-portal, souls-public-website, souls-workspace-portal) with vercel.json SPA rewrites
Swagger UI is mounted on /api/v1 in non-production environments only; a generate:swagger script builds the spec and a generate:postman script converts it to a Postman collection
Prisma schema is split into 12 domain files using the multi-file schema feature; a db:seed script populates reference data (animal types, breeds, colors, tag types, vaccination types)
10 // Gallery

Screenshots

11 // Resources

Links & Assets

01 // HERO