MeetyyAPI
Documentation / API Reference / Following

Follow API

Following is a one-way, independent relationship that sits alongside connections. A follows B without B doing anything, and following or unfollowing never changes connection_status in either direction.

  • Base URL: /api/v1
  • Auth: every endpoint here requires Authorization: Bearer {token} (Sanctum).
  • Content type: application/json

Response envelope (all endpoints):

// Success                                  // Error
{                                           {
  "status": "success",                        "status": "error",
  "message": "…",                             "message": "…",
  "data": { }                                 "data": { }
}                                           }

Switch-over: everything below is live. Flip the frontend constant:

// src/features/networks/constants/follow-api.ts
export const IS_FOLLOW_API_LIVE = true;

The rules that matter

1. Follow and connection are independent. Every combination of the two is reachable and valid: connected-and-following, connected-not-following, following-not-connected, neither. Never derive one from the other.

2. Follow and unfollow are idempotent. Following someone you already follow returns 200, not 422. Unfollowing someone you do not follow returns 200. Optimistic buttons can retry safely.

3. Connecting follows, but disconnecting does not unfollow.

Action Connection effect Follow effect
POST /feed/connections/{user_id} (send request) → pending_outgoing Creates the follow, same transaction
POST /feed/connections/{user_id}/accept → connected None. Only the requester follows
DELETE /feed/connections/{user_id} (remove/cancel) → none None. The follow stands
POST /feed/users/{user_id}/follow None → following
DELETE /feed/users/{user_id}/follow None → not following
POST /users/{user_id}/block Connection deleted Follow deleted, both directions

4. Follow is always immediate. There is no follow-request state and no private-profile gate. The UI's assumption is correct.

5. is_following is the viewer's state. It is false on your own profile and false for a guest. The counts (followers_count, following_count) are public facts about the subject.


New endpoints

Follow a user

POST /api/v1/feed/users/{user_id}/follow

{user_id} is the user id, matching POST /feed/connections/{user_id} — not the username.

200 OK

{
  "status": "success",
  "message": "Followed.",
  "data": { "is_following": true, "followers_count": 1284 }
}

followers_count is the subject's count after the write, so the optimistic counter can be reconciled against it.

Status When
200 Followed, or already following (no-op)
422 Following yourself, or either party has blocked the other
404 No such user

Unfollow a user

DELETE /api/v1/feed/users/{user_id}/follow

200 OK

{
  "status": "success",
  "message": "Unfollowed.",
  "data": { "is_following": false, "followers_count": 1283 }
}

Returns 200 whether or not a follow existed. Unfollowing a connection leaves the connection intact — the header falls back to the connected state, not to "not connected".

Followers list

GET /api/v1/feed/users/{username}/followers?page=1&per_page=15

Keyed by username, matching the profile reads it sits beside. Newest follow first.

Following list

GET /api/v1/feed/users/{username}/following?page=1&per_page=15

Same shape and ordering. Both return the standard connection-user row, so each row carries its own Connect and Follow button:

{
  "status": "success",
  "message": "Followers retrieved.",
  "data": {
    "users": {
      "data": [
        {
          "id": "9d1f…",
          "name": "Sarah K.",
          "username": "sarah.k",
          "bio": "Product designer",
          "avatar_url": "https://…",
          "connection_status": "none",
          "is_connected": false,
          "is_following": true,
          "is_me": false
        }
      ],
      "links": { "first": "…", "last": "…", "prev": null, "next": null },
      "meta": { "current_page": 1, "from": 1, "last_page": 1, "per_page": 15, "to": 1, "total": 1 }
    }
  }
}

Note — these require auth. The request asked for public reads, but the entire /feed prefix is behind auth:sanctum, and the sibling GET /feed/users/{username}/connections is auth-only too. Consistency won. Tell us if you need them genuinely public and we will move them out of the feed group.


Changes to existing endpoints

Every field below is additive. Nothing was renamed or removed, so shipping the frontend before or after this lands is safe either way.

GET /user/{username} — three new fields

{
  "data": {
    "user": {
      "connections_count": 128,
      "posts_count": 42,
      "followers_count": 1284,   // NEW — public
      "following_count": 310,    // NEW — public
      "connection_status": "connected",
      "is_connected": true,
      "is_following": true,      // NEW — the viewer's state; false on your own profile
      "is_me": false
    }
  }
}

GET /feed/users/{username} — the same three fields

followers_count, following_count and is_following, in the same positions.

POST / DELETE /feed/connections/{user_id} — is_following added

All three connection mutations now report the resulting follow state so the UI never has to guess:

// POST /feed/connections/{user_id}          → 201
{ "connection_status": "pending_outgoing", "is_following": true }

// POST /feed/connections/{user_id}/accept   → 200
{ "connection_status": "connected", "is_following": false }   // accepting does NOT follow

// DELETE /feed/connections/{user_id}        → 200
{ "connection_status": "none", "is_following": true }         // removing does NOT unfollow

List surfaces — is_following on every user row

Added to the shared connection-user object, so it appears on all of these at once:

Endpoint Object
GET /feed/connections connection-user
GET /feed/connections/pending connection-user
GET /feed/connections/sent connection-user
GET /feed/users/{username}/connections connection-user
GET /feed/users/{username}/followers connection-user
GET /feed/users/{username}/following connection-user
GET /search (people results) connection-user + headline, role
GET /feed/suggestions suggested-profile
Any endpoint returning posts post.author

Post authors carry it inside the existing author object:

"author": {
  "id": "7d2e…",
  "username": "john_doe",
  "name": "John Doe",
  "avatar_url": null,
  "role": "mentor",
  "connection_status": "connected",
  "is_connected": true,
  "is_following": false,   // NEW
  "is_me": false
}

Suggestions are worth one note: connection_status there is always "none" (the suggestion query excludes anyone you have a connection edge with), but is_following is really computed — a follow does not disqualify a suggestion, so a suggested profile may well be someone you already follow.

total_reviews on the mentor profile

mentor_profile now carries total_reviews next to average_rating:

"mentor_profile": {
  "average_rating": "4.90",
  "rating_count": 128,     // pre-existing, unchanged
  "total_reviews": 128,    // NEW — same number, the name you type it as
  "total_sessions": 412
}

Served on both GET /user/{username} and GET /mentee/mentors. 0 for an unreviewed mentor, never null. You can delete placeholderReviewCount() in src/features/mentor/constants/mentor-card.constants.ts.

rating_count was already being served under that name and other callers use it, so both ship. They are always the same number — read whichever you prefer, and prefer total_reviews in new code.

One asymmetry to be aware of on the mentee dashboard's spotlight (GET /mentee/dashboard/featured-mentor): average_rating stays null for an unreviewed mentor while total_reviews reports 0. That is deliberate — a null average is the signal you already use to hide the star indicator, where 0.00 would render as a mentor nobody liked. A count has no such misreading.


Notification

feed.new_follower is now emitted, on the existing notification channels — it arrives live over the websocket and lands in GET /notifications like any other type. Nothing further is needed on the frontend for it to appear.

{
  "type": "feed.new_follower",
  "follower_id": "9d1f…",
  "follower_name": "Sarah K.",
  "follower_username": "sarah.k"
}

Fired only on a genuinely new follow. A retried optimistic press does not ring the bell twice.


Answers to the open questions

1. Is there an existing followers table? No — this is new. The only prior trace was a migration renaming a post-visibility enum, unrelated. The unique index on (follower_id, followed_id) was added as suggested, which is what makes the no-op cases free.

2. Should blocking drop the follow both ways? Yes, and it does. Blocking already tore down the connection edge; it now deletes the follow in both directions. A one-way leftover would keep one party in the other's feed, which is a half-block.

3. Do private profiles need a follow request state? No. Follow is always immediate — the Follow record has no status column at all. The UI's assumption is correct.