Files
frontend-helper/docs/minnect-feed-browser.spec.md
T

12 KiB

Minnect feed browser

A browser client for the Minnect expert home feed. Anyone who receives the app signs in with their own Minnect email and password. The app then reads and writes the feed as that person.

This file is the build contract. An implementation is done when the acceptance checks at the end pass. Do not invent a second login, a shared account, or extra Minnect products.

Product

Signed out, the only screen is email, password, and a sign-in button.

Signed in, the screen is a single column:

  1. The signed-in person's name and a sign-out control.
  2. A composer for a new public text post, plus an optional image or video.
  3. The home feed, newest first. Each post is a card. A control at the bottom loads the next page.

A card shows the author photo, display name, relative time, the full post text, images, video, like state, like count, and comment count. Opening comments lists them and accepts a new text comment. The signed-in person can remove their own like, and can delete a post only when they are its author.

Non-goals

Do not build ticketing, wallet, circles chat, paid questions, or any admin screen.

Read-only expert and user search is in scope, but only if a public, normal-user API for it is confirmed by the Mobilization spike (task 8kr7vfa5n). If no such API exists, do not build a search box or any search control. Search never calls admin routes. (Amended 2026-10-19: search moved out of non-goals by Cesar.)

Do not call admin routes. Do not call a route whose path contains /admin/. Do not call POST /api/v4/auth/external-services/auth-user. Do not call feed POST /api/v1/auth/login. That feed route is an admin login, not the user login.

Do not put a token, password, or session in the repository, the frontend bundle, a query string, or a log line.

Shape of the app

The browser talks only to this app. This app talks to Minnect.

browser  -- cookie session -->  this app  -- Bearer token -->  Minnect

The Minnect hosts do not allow this app's origin to call them from the browser. A same-origin server is required for that reason, and so the tokens never reach JavaScript.

Session storage holds, per signed-in person:

  • profile access token and its expiry
  • profile refresh token
  • feed access token and its expiry

The browser receives one httpOnly, Secure, SameSite=Lax cookie. The cookie is an opaque session id. It is not a Minnect token.

Hosts

Name Base URL
Profiles https://profiles.minnect.com
Feed https://feed.sharedservices.vt.com
Image CDN https://cdn.feed.sharedservices.vt.com
Video https://stream.mux.com

Send JSON as Content-Type: application/json and Accept: application/json. Send the service token as Authorization: Bearer <token>.

Login

Sign in

POST https://profiles.minnect.com/api/v4/auth/login

{ "email": "person@example.com", "password": "their-password" }

email and password are required. Do not send a device object.

Success is HTTP 200. The saved OpenAPI document does not describe that 200 body. Read the tokens with this order and stop at the first match:

  1. data.accessToken and data.refreshToken
  2. accessToken and refreshToken

Also read data.expiresIn or expiresIn when present. It is seconds.

A failed sign-in is HTTP 401 with this shape:

{ "data": null, "errors": [{ "message": "...", "code": "UnauthorizedException" }] }

Show one message: "Email or password is incorrect." Do not show the server message. The server message differs for an unknown email and a wrong password. The screen must not.

Who the person is

GET https://profiles.minnect.com/api/v4/auth/me with the profile access token.

Use data.userId, data.email, data.firstName, data.lastName, and data.profilePictureUrl. Required fields on the user object are userId, email, and role.

Feed token

POST https://profiles.minnect.com/api/v4/auth/external-services/feed

Header: profile access token. Body: {}.

Success is HTTP 200. Read the feed token in this order:

  1. accessToken
  2. data.accessToken
  3. token
  4. data.token

If none of those fields exist, sign-in fails with "Could not open the feed." Do not show the raw body.

Confirm the feed token with GET https://feed.sharedservices.vt.com/api/v1/auth/me. Keep data.id. That id is the feed author id used later to decide whether a post belongs to this person.

Refresh

Profile and feed access tokens last about one hour. Refresh when either token expires within two minutes, and once after a feed call returns 401.

  1. POST https://profiles.minnect.com/api/v4/auth/refresh-token with { "refreshToken": "<profile refresh token>" }.
  2. Replace the stored profile access token and refresh token from that response, using the same field order as sign-in.
  3. Exchange a new feed token with the call in "Feed token".

If refresh fails, delete the session and return the browser to the sign-in screen.

Sign out

POST https://profiles.minnect.com/api/v4/auth/revoke-token with { "refreshToken": "<profile refresh token>" }. Then delete the server session and clear the cookie. Sign-out still succeeds if revoke returns an error.

Feed

Read the home feed

GET https://feed.sharedservices.vt.com/api/v1/feeds/home/items?limit=20

Pass cursor with the previous page's metadata.nextCursor. Omit cursor on the first page.

The response is:

{
  "data": [],
  "metadata": { "limit": 20, "cursor": null, "nextCursor": "..." }
}

Stop paging when metadata.nextCursor is null. Ask for 20 items. Do not ask for more than 50.

Render an item only when type is "post" and post is an object. Other types, including trending-conversations, are skipped. They must not break the page.

Post card fields

From the feed item: publishedAt, likesCount, commentsCount.

From post:

Field Use
id Post id for like, comment, and delete
content Full text. Keep line breaks
type text, image, or video
isLiked Initial like state
author.id Compare with the feed user id from auth/me
author.displayName Name on the card. Fall back to first + last name
author.profilePictureUrl Avatar. It may be a redirect URL. Use it as the image source
author.slug Optional
mediaItems Images and video, in order

Image media uses imageMediumWebpUrl, then imageMediumUrl, then imageUrl.

Video media has mediaType: "video" and hlsUrl on stream.mux.com. Play it with an HLS player (hls.js or the platform equivalent). Use thumbnailUrl as the poster. A missing hlsUrl shows the thumbnail only.

Like and unlike

POST https://feed.sharedservices.vt.com/api/v1/posts/{postId}/like

DELETE https://feed.sharedservices.vt.com/api/v1/posts/{postId}/like

No body. Success is HTTP 200 and data.success: true. A second like does not increase the count. Update the card from the response, or flip isLiked and adjust the count by one when the response has no count.

Comments

GET https://feed.sharedservices.vt.com/api/v1/posts/{postId}/comments?limit=20&page=1

POST https://feed.sharedservices.vt.com/api/v1/posts/{postId}/comments

{ "content": "comment text", "type": "text", "status": "published" }

content is required. Show content, the author name, and createdAt. A comment with deletedAt set is shown as removed, without the old text.

Create a text post

  1. POST https://feed.sharedservices.vt.com/api/v1/posts
{
  "content": "post text",
  "type": "text",
  "status": "draft",
  "visibility": "public"
}
  1. POST https://feed.sharedservices.vt.com/api/v1/posts/{id}/publish with { "content": "post text" }.

The create call can return status: "draft" after the post is already on the home feed. Reload the first page of the home feed and treat the post as published when its id is there.

Create a post with an image or video

  1. Create a draft as above, with type set to image or video.
  2. POST https://feed.sharedservices.vt.com/api/v1/attachments/presigned-urls
{
  "postId": "<draft id>",
  "files": [{ "name": "photo.jpg", "type": "image/jpeg", "size": 12345 }]
}

files is 1 to 10 items. type must match image/jpeg, image/jpg, image/png, image/gif, image/webp, image/heic, video/mp4, video/quicktime, or video/webm.

  1. Upload each file with HTTP PUT to the returned presigned URL. The server does this. The browser does not receive the feed token in order to do it.
  2. POST https://feed.sharedservices.vt.com/api/v1/attachments/confirm-uploads
{
  "postId": "<draft id>",
  "uploads": [{
    "attachmentId": "<from step 2>",
    "s3Key": "<from step 2>",
    "mediaType": "image",
    "filename": "photo.jpg"
  }]
}

mediaType is image or video. Image confirmation blocks until processing finishes. Video confirmation returns while transcoding continues. Publish anyway. The card shows the thumbnail until hlsUrl exists, then the player. 5. Publish with the same publish call as a text post.

Delete

DELETE https://feed.sharedservices.vt.com/api/v1/posts/{id}

Offer this only when post.author.id equals the signed-in feed user id. Remove the card after HTTP 200.

App routes

These are this app's routes, not Minnect's.

Method Path Behavior
POST /api/session Body { email, password }. Run sign-in, profile me, and the feed-token exchange. Set the cookie. Return { name, email, pictureUrl }
DELETE /api/session Revoke, delete the session, clear the cookie
GET /api/session Return the signed-in person, or 401
GET /api/feed?cursor= One page of post cards plus nextCursor
POST /api/posts JSON text post, or multipart with content and one file
POST /api/posts/:id/like Like
DELETE /api/posts/:id/like Unlike
GET /api/posts/:id/comments Comment list
POST /api/posts/:id/comments Body { content }
DELETE /api/posts/:id Delete when the caller is the author

Every route except POST /api/session requires the session cookie. A missing or dead session is HTTP 401 with { "error": "sign_in" }. The page then shows the sign-in screen.

Screen behavior

  • Empty feed: "No posts yet." The composer stays usable.
  • Feed request failed: "The feed did not load." with a retry control. Keep the person signed in.
  • Like, comment, and publish failures name the action and leave the previous card in place.
  • The composer clears only after the new post is on the reloaded first page.
  • Long text is fully visible. Do not truncate a post.
  • The layout is one column, readable on a phone and on a desktop window.

Acceptance

Run these with two real Minnect accounts, A and B.

  1. Signed out, opening the feed route returns 401 and the sign-in screen.
  2. A wrong password shows "Email or password is incorrect." and creates no session.
  3. Account A signs in and sees the home feed, including author, text, and a working image or video on a card that has one.
  4. Account A publishes a short public text post. After reload, that post is the newest card and its author is A.
  5. Account B signs in on the same deployment, sees A's post, likes it, and comments. Account A sees the like and the comment.
  6. Account A cannot see a delete control on B's posts. Account A can delete the post from step 4, and it leaves the feed.
  7. Sign-out clears the cookie. The next feed request returns 401.
  8. A search of the built frontend assets finds no access token, refresh token, or password.

Implementation notes

  • The home-feed path that is known to work is /api/v1/feeds/home/items. The OpenAPI form /api/v1/feeds/{feedKey}/items with feedKey=home is the same resource. Use the first path.
  • Like is idempotent. Do not retry a like in a loop.
  • Publish can report draft while the post is already public. The home feed is the check that matters.
  • Saved feed JSON in this repository is an old sample. Do not use it as the data source. The live calls above are the source.