# 07 Application Flow Alignment Tracker

This file preserves the product decisions made while aligning `07-application-flow.md` with the API, schema, and public docs.

`07-application-flow.md` mirrors FigJam for application flow. FigJam is the source of truth for that specific flow file. API route docs and OpenAPI remain the source of truth for API contracts, but they should be changed to match approved product decisions from the flow.

Working rule: resolve one collision at a time, confirm the intended meaning, then update the docs/contracts before moving to the next item.

---

## Resolved Decisions

### 1. First Entry, Guest Home, and Default Feed Users

Status: done.

Decision:
- First app open goes to Home before registration/login.
- Guests can see the Home shell and public feed content.
- Anonymous users can use public reads, drafts, reports, and blocks.
- Registered public-profile basics are required for follow, vote, public comment, save, feed share, native share, and public-profile management; phone confirmation is not required.
- Public post and repost creation require verified phone identity plus completed profile setup; contact access is not required.
- Actual Hub actions require verified phone identity, completed profile setup, the one-time `contacts_synced_at` acknowledgement, and applicable membership/role checks.
- Public Hub-originated content shown in General continues to use the lower public gates until the user enters actual Hub context.
- To avoid an empty first feed, admins define default feed users.
- Guests and users with no personal feed sources receive public posts from default feed users.

Docs changed:
- `07-application-flow.md`
- `docs/api/routes/posts-feed-list.md`
- `docs/api/routes/posts-get.md`
- `docs/api/ROUTE_DESCRIPTIONS.md`
- `docs/api/openapi-strict.yaml`
- `docs/infrastructure/06-database-schema.md`

Schema decision:
- Added `default_feed_users`.

### 2. Authentication Is Phone Only

Status: done.

Decision:
- Phone authentication and sign-in are phone-number only.
- Public-profile registration is a separate earlier level and may be completed before phone confirmation.
- Google, Apple, and email login are removed from the planned product/API.
- OTP is SMS-only.

Docs changed:
- Removed Google/Apple OAuth route docs from planned API.
- Updated login config to `enabledMethods: ["phone"]`.
- Updated OTP request to phone-only.
- Updated onboarding wording to distinguish public registration from phone confirmation.
- Updated schema identity notes to phone-only.
- Cleaned stale OAuth/email references from API, support, privacy, status, and infrastructure docs where relevant.

### 3. Local Contacts Access And Hub-Readiness Acknowledgement

Status: done.

Decision:
- Hubs require local contacts access on the device.
- This is an OS/app permission plus a one-time backend Hub-readiness acknowledgement, not a server-side contact-book upload.
- The backend does not store the user's full address book.
- A successful empty or OS-limited contact read qualifies; the backend stores only `contacts_synced_at` for the gate.
- Later OS permission changes do not automatically revoke Hub readiness.
- The app uses contacts locally for selecting invitees.
- The backend receives only phone numbers explicitly selected for a specific action, such as a hub invite.
- Explicitly selected invitation numbers retain the current no-fixed-expiration storage policy.

Docs changed:
- `07-application-flow.md`
- `docs/api/routes/hubs-create.md`
- Hub invitation/join request routes
- `docs/api/ROUTE_DESCRIPTIONS.md`
- `docs/api/openapi-strict.yaml`
- `docs/infrastructure/06-database-schema.md`
- `docs/privacy/index.html`

### 4. Profile Onboarding and Profile Editing

Status: done.

Decision:
- Initial profile registration collects name, username, and terms approval.
- Profile image is optional.
- User cover image is not part of onboarding/profile.
- Profile editing allows name, username, bio, and profile image.
- When a new profile image replaces an old one, the old image is deleted after the new one is linked.

Docs changed:
- `docs/api/routes/users-me-onboarding-profile-basics.md`
- `docs/api/routes/users-me-onboarding-media.md`
- `docs/api/routes/users-profile-patch-me.md`
- `docs/api/routes/users-profile-get.md`
- `docs/api/routes/users-me-onboarding-status.md`
- `docs/api/ROUTE_DESCRIPTIONS.md`
- `docs/api/openapi-strict.yaml`
- `docs/infrastructure/06-database-schema.md`
- `docs/privacy/index.html`

### 5. Public Profile vs Private Hub Context

Status: done.

Decision:
- There is one user/profile entity.
- Public profile is visible by username and never returns phone number.
- "Private profile" means the same user shown inside a hub context with extra private context.
- Authenticated status alone does not reveal private profile data.
- Phone numbers are visible only inside hubs where the viewer is an approved member.
- If two users are not together in any approved hub, the viewer cannot see the other user's private profile data or phone number.

Docs changed:
- `07-application-flow.md`
- `docs/api/routes/users-profile-get.md`
- `docs/api/routes/hubs-members-list.md`
- `docs/api/API_CONVENTIONS.md`
- `docs/api/ROUTE_DESCRIPTIONS.md`
- `docs/api/openapi-strict.yaml`

### 6. People Search

Status: done.

Decision:
- People/account search is global and public.
- Add `GET /api/v1/search/users`.
- Authentication is optional.
- Results return public profile fields only: `id`, `name`, `username`, `profilePhotoUrl`, `bio`.
- Phone numbers are never returned from global people search.
- If authenticated, results may include `viewerFollows`.
- Follow action requires registered public-profile basics. Phone confirmation is not required.

Docs changed:
- `07-application-flow.md`
- `docs/api/routes/search-users.md`
- `docs/api/ROUTES_INDEX.md`
- `docs/api/ROUTE_DESCRIPTIONS.md`
- `docs/api/API_CONVENTIONS.md`
- `docs/api/openapi-strict.yaml`

### 7. Hub Creation Requirements

Status: done.

Decision:
- Hub creation requires `name` and one hub image (`profileAssetId`).
- Hub `description` is optional.
- Hub `customLink` remains optional.
- Hub cover image is removed from the planned create/edit/detail/schema flow because `07` has no separate cover image screen.
- Backend keeps role `super_admin` for the creator/owner role.
- The app displays the creator's `super_admin` role as manager.
- Upload purpose `cover_photo` is replaced by `hub_photo`.

Docs changed:
- `docs/api/routes/hubs-create.md`
- `docs/api/routes/hubs-patch.md`
- `docs/api/routes/hubs-get.md`
- `docs/api/routes/uploads-presign.md`
- `docs/api/ROUTE_DESCRIPTIONS.md`
- `docs/api/openapi-strict.yaml`
- `docs/infrastructure/06-database-schema.md`

Schema decision:
- `hubs.description` is nullable.
- Removed `hubs.cover_asset_id`.

### 8. Hub Invitations and Pending/Dark Hubs

Status: done.

Decision:
- There is no direct active hub join endpoint.
- `GET /api/v1/users/me/hubs` powers the hubs screen and includes active hubs, dark invited hubs, and pending admin-approval hubs.
- `hub_memberships` represents active members only.
- Phone-number invitations are created only from selected phone numbers, never full contact sync.
- Admin-created invitations become active membership when the invited user accepts.
- Member-created invitations become `pending_admin_approval` when the invited user accepts; an admin/manager must approve before membership is active.
- Hub invite-link joins always create `pending_admin_approval`.
- Hub invitations and invite links expire after 7 days.
- The server does not send SMS. It returns invite URLs/share text so the mobile app can use the device's native SMS/share flow.
- Pending/dark users cannot vote, chat, comment, or see private member phone numbers.
- Pending/dark users may see safe hub and member previews, with phone numbers omitted.

Docs changed:
- `07-application-flow.md`
- `docs/api/routes/users-me-hubs-list.md`
- `docs/api/routes/hubs-invitations-create.md`
- `docs/api/routes/hubs-invitations-list.md`
- `docs/api/routes/hub-invitations-accept.md`
- `docs/api/routes/hub-invitations-decline.md`
- `docs/api/routes/hubs-invite-links-create.md`
- `docs/api/routes/hub-invite-links-get.md`
- `docs/api/routes/hub-invite-links-join-requests-create.md`
- `docs/api/routes/hubs-join-requests-list.md`
- `docs/api/routes/hubs-join-requests-approve.md`
- `docs/api/routes/hubs-join-requests-reject.md`
- `docs/api/routes/hubs-create.md`
- `docs/api/routes/hubs-get.md`
- `docs/api/routes/hubs-members-list.md`
- `docs/api/routes/notifications-list.md`
- `docs/api/ROUTES_INDEX.md`
- `docs/api/ROUTE_DESCRIPTIONS.md`
- `docs/api/API_CONVENTIONS.md`
- `docs/api/ERROR_CODES.md`
- `docs/api/openapi-strict.yaml`
- `docs/infrastructure/06-database-schema.md`
- `docs/privacy/index.html`

Schema decision:
- Added `hub_invitations`.
- Added `hub_invite_links`.
- Added `hub_join_requests`.
- Added notification references for `invitation_id` and `join_request_id`.
- Removed planned direct `POST /api/v1/hubs/{hubId}/join`.
- Removed planned `GET /api/v1/hubs/{hubId}/share-link` in favor of invite-link endpoints.

### 9. Hub Chat and Discussion

Status: done.

Decision:
- Hub discussion is separate from public post comments.
- Public post comments stay under `GET/POST /api/v1/posts/{postId}/comments`.
- Hub discussion is private to active members of one hub.
- A hub discussion is tied to one post inside that hub.
- `LETS TALK` opens the hub discussion for that hub/post.
- Messages are flat; there are no replies to replies.
- Hub discussion activity updates hub/discussion last-activity ordering.
- Hub discussion messages do not create notification events.

Docs changed:
- `07-application-flow.md`
- `docs/api/routes/hubs-discussions-list.md`
- `docs/api/routes/hubs-discussion-messages-list.md`
- `docs/api/routes/hubs-discussion-messages-create.md`
- `docs/api/routes/hubs-feed-list.md`
- `docs/api/routes/posts-comments-list.md`
- `docs/api/routes/posts-comments-create.md`
- `docs/api/routes/comments-delete.md`
- `docs/api/ROUTES_INDEX.md`
- `docs/api/ROUTE_DESCRIPTIONS.md`
- `docs/api/API_CONVENTIONS.md`
- `docs/api/ERROR_CODES.md`
- `docs/api/openapi-strict.yaml`
- `docs/infrastructure/06-database-schema.md`

Schema decision:
- Added `hub_discussions`.
- Added `hub_discussion_messages`.
- Public `comments` remain public flat post comments.

### 9A. Hub Notification Event Matrix

Status: done.

Decision:
- All meaningful hub activity creates notification events except hub discussion/chat messages.
- New original post/article inside a hub creates `hub_post_created`.
- Existing post shared/reposted into a hub creates `post_shared_to_hub`.
- Hub invitation creates `hub_invite_received` for matching existing users.
- Join requests create `hub_join_request_created` for hub admins/managers.
- Join-request approval creates `hub_join_request_approved` for the requester and `hub_member_joined` for active hub members.
- Join-request rejection creates `hub_join_request_rejected` for the requester.
- Admin-created invitation acceptance creates `hub_member_joined`.
- Member removal creates `hub_member_removed` for the removed user.
- Hub role changes create `hub_role_changed` for the target member.
- Hub discussion/chat messages create no notification event; they only update hub/discussion last-activity ordering.

Docs changed:
- `07-application-flow.md`
- `docs/api/API_CONVENTIONS.md`
- `docs/api/routes/notifications-list.md`
- `docs/api/routes/notifications-stream.md`
- `docs/api/routes/posts-create.md`
- `docs/api/routes/posts-share-hubs-create.md`
- `docs/api/routes/hubs-create.md`
- `docs/api/routes/hub-invitations-accept.md`
- `docs/api/routes/hub-invite-links-join-requests-create.md`
- `docs/api/routes/hubs-join-requests-approve.md`
- `docs/api/routes/hubs-join-requests-reject.md`
- `docs/api/routes/hubs-admins-add.md`
- `docs/api/routes/hubs-admins-delete.md`
- `docs/api/routes/hubs-members-remove.md`
- `docs/api/ROUTE_DESCRIPTIONS.md`
- `docs/api/openapi-strict.yaml`
- `docs/infrastructure/06-database-schema.md`

### 10. Hub Mute Notification Settings

Status: done.

Decision:
- Hub mute is per-user and per-hub.
- Mute setting is `contentMuted` with optional `mutedUntil`.
- Mute suppresses hub content activity: `hub_post_created`, `post_shared_to_hub`, and Hub-scoped `post_reposted`. Global `post_reposted` is not muted.
- Suppressed means no in-app notification feed row, no notification stream event, no push, no app-icon badge, and no hub footer badge for that user's muted hub content activity.
- Mute does not suppress invitations, join requests, approvals/rejections, member joined/removed, role changes, vote notifications, or non-hub notifications.
- Hub discussion/chat messages are unaffected because they do not create notification events.
- Existing notifications are not deleted when mute is enabled.

Docs changed:
- `07-application-flow.md`
- `docs/api/routes/hubs-notification-settings-get.md`
- `docs/api/routes/hubs-notification-settings-patch.md`
- `docs/api/routes/notifications-list.md`
- `docs/api/routes/posts-create.md`
- `docs/api/routes/posts-share-hubs-create.md`
- `docs/api/ROUTES_INDEX.md`
- `docs/api/ROUTE_DESCRIPTIONS.md`
- `docs/api/API_CONVENTIONS.md`
- `docs/api/openapi-strict.yaml`
- `docs/api/index.html`
- `docs/infrastructure/06-database-schema.md`

Schema decision:
- Added `hub_notification_settings`.

### 11. Post Creation and Repost Article / Thought / Slider Model

Status: done.

Decision:
- External APIs use `article` for optional reusable article/link/media/text content.
- Physical storage still uses `post_main_contents` / nullable `main_content_id`.
- Original posts may be article-only, thought-only, slider-only, or combinations. At least one of article, thought text, or slider text is required by application validation.
- A post has optional post-specific `thoughtText`, optional `sliderText`, and optional `slider` labels.
- `thoughtText` is capped at 250 characters.
- `sliderText` is capped at 260 characters and is the visible slider title/prompt when a slider exists.
- There is no separate `sliderTitle` field.
- Original post creation creates a post frame and creates a reusable article record only when article content is supplied.
- Repost creates a new post frame that points to the original source post through `sourcePostId`.
- Repost-owned content is limited to new `thoughtText` and/or new `sliderText`/`slider`; reposts do not include a new `article`.
- Repost cards keep top-level `article` null and return original article content as `sourcePost.article`.
- Repost source fallback order is `sourcePost.article`, then `sourcePost.thoughtText`, then `sourcePost.sliderText`.
- `slider` is current-frame vote-control metadata. `sourcePost.slider` may be present as source metadata, but it does not make the repost frame votable.
- A no-new-content feed share is a share concept, not a repost, and uses `POST /api/v1/posts/{postId}/share/feed`.
- Reposting a repost is not allowed.
- Repost cards still show the repost affordance when their original source is
  eligible. The client submits against the original `sourcePost.id`, so the new
  frame is a sibling first-level repost rather than a repost of the displayed
  repost frame.
- Repost count is tracked on the original source post and counts active reposts only.
- Share-to-hub remains separate from repost: it shares an existing post as-is without creating a new frame.

Docs changed:
- `07-application-flow.md`
- `docs/api/routes/posts-create.md`
- `docs/api/routes/posts-repost-create.md`
- `docs/api/routes/posts-get.md`
- `docs/api/routes/posts-feed-list.md`
- `docs/api/routes/hubs-feed-list.md`
- `docs/api/routes/hubs-discussions-list.md`
- `docs/api/routes/users-feed-list.md`
- `docs/api/routes/posts-share-hubs-create.md`
- `docs/api/routes/posts-delete.md`
- `docs/api/routes/posts-vote-create.md`
- `docs/api/ROUTES_INDEX.md`
- `docs/api/ROUTE_DESCRIPTIONS.md`
- `docs/api/API_CONVENTIONS.md`
- `docs/api/ERROR_CODES.md`
- `docs/api/openapi-strict.yaml`
- `docs/api/index.html`
- `docs/infrastructure/06-database-schema.md`

Schema decision:
- Added `post_main_contents`.
- Updated `posts` to reference `main_content_id` and optional `source_post_id`.
- Added `is_repost`.
- Added `thought_text` and `slider_text` in follow-up migrations; `frame_text` was dropped after backfill.
- Added `post_repost_counters`.

### 12. Slider Scale

Status: done.

Decision:
- Slider vote requests use one of five positions: `strong_left`, `left`, `neutral`, `right`, `strong_right`.
- `POST /api/v1/posts/{postId}/vote` accepts `position` as the one-time vote input.
- Summary responses group positions into three buckets:
  - `leftCount` = `strong_left + left`
  - `neutralCount` = `neutral`
  - `rightCount` = `right + strong_right`
- Detailed distribution responses expose all five positions.
- Hub vote analytics use the same 3-bucket summary and 5-position detailed distribution.

Docs changed:
- `docs/api/API_CONVENTIONS.md`
- `docs/api/routes/posts-vote-create.md`
- `docs/api/routes/posts-vote-me-get.md`
- `docs/api/routes/posts-vote-summary-get.md`
- `docs/api/routes/posts-vote-distribution-get.md`
- `docs/api/routes/posts-vote-hubs-get.md`
- `docs/api/routes/posts-feed-list.md`
- `docs/api/routes/posts-get.md`
- `docs/api/routes/hubs-feed-list.md`
- `docs/api/ROUTE_DESCRIPTIONS.md`
- `docs/api/openapi-strict.yaml`
- `docs/infrastructure/06-database-schema.md`

Schema decision:
- User-linked vote receipts store no selected position.
- Anonymous vote counters and bucket counters store the aggregate position values.
- Vote summary counters use `left_count`, `neutral_count`, and `right_count`.
- Detailed vote bucket counters store 5-position `position` values.

### 13. Public, Hub, and Private Locked Content Visibility

Status: done.

Decision:
- Add explicit post visibility enum: `public`, `public_hub`, `private_locked_hub`.
- `public` appears in public/profile/follower feeds according to normal feed ranking.
- `public_hub` appears publicly and also in selected hub feeds/member feeds.
- `private_locked_hub` appears only in selected hub feeds and member feeds for active members of those hubs.
- `private_locked_hub` does not appear in the creator's public profile feed.
- `private_locked_hub` does not appear to followers outside the selected hubs.
- `private_locked_hub` cannot be reposted.
- `private_locked_hub` cannot be shared to another hub or exposed through a public share link.
- Direct access to private locked hub post detail/comments/votes requires the viewer to be the author or an active member of at least one selected hub.

Docs changed:
- `07-application-flow.md`
- `docs/api/API_CONVENTIONS.md`
- `docs/api/ERROR_CODES.md`
- `docs/api/ROUTE_DESCRIPTIONS.md`
- `docs/api/routes/posts-create.md`
- `docs/api/routes/posts-repost-create.md`
- `docs/api/routes/posts-share-hubs-create.md`
- `docs/api/routes/posts-share-link-get.md`
- `docs/api/routes/posts-feed-list.md`
- `docs/api/routes/posts-get.md`
- `docs/api/routes/hubs-feed-list.md`
- `docs/api/routes/users-feed-list.md`
- `docs/api/routes/users-me-saved-discussions-list.md`
- `docs/api/routes/posts-vote-create.md`
- `docs/api/routes/posts-comments-list.md`
- `docs/api/routes/posts-comments-create.md`
- `docs/api/openapi-strict.yaml`
- `docs/infrastructure/06-database-schema.md`

Schema decision:
- Added `posts.visibility`.
- Added `post_hub_targets` for selected hub visibility targets.

---

### 16. Saved Content vs Bookmarks

Status: done.

Decision:
- `07` is source of truth: public posts can be saved/bookmarked from the post card, while hub discussions can be saved from hub discussion/chat.
- A saved hub discussion item points to the hub discussion, not the standalone post.
- The saved-discussions list only returns items from hubs where the current user is still an active member.
- Saved discussion cards may include up to three earliest chat messages as preview context.

Docs changed:
- `07-application-flow.md`
- `docs/api/API_CONVENTIONS.md`
- `docs/api/ROUTES_INDEX.md`
- `docs/api/ROUTE_DESCRIPTIONS.md`
- `docs/api/routes/hubs-discussion-save-create.md`
- `docs/api/routes/hubs-discussion-save-delete.md`
- `docs/api/routes/users-me-saved-discussions-list.md`
- `docs/api/routes/posts-bookmark-create.md` (deleted)
- `docs/api/routes/posts-bookmark-delete.md` (deleted)
- `docs/api/routes/users-bookmarks-list.md` (deleted)
- `docs/api/routes/posts-feed-list.md`
- `docs/api/routes/posts-delete.md`
- `docs/api/openapi-strict.yaml`
- `docs/api/index.html`
- `docs/infrastructure/06-database-schema.md`
- `docs/privacy/index.html`

Schema decision:
- Removed `bookmarks`.
- Added `saved_hub_discussions`.

---

### 17. Notifications

Status: done.

Decision:
- Vote on your post creates `post_voted` for the post owner, when voter is not owner.
- Comment on your post creates `post_commented` for the post owner, when commenter is not owner.
- Repost of your post creates `post_reposted` for the original post owner, when reposter is not owner.
- If the repost is also published into a hub, active hub members receive `post_shared_to_hub`, but the original source author receives `post_reposted` rather than a duplicate share-to-hub notification. A `private_locked_hub` repost uses Hub-scoped `post_reposted` only for target Hubs where the source author is an active member; General/`public_hub` uses one global `post_reposted`.
- New follower creates `user_followed` for the followed user.
- Saved discussion creates no notification.
- Report/block/moderation actions create no user-facing notification; moderation workflow is internal.
- Existing notification delivery rule remains: a notification event creates an in-app feed row and is an SSE candidate; push/app-icon/hub-footer delivery is controlled by settings and hub mute rules.

Docs changed:
- `docs/api/API_CONVENTIONS.md`
- `docs/api/ROUTE_DESCRIPTIONS.md`
- `docs/api/routes/notifications-list.md`
- `docs/api/routes/notifications-stream.md`
- `docs/api/routes/posts-comments-create.md`
- `docs/api/routes/posts-repost-create.md`
- `docs/api/routes/users-follow-create.md`
- `docs/api/routes/users-follow-delete.md`
- `docs/api/routes/users-block-create.md`
- `docs/api/routes/users-block-delete.md`
- `docs/api/routes/reports-user-create.md`
- `docs/api/routes/reports-post-create.md`
- `docs/api/routes/reports-hub-create.md`
- `docs/api/openapi-strict.yaml`
- `docs/infrastructure/06-database-schema.md`

Schema decision:
- Added notification types `post_commented`, `post_reposted`, and `user_followed`.
- Added optional notification references `source_post_id` and `comment_id`.

---

### 18. Home Feed Composition

Status: done.

Decision:
- `07` is source of truth: Home feed includes my posts, posts from hub friends, posts from people I follow, and posts that friends interacted with.
- Hub friends/co-members mean users who share at least one active hub membership with the viewer.
- Friend interactions mean voting, commenting, reposting, or sharing by a followed user or active hub co-member.
- Home feed may show public and public-hub posts by active hub co-members even if the viewer does not follow them.
- Home feed may show public and public-hub posts that followed users or active hub co-members interacted with.
- Posts targeted to hubs where the viewer is an active member can appear in Home, including private locked hub posts for those selected hubs.
- `private_locked_hub` content must never leak through friend-interaction discovery; it appears only when the viewer is the author or an active member of at least one selected hub for that post.
- Guests and users with no personal feed sources still fall back to admin-managed default feed users.
- Feed cards may include `feedContext` so the client can explain why an item appears.

Docs changed:
- `07-application-flow.md`
- `docs/api/API_CONVENTIONS.md`
- `docs/api/ROUTE_DESCRIPTIONS.md`
- `docs/api/routes/posts-feed-list.md`
- `docs/api/routes/posts-create.md`
- `docs/api/routes/posts-vote-create.md`
- `docs/api/routes/posts-comments-create.md`
- `docs/api/routes/posts-repost-create.md`
- `docs/api/routes/posts-share-hubs-create.md`
- `docs/api/openapi-strict.yaml`
- `docs/infrastructure/06-database-schema.md`

Schema decision:
- Added `post_feed_activity_events` to support interaction-based Home feed candidates.

---

### 19. Share To Feed

Status: done.

Decision:
- `POST /api/v1/posts/{postId}/share/feed` shares an existing post or repost to the current user's feed without creating a new post frame.
- Feed-share points to the exact `postId` supplied. If the target is a repost, the repost frame is shared and the repost author is notified.
- `POST /api/v1/posts/{postId}/reposts` is used only when the user adds new thought and/or slider content around an original post.
- Repost creates a new post frame pointing to the original source post and returns expanded `sourcePost` for source rendering. Original article content is returned as `sourcePost.article`, not as top-level repost `article`.
- The repost appears in the creator's profile and follower feeds when visibility allows. Feed-share appears as a repeatable share event in the sharer's profile/follower feeds.
- Share externally remains `GET /api/v1/posts/{postId}/share-link`.
- Share to hub remains `POST /api/v1/posts/{postId}/share/hubs` and preserves the existing post frame.

Docs changed:
- `07-application-flow.md`
- `docs/api/API_CONVENTIONS.md`
- `docs/api/ROUTE_DESCRIPTIONS.md`
- `docs/api/routes/posts-repost-create.md`
- `docs/api/routes/posts-share-hubs-create.md`
- `docs/api/routes/posts-share-link-get.md`
- `docs/api/routes/users-feed-list.md`
- `docs/api/openapi-strict.yaml`

---

### 20. Slider Text

Status: done.

Decision:
- `sliderText` is the slider title/prompt.
- There is no separate `sliderTitle` field.
- Original posts and reposts may omit `sliderText`.
- The Flutter original-post and repost composers initialize the slider as enabled and show `Do you agree with me?` as placeholder-only text.
- At submission, an enabled slider with blank or whitespace-only input materializes `Do you agree with me?` as `sliderText`; nonblank user input wins, and a disabled slider is omitted.
- Default resolution occurs only at the submission boundary and must not mutate the text controller or turn placeholder text into editable field content.
- An enabled default slider counts as Hub-eligible frame content in the Flutter destination flow.
- This is first-party client normalization. The API and backend still require clients to send explicit `sliderText` and matching slider labels and do not synthesize the default.
- Reposts supply their own `thoughtText` and/or current-frame `sliderText`/`slider`, while `sourcePost` carries the original post's display content.
- For repost source fallback, clients use `sourcePost.article`, then `sourcePost.thoughtText`, then `sourcePost.sliderText`.
- `sourcePost.slider` is source metadata only; it does not make a repost votable.

Docs changed:
- `07-application-flow.md`
- `docs/api/API_CONVENTIONS.md`
- `docs/api/ROUTE_DESCRIPTIONS.md`
- `docs/api/routes/posts-create.md`
- `docs/api/routes/posts-repost-create.md`
- `docs/api/routes/posts-get.md`
- `docs/api/routes/posts-feed-list.md`
- `docs/api/routes/hubs-feed-list.md`
- `docs/api/openapi-strict.yaml`
- `docs/infrastructure/06-database-schema.md`
- `skills/appinion-product-audit/SKILL.md`

---

### 21. Vote Result Anonymity Threshold (superseded)

Status: superseded on 2026-08-10.

Previous decision:
- Aggregate vote results require at least 3 votes in the relevant result scope.
- Under 3 votes, APIs return `resultsVisible=false`, `minimumVotesToShow=3`, and may return `totalVotes`, but bucket counts and detailed distributions are hidden.
- Hidden summary counts are `null`.
- Hidden detailed distributions are empty arrays.
- The threshold applies independently per scope:
  - Post summary/distribution: 3 votes on the post frame.
  - Hub vote analytics: 3 votes in that hub scope.
  - Feed/detail vote summary: the displayed vote scope.
- `GET /api/v1/posts/{postId}/vote/me` still returns the current user's vote status regardless of aggregate visibility, but never returns the selected position.

Replacement decision:
- There is no minimum Hub member count for voting or result visibility.
- Every eligible active-member Hub scope receives its receipt and aggregate counter update at vote time.
- Global and Hub aggregate results are exposed from the first vote.
- Zero-vote scopes remain empty with `resultsVisible=false`; `minimumVotesToShow` was removed from the API.

Docs changed:
- `07-application-flow.md`
- `docs/api/API_CONVENTIONS.md`
- `docs/api/ROUTE_DESCRIPTIONS.md`
- `docs/api/routes/posts-feed-list.md`
- `docs/api/routes/posts-get.md`
- `docs/api/routes/hubs-feed-list.md`
- `docs/api/routes/posts-vote-create.md`
- `docs/api/routes/posts-vote-me-get.md`
- `docs/api/routes/posts-vote-summary-get.md`
- `docs/api/routes/posts-vote-distribution-get.md`
- `docs/api/routes/posts-vote-hubs-get.md`
- `docs/api/openapi-strict.yaml`
- `docs/infrastructure/06-database-schema.md`

---

### 22. Anonymous Vote Pools and Hub Analytics

Status: done.

Decision:
- One post frame is one poll.
- Original posts and reposts have independent anonymous vote pools.
- A repost is votable only when its own top-level `sliderText`/`slider` is present; `sourcePost.sliderText` or `sourcePost.slider` are display fallback/source metadata only.
- Share-to-hub does not create a new poll; it adds hub visibility and a hub analytics scope for future votes only.
- User-linked vote storage records only that the user voted and which scopes counted the vote. It never stores the selected position.
- `POST /api/v1/posts/{postId}/vote` casts a one-time vote. Repeat votes return `VOTE_ALREADY_CAST`.
- At vote time, the vote is counted in the global post pool and every hub where the post is already visible and the voter is already an active member.
- Later hub joins and later hub shares do not backfill old votes because the selected position is not stored.
- `GET /api/v1/posts/{postId}/vote/me` returns vote status only.
- `GET /api/v1/posts/{postId}/frames` returns visible original/repost frames for the same source lineage using `source_post_id`, not `main_content_id`.

Docs changed:
- `07-application-flow.md`
- `docs/api/API_CONVENTIONS.md`
- `docs/api/ERROR_CODES.md`
- `docs/api/ROUTES_INDEX.md`
- `docs/api/ROUTE_DESCRIPTIONS.md`
- `docs/api/index.html`
- `docs/api/openapi-strict.yaml`
- `docs/api/routes/posts-vote-create.md`
- `docs/api/routes/posts-vote-me-get.md`
- `docs/api/routes/posts-vote-summary-get.md`
- `docs/api/routes/posts-vote-distribution-get.md`
- `docs/api/routes/posts-vote-hubs-get.md`
- `docs/api/routes/posts-feed-list.md`
- `docs/api/routes/posts-get.md`
- `docs/api/routes/hubs-feed-list.md`
- `docs/api/routes/posts-create.md`
- `docs/api/routes/posts-repost-create.md`
- `docs/api/routes/posts-share-hubs-create.md`
- `docs/api/routes/posts-frames-list.md`
- `docs/infrastructure/06-database-schema.md`
- `docs/privacy/index.html`

Schema decision:
- Replace `votes(position)` with `post_vote_receipts` and `post_vote_scope_receipts`.
- Keep anonymous aggregate counters as the read model for post and hub vote results.

### 23. One-Level Reply Threads in Comments and Hub Discussion

Status: done.

Decision:
- Public post comments and hub discussion messages support replies, one level deep.
- `POST /api/v1/posts/{postId}/comments` accepts optional `parentCommentId`; `POST .../discussions/{discussionId}/messages` accepts optional `parentMessageId`.
- The parent must be an existing item on the same post/discussion. Replying to a reply attaches the new item to the thread's root, so stored threads are never deeper than one level.
- List responses return `parentCommentId`/`parentMessageId` (root of the thread, `null` for top-level) and `replyCount` per top-level item.
- The comments screen renders threads as a tree: replies are indented under their root comment, and the root shows a reply count next to the reply icon.
- Opening comments on a public (non-hub-scoped) post uses the same full-screen comments view as hub discussions, instead of the old modal comments sheet.
- Comments and discussion messages are displayed oldest-first so threads read top-down and new items append at the bottom.
- Replies do not create additional notification events beyond the existing `post_commented` notification for the post owner.

Docs changed:
- `07-application-flow.md`
- `docs/api/API_CONVENTIONS.md`
- `docs/api/ROUTE_DESCRIPTIONS.md`
- `docs/api/openapi-strict.yaml`
- `docs/api/routes/posts-comments-list.md`
- `docs/api/routes/posts-comments-create.md`
- `docs/api/routes/hubs-discussion-messages-list.md`
- `docs/api/routes/hubs-discussion-messages-create.md`
- `docs/infrastructure/06-database-schema.md`

Schema decision:
- Added nullable `comments.parent_comment_id` and `hub_discussion_messages.parent_message_id` (migration `010_comment_reply_threading.sql`).
- Reply counts are computed at read time; no denormalized counter columns.

### 24. Comment Author Identity by General vs Hub Context

Status: done.

Decision:
- General/public post comments display the author's public username, including public Hub-originated content viewed in General.
- A post opened inside an actual Hub uses the separate Hub discussion. Active members see the verified phone number of each active non-anonymous author in that same Hub.
- Saved Hub-discussion previews use the same private phone projection.
- Invited, pending, removed, and unrelated viewers cannot read Hub discussions or phone labels. Former authors no longer project a phone, and historical anonymous authors expose no stable identity.
- Generic `GET/POST /posts/{postId}/comments` routes reject `private_locked_hub` posts. Direct clients must use the Hub discussion endpoints and pass Hub-readiness plus active-membership enforcement.
- Public and Hub author payloads are structurally distinct: public comments use `UserLite`; Hub discussion messages use `HubDiscussionAuthor` with explicit `phoneNumber`/`publicUsername` fields.

Docs changed:
- `docs/authentication-four-level-contract.md`
- `07-application-flow.md`
- `docs/api/API_CONVENTIONS.md`
- `docs/api/ROUTE_DESCRIPTIONS.md`
- `docs/api/openapi-strict.yaml`
- Hub discussion, saved discussion, and public comment route specs
- `docs/privacy/index.html`
- `skills/appinion-product-audit/SKILL.md`

### 25. Two-Stage Topic Scope Selection Without Chat Bubbles

Status: done.

Decision:
- The post topic rail has no separate chat/comment bubble under General or Hub tiles.
- The complete tile is the only interaction target.
- Tapping an unselected General or Hub scope selects its statistics without navigation.
- Tapping the already-selected General scope opens public comments; tapping the already-selected Hub scope opens that Hub's private discussion.
- Switching Hub A to Hub B clears Hub A, and selecting General clears the selected Hub and restores public statistics. Scope changes reset expanded result state.
- The rule applies to feed cards, continuation/repost cards, original-post detail rails, and private rails where General is absent.
- Saved-content scope tiles remain direct links to the already-saved public thread or Hub discussion and do not use the two-stage rail interaction.
- No API, schema, storage, or privacy contract changed.

Verification:
- The topic-rail widget suite covers every selected-scope/tapped-scope combination across General, Hub 1, and Hub 2, plus private/no-General rails.
- BDD coverage proves the forum bubble asset is absent while two taps on the same Hub tile still select statistics and then open chat.
- The full Flutter widget regression suite verifies that public comments, gated Hub discussions, saved discussions, and original-post detail retain access.

Docs changed:
- `docs/authentication-four-level-contract.md`
- `07-application-flow.md`
- `skills/appinion-product-audit/SKILL.md`
