Posts / Timeline Domain — Story Bundles¶
POST-STORY-001 — Create a post ✅ [CORE]¶
AS A logged-in user I WANT to publish a short-form post SO THAT my followers can see it in their timeline feed
SCENARIO 1: Successful post creation¶
Scenario ID: POST-STORY-001-S1
GIVEN
The user is authenticated (valid JWT)
The post text is non-empty and within the allowed length
WHEN
The user submits the post form
THEN
A post record is created in the
postsschema withauthor_id,text, andcreated_atThe API returns
201 Createdwith the new post object
SCENARIO 2: Empty post rejected¶
Scenario ID: POST-STORY-001-S2
GIVEN
The user submits a post with an empty or whitespace-only text body
WHEN
The request reaches the Posts Service
THEN
The API returns
422 Unprocessable Entitywith error codePOST_TEXT_REQUIREDNo record is created
SCENARIO 3: Post exceeds maximum length¶
Scenario ID: POST-STORY-001-S3
GIVEN
The post text exceeds 280 characters
WHEN
The request reaches the Posts Service
THEN
The API returns
422 Unprocessable Entitywith error codePOST_TEXT_TOO_LONGNo record is created
SCENARIO 4: Unauthenticated request rejected¶
Scenario ID: POST-STORY-001-S4
GIVEN
The request carries no JWT or an expired JWT
WHEN
POST /postsis called
THEN
The API returns
401 Unauthorized
Architecture reference: Chapter 1 — Introduction and Goals, Chapter 5 — Building Block View, Chapter 6 — Runtime View
POST-FE-001.1 — Compose post UI¶
AS A logged-in user I WANT a text input area with a submit button to compose a post SO THAT I can publish content from the timeline page
SCENARIO 1: Post submitted and appears in feed¶
Scenario ID: POST-FE-001.1-S1
GIVEN
The user is on the timeline page and has typed text into the compose area
WHEN
The user clicks “Post”
THEN
A
POST /postsrequest is sent with the Bearer JWTOn
201 Createdthe new post is prepended to the feed without a full page reloadThe compose area is cleared
Architecture reference: Chapter 3 — Context and Scope
POST-FE-001.2 — Compose form validation¶
AS A logged-in user I WANT the compose form to enforce length limits before submission SO THAT I get immediate feedback without a round-trip to the server
SCENARIO 1: Character counter warns near limit¶
Scenario ID: POST-FE-001.2-S1
GIVEN
The user is typing in the compose area
WHEN
The text length reaches 260 characters
THEN
A character counter shows remaining characters in amber
WHEN
The text length exceeds 280 characters
THEN
The counter turns red and the submit button is disabled
Architecture reference: Chapter 3 — Context and Scope
POST-BE-001.1 — POST /posts endpoint¶
AS A Posts Service
I WANT to expose POST /posts
SO THAT authenticated users can create posts
SCENARIO 1: Valid post is persisted¶
Scenario ID: POST-BE-001.1-S1
GIVEN
The request carries a valid RS256 JWT
The body contains
{"text": "<non-empty string ≤280 chars>"}
WHEN
POST /postsis called
THEN
A row is inserted into
posts.posts (id, author_id, text, created_at)Response is
201 Createdwith{id, author_id, text, created_at}
SCENARIO 2: JWT validated locally without calling Users Service¶
Scenario ID: POST-BE-001.1-S2
GIVEN
The Posts Service has the RS256 public key in
JWT_PUBLIC_KEY
WHEN
Any authenticated request arrives
THEN
The token is verified locally; no HTTP call to the Users Service is made for auth
Architecture reference: Chapter 5 — Building Block View, Chapter 6 — Runtime View, Chapter 9 — Architecture Decisions
POST-INFRA-001.1 — Posts Service containerised and deployable¶
AS AN operator I WANT the Posts Service packaged as a Docker image SO THAT it can be deployed independently behind the nginx reverse proxy
SCENARIO 1: Container starts and serves traffic¶
Scenario ID: POST-INFRA-001.1-S1
GIVEN
A Docker image is built from the Posts Service source
DATABASE_URLandJWT_PUBLIC_KEYare set as environment variables
WHEN
The container starts
THEN
The service is reachable on its configured port
GET /healthreturns200 OK
Architecture reference: Chapter 7 — Deployment View
POST-INFRA-001.2 — Posts schema migration¶
AS AN operator
I WANT an Alembic migration that creates the posts.posts table
SO THAT the Posts Service can persist user content
SCENARIO 1: Migration creates posts table with required index¶
Scenario ID: POST-INFRA-001.2-S1
GIVEN
A PostgreSQL instance has no
postsschema tables
WHEN
alembic upgrade headis run for the Posts Service
THEN
Table
posts.postsexists with columns(id, author_id, text, created_at)The
(author_id, created_at DESC)index is managed by the feed-story migration (POST-INFRA-002.2)
Architecture reference: Chapter 7 — Deployment View, Chapter 9 — Architecture Decisions
POST-INFRA-001.3 — Event handling¶
Not applicable — post creation is a synchronous operation at this stage. No async fan-out events are triggered (ADR-004 uses fan-out on read).
POST-INFRA-001.4 — Monitoring and alarms for Posts Service¶
AS AN operator I WANT a health check endpoint and an error-rate alert on the Posts Service SO THAT I am notified when post creation is degraded
SCENARIO 1: Health check returns 200¶
Scenario ID: POST-INFRA-001.4-S1
GIVEN
The Posts Service is running and connected to its database
WHEN
GET /healthis called
THEN
Response is
200 OKwith{"status": "ok"}
SCENARIO 2: Alert fires on elevated error rate¶
Scenario ID: POST-INFRA-001.4-S2
GIVEN
Prometheus scrapes
/metricson the Posts Service every 15 seconds
WHEN
The HTTP 5xx error rate exceeds 1% for 5 consecutive minutes
THEN
An alert fires and is routed to the on-call channel
Architecture reference: Chapter 7 — Deployment View, Chapter 8 — Cross-Cutting Concepts
POST-STORY-002 — Read aggregated timeline feed ✅ [CORE]¶
AS A logged-in user I WANT to see a paginated feed of posts from users I follow SO THAT I can stay up to date with their content
SCENARIO 1: Timeline returns posts from followees¶
Scenario ID: POST-STORY-002-S1
GIVEN
The authenticated user follows at least one user who has posts
WHEN
The user requests their timeline feed
THEN
Posts from all followees are returned, sorted by
created_at DESCEach post includes the author’s
usernameandavatar_urlThe response is paginated (default page size: 20)
SCENARIO 2: Empty feed for user with no followees¶
Scenario ID: POST-STORY-002-S2
GIVEN
The authenticated user follows nobody
WHEN
The user requests their timeline feed
THEN
The API returns
200 OKwith an emptyitemsarray and pagination metadata (page,page_size,has_next)
SCENARIO 3: Pagination returns correct page¶
Scenario ID: POST-STORY-002-S3
GIVEN
The user’s followees have published more than 20 posts in total
WHEN
The user requests page 2 (
?page=2&page_size=20)
THEN
The response contains posts 21–40 sorted by
created_at DESCThe response includes
page,page_size, andhas_nextmetadata
SCENARIO 4: Feed loads within performance budget¶
Scenario ID: POST-STORY-002-S4
GIVEN
The authenticated user follows up to 1 000 users
WHEN
The timeline feed is requested
THEN
The response is returned in under 500 ms (p95)
Architecture reference: Chapter 1 — Introduction and Goals, Chapter 5 — Building Block View, Chapter 6 — Runtime View, Chapter 9 — Architecture Decisions
POST-FE-002.1 — Timeline feed page¶
AS A logged-in user I WANT an infinite-scroll timeline page that loads posts from my followees SO THAT I can browse content without manual pagination
SCENARIO 1: Initial feed loads on page open¶
Scenario ID: POST-FE-002.1-S1
GIVEN
The authenticated user navigates to
/timeline
WHEN
The page mounts
THEN
A
GET /feedrequest is sent with the Bearer JWTUp to 20 posts are rendered, each showing author avatar, username, text, and timestamp
SCENARIO 2: Next page loads on scroll to bottom¶
Scenario ID: POST-FE-002.1-S2
GIVEN
The user has scrolled to the bottom of the currently loaded posts
has_nextistruein the last response
WHEN
The scroll threshold is crossed
THEN
A
GET /feed?page=<next>request is sentNew posts are appended below existing ones without replacing them
Architecture reference: Chapter 3 — Context and Scope
POST-BE-002.1 — GET /feed endpoint¶
AS A Posts Service
I WANT to expose GET /feed
SO THAT authenticated users can retrieve their aggregated timeline
SCENARIO 1: Feed query aggregates followee posts (fan-out on read)¶
Scenario ID: POST-BE-002.1-S1
GIVEN
The request carries a valid JWT containing
user_idThe
users.followstable has the caller’s followee IDs (read via internal API call to Users Service)
WHEN
GET /feed?page=1&page_size=20is called
THEN
The Posts Service obtains the followee ID list from the Users Service via the follow-graph contract (candidate:
GET /users/{user_id}/followees; cross-context interface to be formally defined in architecture docs)A single query selects from
posts.posts WHERE author_id = ANY(:followee_ids) ORDER BY created_at DESC LIMIT 20 OFFSET 0Response is
200 OKwith{items: [...], page, page_size, has_next}
POST-BE-002.2 — Followees cross-context call (candidate contract)¶
AS A Posts Service
I WANT to retrieve a user’s followee list from the Users Service
SO THAT the feed query knows which author_id values to include
Note: GET /users/{user_id}/followees is a candidate cross-context contract pending formal definition in architecture/05-building-block-view.md.
SCENARIO 1: Followee list returned successfully¶
Scenario ID: POST-BE-002.2-S1
GIVEN
The Posts Service calls the Users-domain follow-graph contract (candidate:
GET /users/{user_id}/followees) with the Bearer JWT
WHEN
The Users Service processes the request
THEN
A list of
followee_idvalues is returnedThe Posts Service uses this list as the
author_idfilter in the feed query
POST-INFRA-002.1 — Posts Service containerised and deployable¶
Covered by POST-INFRA-001.1 — the same container serves /feed.
Architecture reference: Chapter 7 — Deployment View
POST-INFRA-002.2 — Feed query index migration¶
AS AN operator
I WANT an Alembic migration that ensures the (author_id, created_at DESC) index exists on posts.posts
SO THAT fan-out-on-read feed queries meet the 500 ms performance budget
SCENARIO 1: Index present after migration¶
Scenario ID: POST-INFRA-002.2-S1
GIVEN
The
posts.poststable exists (POST-INFRA-001.2 has run)
WHEN
The migration for POST-STORY-002 is applied
THEN
EXPLAIN ANALYZEon the feed query shows an index scan on(author_id, created_at DESC)Migration is idempotent (
CREATE INDEX IF NOT EXISTS)
Architecture reference: Chapter 7 — Deployment View, Chapter 9 — Architecture Decisions
POST-INFRA-002.3 — Event handling¶
Not applicable — the timeline feed is a synchronous read; no async events are triggered (ADR-004: fan-out on read).
POST-INFRA-002.4 — Feed latency alert¶
AS AN operator I WANT an alert when feed response latency exceeds the 500 ms quality goal SO THAT performance degradation is caught before users notice
SCENARIO 1: Latency alert fires on p95 breach¶
Scenario ID: POST-INFRA-002.4-S1
GIVEN
Prometheus scrapes
/metricson the Posts Service and recordshttp_request_duration_seconds
WHEN
The p95 latency for
GET /feedexceeds 500 ms for 5 consecutive minutes
THEN
An alert fires and is routed to the on-call channel
Architecture reference: Chapter 7 — Deployment View, Chapter 8 — Cross-Cutting Concepts
POST-STORY-003 — Like / unlike a post ✅ [Supporting]¶
AS A logged-in user I WANT to like or unlike a post SO THAT I can express appreciation for content
SCENARIO 1: Successfully like a post¶
Scenario ID: POST-STORY-003-S1
GIVEN
The authenticated user has not already liked the post
WHEN
The user clicks the like button on a post
THEN
A like record is created in
posts.likesThe post’s like count increments by 1
The API returns
200 OK
SCENARIO 2: Unlike a post¶
Scenario ID: POST-STORY-003-S2
GIVEN
The authenticated user has already liked the post
WHEN
The user clicks the like button again
THEN
The like record is removed from
posts.likesThe post’s like count decrements by 1
SCENARIO 3: Like is idempotent¶
Scenario ID: POST-STORY-003-S3
GIVEN
The authenticated user has already liked the post
WHEN
POST /posts/{id}/likeis called again
THEN
Response is
200 OKwith no duplicate like record created
Architecture reference: Chapter 5 — Building Block View
POST-BE-003.1 — POST /posts/{id}/like and DELETE /posts/{id}/like endpoints¶
AS A Posts Service
I WANT to expose POST /posts/{id}/like and DELETE /posts/{id}/like
SO THAT authenticated users can like and unlike posts
SCENARIO 1: Like created¶
Scenario ID: POST-BE-003.1-S1
GIVEN
The request carries a valid JWT
No existing like from this user on this post
WHEN
POST /posts/{id}/likeis called
THEN
A row is inserted into
posts.likes (post_id, user_id, created_at)Response is
200 OKwith updatedlike_count
SCENARIO 2: Unlike removes like¶
Scenario ID: POST-BE-003.1-S2
GIVEN
A like record exists for this user and post
WHEN
DELETE /posts/{id}/likeis called
THEN
The row is deleted from
posts.likesResponse is
200 OKwith updatedlike_count
Architecture reference: Chapter 5 — Building Block View
POST-INFRA-003.1 — Posts Service containerised and deployable¶
Covered by POST-INFRA-001.1.
Architecture reference: Chapter 7 — Deployment View
POST-INFRA-003.2 — likes table migration¶
AS AN operator
I WANT an Alembic migration that creates the posts.likes table
SO THAT like relationships can be persisted
SCENARIO 1: Migration creates likes table¶
Scenario ID: POST-INFRA-003.2-S1
GIVEN
The
postsschema exists (POST-INFRA-001.2 has run)
WHEN
The migration for POST-STORY-003 is applied
THEN
Table
posts.likesexists with columns(post_id, user_id, created_at)A composite primary key on
(post_id, user_id)prevents duplicate likes
Architecture reference: Chapter 7 — Deployment View
POST-INFRA-003.3 — Event handling¶
Not applicable — likes are synchronous; no async events required.
POST-INFRA-003.4 — Monitoring¶
Covered by POST-INFRA-001.4.
Architecture reference: Chapter 7 — Deployment View
POST-STORY-004 — Edit / delete own post ✅ [Supporting]¶
AS A logged-in user I WANT to edit or delete a post I authored SO THAT I can correct mistakes or remove content
SCENARIO 1: Successfully edit a post¶
Scenario ID: POST-STORY-004-S1
GIVEN
The authenticated user is the author of the post
The new text is non-empty and ≤280 characters
WHEN
The user submits the edited text
THEN
The
posts.postsrow is updated with the new text and anedited_attimestampThe API returns
200 OKwith the updated post
SCENARIO 2: Successfully delete a post¶
Scenario ID: POST-STORY-004-S2
GIVEN
The authenticated user is the author of the post
WHEN
The user confirms deletion
THEN
The post row is deleted from
posts.postsThe API returns
204 No Content
POST-FE-004.1 — Edit and delete controls on own post card¶
AS A logged-in user I WANT edit and delete options on posts I authored SO THAT I can manage my content inline
SCENARIO 1: Edit mode replaces post text with input¶
Scenario ID: POST-FE-004.1-S1
GIVEN
The user is viewing their own post
WHEN
The user clicks “Edit”
THEN
The post text is replaced with an editable input pre-filled with the current text
“Save” and “Cancel” buttons appear
SCENARIO 2: Delete requires confirmation¶
Scenario ID: POST-FE-004.1-S2
GIVEN
The user clicks “Delete” on their own post
WHEN
A confirmation dialog appears and the user confirms
THEN
DELETE /posts/{id}is called and the post is removed from the feed on204
Architecture reference: Chapter 3 — Context and Scope
POST-BE-004.1 — PATCH /posts/{id} and DELETE /posts/{id} endpoints¶
AS A Posts Service
I WANT to expose PATCH /posts/{id} and DELETE /posts/{id}
SO THAT authors can update or remove their posts
POST-INFRA-004.1 — Posts Service containerised and deployable¶
Covered by POST-INFRA-001.1.
Architecture reference: Chapter 7 — Deployment View
POST-INFRA-004.2 — edited_at column migration¶
AS AN operator
I WANT an Alembic migration that adds edited_at to posts.posts
SO THAT edited posts can be distinguished from original ones
SCENARIO 1: Migration adds edited_at column¶
Scenario ID: POST-INFRA-004.2-S1
GIVEN
The
posts.poststable exists
WHEN
The migration for POST-STORY-004 is applied
THEN
Column
edited_at TIMESTAMPTZ NULLexists onposts.postsExisting rows have
edited_at = NULL
Architecture reference: Chapter 7 — Deployment View
POST-INFRA-004.3 — Event handling¶
Not applicable — edit and delete are synchronous operations.
POST-INFRA-004.4 — Monitoring¶
Covered by POST-INFRA-001.4.
Architecture reference: Chapter 7 — Deployment View