Skip to main content

Overview

Macondo Link Manager is built with a domain-driven architecture where the backend serves as the single source of truth and the frontend acts as a declarative consumer. This design ensures consistency, predictability, and scalability. The project is organized as a monorepo containing:
  • Backend (API): Node.js + Fastify + Prisma
  • Frontend (Web): Next.js 14 with App Router
/
├── api/        # Backend (Fastify + Prisma)
│   ├── src/
│   ├── prisma/
│   └── Dockerfile

├── web/        # Frontend (Next.js 14 + App Router)
│   ├── src/
│   └── next.config.ts

└── README.md

Domain Model

The system follows a hierarchical domain structure:
Client
 └── Campaign
      └── Link
           ├── Tags (many-to-many)
           └── Clicks

Entities

Represents an agency client. Clients own campaigns and links.Relationships:
  • Has many Campaigns (one-to-many)
  • Has many Links (one-to-many)
Cascade behavior:
  • Deleting a client deletes all associated campaigns and links
Represents a marketing campaign associated with a client.Relationships:
  • Belongs to one Client (many-to-one)
  • Has many Links (one-to-many)
Cascade behavior:
  • Deleting a campaign sets campaignId to NULL on associated links (SetNull)
Categorization labels that can be applied to links.Relationships:
  • Has many Links (many-to-many via LinkTag)
Behavior:
  • Tags are sent as string[] (array of names)
  • Non-existent tags are created automatically
  • Updates replace the entire tag set transactionally
  • No duplicate tags or orphaned relationships
Records each click event on a shortened link.Properties:
  • timestamp: When the click occurred
  • ipAddress: IP address of the visitor
  • userAgent: Browser/device information
  • country: Determined via GeoIP
  • city: Determined via GeoIP
  • isBot: Whether the click is from a bot
  • botReason: Explanation if detected as bot
Bot detection occurs at write-time, not read-time. The isBot field is persisted, ensuring metrics remain consistent.

Database Schema

The platform uses PostgreSQL with Prisma ORM. Below is the complete schema:
// User model - Agency employees
model User {
  id        String   @id @default(uuid())
  email     String   @unique
  name      String
  avatarUrl String?
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  links Link[]

  @@map("users")
}

// Client model - Agency clients
model Client {
  id        String   @id @default(uuid())
  name      String   @unique
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  links     Link[]
  campaigns Campaign[]

  @@map("clients")
}

// Campaign model - Marketing campaigns
model Campaign {
  id        String   @id @default(uuid())
  name      String
  clientId  String
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  client Client @relation(fields: [clientId], references: [id], onDelete: Cascade)
  links  Link[]

  @@map("campaigns")
}

// Tag model - Link categorization
model Tag {
  id        String   @id @default(uuid())
  name      String   @unique
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  links LinkTag[]

  @@map("tags")
}

// Link model - Shortened links
model Link {
  id          String   @id @default(uuid())
  originalUrl String   @map("original_url") @db.Text
  shortCode   String   @unique @map("short_code")
  userId      String   @map("user_id")
  clientId    String   @map("client_id")
  campaignId  String?  @map("campaign_id")
  createdAt   DateTime @default(now()) @map("created_at")
  updatedAt   DateTime @updatedAt @map("updated_at")

  user     User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  client   Client    @relation(fields: [clientId], references: [id], onDelete: Cascade)
  campaign Campaign? @relation(fields: [campaignId], references: [id], onDelete: SetNull)

  tags   LinkTag[]
  clicks Click[]

  @@index([userId])
  @@index([clientId])
  @@index([campaignId])
  @@map("links")
}

// LinkTag model - Many-to-many junction table
model LinkTag {
  linkId     String   @map("link_id")
  tagId      String   @map("tag_id")
  assignedAt DateTime @default(now()) @map("assigned_at")

  link Link @relation(fields: [linkId], references: [id], onDelete: Cascade)
  tag  Tag  @relation(fields: [tagId], references: [id], onDelete: Cascade)

  @@id([linkId, tagId])
  @@index([tagId])
  @@map("link_tags")
}

// Click model - Click tracking
model Click {
  id        String   @id @default(uuid())
  linkId    String   @map("link_id")
  timestamp DateTime @default(now())
  ipAddress String?  @map("ip_address")
  userAgent String?  @map("user_agent") @db.Text
  country   String?
  city      String?
  isBot     Boolean  @default(false)
  botReason String?  @map("bot_reason")

  link Link @relation(fields: [linkId], references: [id], onDelete: Cascade)

  @@index([linkId])
  @@index([timestamp])
  @@map("clicks")
}

Key Schema Features

  • UUIDs for all primary keys
  • Cascade deletes for maintaining referential integrity
  • Indexes on foreign keys and timestamp columns for query performance
  • Text fields for potentially long content (URLs, user agents)
  • Optional fields marked with ? for flexible data capture

Technology Stack

Backend (API)

Fastify

High-performance web framework with schema validation and plugin architecture

Prisma

Type-safe ORM for PostgreSQL with migrations and query builder

Zod

Runtime schema validation for API inputs and outputs

JWT

Secure authentication with JSON Web Tokens stored in httpOnly cookies
Key Backend Dependencies:
{
  "fastify": "^5.0.0",
  "@prisma/client": "^6.0.0",
  "@fastify/jwt": "^10.0.0",
  "@fastify/oauth2": "^8.1.2",
  "@fastify/swagger": "^9.5.2",
  "zod": "^4.1.12",
  "geoip-lite": "^1.4.10",
  "maxmind": "^5.0.3"
}

Frontend (Web)

Next.js 14

React framework with App Router for server components and routing

TanStack Query

Powerful data fetching, caching, and state management

shadcn/ui

Beautifully designed components built with Radix UI and Tailwind

React Hook Form

Performant forms with Zod validation
Key Frontend Dependencies:
{
  "next": "^14.2.5",
  "react": "^18.3.1",
  "@tanstack/react-query": "^5.90.11",
  "axios": "^1.13.2",
  "react-hook-form": "^7.68.0",
  "zod": "^4.1.13",
  "tailwindcss": "^4",
  "recharts": "^2.15.4"
}

Infrastructure

  • Vercel: Frontend hosting with edge network and automatic deployments
  • Railway: Backend API and PostgreSQL database hosting
  • Docker: Containerization for local development and deployment
  • Custom Domains: Production URLs at mcd.ppg.br

Architectural Principles

The system follows these core principles:
  • All business logic resides in the backend
  • Frontend never performs domain calculations
  • API returns complete, ready-to-render data models
  • State management is simplified on the client
  • Metrics use persisted state (e.g., isBot field)
  • No runtime calculations or heuristics for critical data
  • Aggregations are computed by the backend
  • Historical data remains consistent over time
  • Critical operations use database transactions
  • Tag updates replace the entire set atomically
  • No partial states or race conditions
  • Cascade deletes maintain referential integrity
  • Create and Update operations follow the same contract
  • Same validation rules apply to both
  • Predictable API behavior
  • Easier client implementation

Backend Responsibilities

The backend handles:
  1. Authentication & Authorization
    • Google OAuth integration
    • Domain-based access control
    • JWT token generation and validation
  2. Data Aggregation
    • Count campaigns per client
    • Count links per client and campaign
    • Calculate click metrics (by date, browser, country, city)
    • Filter bot traffic
  3. Business Logic
    • URL shortening and validation
    • Tag management (create, associate, sync)
    • QR code generation
    • Bot detection at write-time
  4. Data Persistence
    • CRUD operations for all entities
    • Transaction management
    • Database migrations
    • Referential integrity

Frontend Responsibilities

The frontend handles:
  1. User Interface
    • Responsive design with Tailwind CSS
    • Component composition with shadcn/ui
    • Dark/light theme support
  2. Data Fetching & Caching
    • TanStack Query for server state
    • Optimistic updates
    • Cache invalidation
    • Loading and error states
  3. Form Management
    • React Hook Form for performance
    • Zod schema validation
    • Client-side validation feedback
  4. Routing & Navigation
    • Next.js App Router
    • Dynamic routes for clients, campaigns, links
    • Server components where applicable
The frontend never performs domain calculations like counting, filtering, or aggregating. It only displays data provided by the backend.

Data Flow

Here’s how data flows through the system:
  1. User submits form in frontend
  2. Frontend validates with Zod schema
  3. POST request sent to /api/links
  4. Backend validates with Zod
  5. Backend creates link record
  6. Backend associates tags (creates new ones if needed)
  7. Backend returns complete link object with tags
  8. Frontend updates cache and displays success

Click Tracking Flow

  1. User visits shortened link (https://li.mcd.ppg.br/{shortCode})
  2. Backend receives redirect request
  3. Backend extracts IP, user agent
  4. Backend performs bot detection
  5. Backend determines country/city via GeoIP
  6. Backend creates click record with isBot flag
  7. Backend redirects to original URL
  8. Metrics queries only count clicks where isBot = false

Dashboard Metrics Flow

  1. Frontend requests dashboard data
  2. Backend queries database with joins and aggregations
  3. Backend filters out bot clicks
  4. Backend groups by date, browser, country, city
  5. Backend returns structured metrics
  6. Frontend renders charts using Recharts
  7. TanStack Query caches result

Authentication Flow

JWTs are stored in httpOnly cookies to prevent XSS attacks. The frontend never accesses the token directly.

Read Models

The backend provides optimized read models for list views: Client List:
{
  id: string
  name: string
  campaignCount: number  // Aggregated by backend
  linkCount: number      // Aggregated by backend
  createdAt: string
}
Campaign List:
{
  id: string
  name: string
  clientId: string
  clientName: string     // Joined by backend
  linkCount: number      // Aggregated by backend
  createdAt: string
}
Link List:
{
  id: string
  originalUrl: string
  shortCode: string
  clientId: string
  campaignId: string | null
  tags: { id: string; name: string }[]  // Joined by backend
  clickCount: number     // Aggregated by backend
  createdAt: string
}
These read models eliminate the need for frontend calculations and ensure consistent data representation across all views.

Scalability Considerations

Database

  • Indexed foreign keys for fast joins
  • Indexed timestamps for time-based queries
  • Partitioning strategy for clicks table (future)
  • Connection pooling via Prisma

API

  • Fastify’s high-performance event loop
  • Stateless design for horizontal scaling
  • Swagger documentation for API discovery
  • Health check endpoint for monitoring

Frontend

  • Static generation where possible
  • Server components to reduce client JS
  • Code splitting via Next.js
  • TanStack Query caching to reduce API calls

Monitoring & Health

The API provides a health check endpoint:
GET /health
Response:
{
  "status": "ok",
  "dbConnection": "healthy"
}
Use this endpoint for:
  • Container health checks
  • Load balancer probes
  • Uptime monitoring
  • CI/CD pipeline validation

Next Steps

API Reference

Explore all available endpoints

Core Features

Learn about link management and features

Database Guide

Manage schema changes with Prisma

Deployment

Deploy to production environments

Build docs developers (and LLMs) love