# Follow/Unfollow Transactional Logic

- **Pattern:** `ab-000760` (`community-social-engagement.follow-graph.follow-mutation-logic`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000760
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000760)

## Why it matters

Non-transactional follow mutations create race conditions (CWE-362) that permanently corrupt follower counts — a user can end up showing 5,000 followers in their count column while the actual `follows` row count is 4,200. Worse, without a unique constraint on `(followerId, followedId)`, concurrent double-taps produce duplicate follow rows that inflate counts and break feed queries. Self-following bypasses are both a data integrity failure and an embarrassing product defect. These are not edge-case concerns: follow endpoints are hammered at scale, making TOCTOU collisions routine.

## Severity rationale

Critical because non-transactional follow mutations cause irreversible count corruption under concurrent load, and absent unique constraints allow data model violations that cascade into feed and notification logic.

## Remediation

Add `@@unique([followerId, followedId])` to your Prisma `Follow` model to enforce deduplication at the database level, then wrap every follow mutation in a `db.$transaction` that updates both the relationship and the denormalized counts atomically.

```ts
// app/api/users/[id]/follow/route.ts
export async function POST(req: Request, { params }: { params: { id: string } }) {
  const userId = getCurrentUserId()
  if (userId === params.id) {
    return Response.json({ error: 'Cannot follow yourself' }, { status: 400 })
  }

  await db.$transaction([
    db.follow.create({ data: { followerId: userId, followedId: params.id } }),
    db.user.update({ where: { id: userId }, data: { following_count: { increment: 1 } } }),
    db.user.update({ where: { id: params.id }, data: { followers_count: { increment: 1 } } })
  ])

  return Response.json({ success: true })
}
```

The unique constraint ensures a duplicate follow throws a Prisma P2002 error before the transaction commits, eliminating the race window entirely.

## Detection

- **ID:** `follow-mutation-logic`
- **Severity:** `critical`
- **What to look for:** Enumerate all relevant files and Find the API endpoints or server actions for following/unfollowing users. Look for `POST /api/users/[id]/follow`, `DELETE /api/users/[id]/follow`, or server actions like `followUser`, `unfollowUser`. Verify that the operations are transactional: creating the follow relationship AND updating follower/following counts happen atomically. Check for guards against duplicate follows (unique constraint) and self-following.
- **Pass criteria:** No more than 0 violations are acceptable. Follow/unfollow endpoints exist. Database mutations use transactions or have a unique constraint preventing duplicate follows. Logic prevents users from following themselves.
- **Fail criteria:** No follow endpoints, or following is not transactional (counts can become out of sync), or duplicate follows are possible, or a user can follow themselves.
- **Skip (N/A) when:** The platform has no follow feature.
- **Detail on fail:** `"Follow logic at POST /api/follow does not use transactions — follower count and follow relationship can desynchronize"` or `"Unique constraint missing on (followerId, followedId) — same user can follow another multiple times"`
- **Remediation:** Implement follow logic with transactions and constraints:

  ```ts
  // Prisma schema
  model Follow {
    id String @id @default(cuid())
    followerId String
    followedId String
    createdAt DateTime @default(now())

    follower User @relation(name: "following", fields: [followerId], references: [id], onDelete: Cascade)
    followed User @relation(name: "followers", fields: [followedId], references: [id], onDelete: Cascade)

    @@unique([followerId, followedId]) // Prevent duplicates
    @@index([followedId]) // For efficient "followers" queries
  }

  // app/api/users/[id]/follow/route.ts
  export async function POST(req: Request, { params }) {
    const userId = getCurrentUserId()
    const targetId = params.id

    if (userId === targetId) {
      return Response.json({ error: 'Cannot follow yourself' }, { status: 400 })
    }

    const follow = await db.follow.create({
      data: { followerId: userId, followedId: targetId }
    })

    // Update counts (Prisma transactions)
    await db.$transaction([
      db.follow.create({ data: { followerId: userId, followedId: targetId } }),
      db.user.update({ where: { id: userId }, data: { following_count: { increment: 1 } } }),
      db.user.update({ where: { id: targetId }, data: { followers_count: { increment: 1 } } })
    ])

    return Response.json({ success: true })
  }

  export async function DELETE(req: Request, { params }) {
    const userId = getCurrentUserId()
    const targetId = params.id

    await db.$transaction([
      db.follow.delete({ where: { followerId_followedId: { followerId: userId, followedId: targetId } } }),
      db.user.update({ where: { id: userId }, data: { following_count: { decrement: 1 } } }),
      db.user.update({ where: { id: targetId }, data: { followers_count: { decrement: 1 } } })
    ])

    return Response.json({ success: true })
  }
  ```

## External references

- cwe CWE-362
- cwe CWE-284
- iso-25010 functional-suitability

Taxons: data-integrity, access-control

HTML version: https://auditbuffet.com/patterns/ab-000760
