feed-service — business features

The company social wall: announcements, reactions, "seen" tracking and a live stream · ← platform hub · entry points verified against tools/feature-docs/out/feed-service.json

1. What it is

feed-service is the place where a company talks to its own staff: an admin posts an announcement, employees see it in the mobile app, react to it, and the app records who has already seen what. It was also designed to celebrate achievements automatically — certificates, recognition, rewards, leaderboard wins — but that half of the product does not run today (see §8).

Who uses itEmployees through the mobile app · admins through the admin web · users-auth-service for GDPR exports · kubelet probes
RuntimeFastAPI (pinned fastapi==0.109.2, feed-service/requirements.txt:1) · Deployment feed-service, replicas: 1 (feed-service/k8s/deployment.yaml:9)
DatabasePostgres logical DB feed_db, sole writer (ARCHITECTURE.md §3.4)
Redisusers-auth-redis index 5 — visibility cache and author-profile cache (feed-service/k8s/configmap.yaml:7)
NATS streamsNone owned at runtime. A SOCIAL stream would be created by EventProcessorService.start_consuming (app/services/event_processor.py:322), but that method is never called.
External APIsNone. One outbound in-cluster edge: users-auth-service batch profile lookup on the feed read path (app/clients/users_auth_client.py:33, app/services/post_service.py:206). ARCHITECTURE.md §3.2 still lists feed-service as "zero-outbound"; the code disagrees.
Entry points21 scanned HTTP routes (plus GET /v1/mobile/feed, which the scanner's empty-path rule missed) · 16 NATS consumer registrations over 8 subjects · 2 published subjects · 1 background job

Read this before trusting the async half

Neither of the two NATS consumer groups in this service starts in the deployed image. The admin announcement path, the read path, reactions and view tracking are all real and working; everything that depends on incoming events is not. Details and evidence in §5 and §8.

2. Feature map

flowchart LR
  ADMIN["Admin (admin web)"] --> ANN["Publish announcement"]
  ADMIN --> MOD["Moderate posts"]
  ADMIN --> CFG["Auto-post settings"]
  EMP["Employee (mobile app)"] --> READ["Read the feed"]
  EMP --> REACT["React to a post"]
  EMP --> SEEN["Mark posts seen"]
  EMP --> LIVE["Live updates (SSE)"]
  UA["users-auth-service"] --> GDPR["GDPR export"]
  ANN --> R1["POST /v1/admin/posts"]
  ANN --> N1["publish training.notifications...feed.*"]
  MOD --> R2["PATCH/DELETE /v1/admin/posts/*"]
  CFG --> R3["/v1/admin/announcement-feed/config"]
  READ --> R4["GET /v1/mobile/feed*"]
  REACT --> R5["/v1/mobile/posts/{id}/reactions/*"]
  SEEN --> R6["/v1/mobile/posts/*/viewed"]
  LIVE --> R7["GET /v1/feed/sse"]
  GDPR --> R8["GET /v1/internal/export/{user_id}"]

3. Features

Admin

Publish a company announcement live

admin

An admin writes a title and body, chooses who should see it (whole company, a department, a location, a team, or a single person), and the post appears in those employees' feeds. For company-wide announcement types the service also fires a push notification so people see it without opening the app. If the notification fails the post is still saved — the announcement matters more than the push.

Entry points
POST /v1/admin/posts
Touches
posts; publishes training.notifications.ten.<tenant>.feed.<category>.<subtype>
Related
Consumed by notification-worker for FCM delivery
Evidence
app/routes/admin_routes.py:51 · notification publish at app/routes/admin_routes.py:121 → app/events/nats_publisher.py:134

Moderate the wall — approve, pin, re-scope, delete suspect

admin

Admins can approve a post that is waiting for review, pin an important one to the top, change who it is visible to, or delete it. All four require an admin token carrying the FEED_WRITE scope. The admin web app is not in this repository, so no caller can be shown for these routes here.

Entry points
PATCH /v1/admin/posts/{post_id}/approval · POST /v1/admin/posts/{post_id}/pin · PATCH /v1/admin/posts/{post_id}/visibility · DELETE /v1/admin/posts/{post_id}
Touches
posts; Redis visibility cache invalidation
Related
Router-level require_admin at app/routes/admin_routes.py:26
Evidence
app/routes/admin_routes.py:223, :306, :153, :261 — searched the repo for each path string; only feed-service's own smoke tests match

Decide which events become celebration posts suspect

admin

A settings screen where an admin turns automatic celebration posts on or off per event type — "post when someone earns a certificate", "post the weekly leaderboard top 3". The rows are stored and returned correctly. Today the switch controls nothing, because the component that would read it and write the posts never starts.

Entry points
GET /v1/admin/announcement-feed/config · PATCH /v1/admin/announcement-feed/config/{event_subject}
Touches
auto_post_config, event_template_map, post_templates
Related
Consumed by the dead auto-post pipeline — see §8
Evidence
app/routes/admin_routes.py:356, :395 · config table created in migrations/versions/305ebc942f22_add_auto_post_config_table.py:19

Employee (mobile)

Read the feed live

mobile

The main wall. The app asks for a page of posts, gets back only the posts that person is allowed to see, each already carrying the author's name and photo and that person's own reaction and seen state. Opening a single post works the same way. Author names come from users-auth-service in one batched call, cached in Redis so a busy feed does not hammer it.

Entry points
GET /v1/mobile/feed · GET /v1/mobile/feed/{post_id}
Touches
posts, reactions, post_views, user_preferences; Redis; users-auth-service profile batch
Related
Records views as a side effect — see "Remember what I have already seen"
Evidence
app/routes/feed_routes.py:63, :191 · client usage documented at docs/flutter_user_state_integration.md:30, :73

Home-screen highlights suspect

mobile

A short, cached strip of the most relevant posts for the app's home screen, so the landing view does not have to load the whole feed. No mobile client code or client-facing document in this repository names this path.

Entry points
GET /v1/mobile/feed/home-highlights
Touches
posts; Redis; users-auth-service profile batch
Related
app/services/home_highlights_service.py
Evidence
app/routes/feed_routes.py:131 — repo-wide search for "home-highlights" returns only bug-hunt-reports/feed-service.md:124 and to-be-reviewed/_summaries/feed-service.md:22, no caller

React to a post live

mobile

Tap to like, celebrate or otherwise react; tap the same reaction again and it is removed. The app can also ask which reactions this person left on a post, and for the per-type totals shown under it. The summary endpoint used to be readable without a token by anyone who knew a post id — that hole is closed.

Entry points
POST /v1/mobile/posts/{post_id}/reactions/{reaction_type} live · DELETE same path suspect · GET /v1/mobile/posts/{post_id}/reactions suspect · GET /v1/mobile/posts/{post_id}/reactions/summary suspect
Touches
reactions, posts
Related
Fixed IDOR: bug-hunt-reports/feed-service.md #1/#7. Known wart: the toggle-off branch still answers 201 (#14).
Evidence
app/routes/reaction_routes.py:16, :51, :86, :118 · POST documented for the app at docs/flutter_user_state_integration.md:93

Remember what I have already seen live

mobile

As a person scrolls, the app reports which posts were actually on screen, for how long, and how much of the post was visible — one at a time or as a batch when the screen closes. That is what keeps a post from being shown as new twice. Writes are idempotent, so a double pull-to-refresh no longer fails the whole feed load.

Entry points
POST /v1/mobile/posts/{post_id}/viewed · POST /v1/mobile/posts/bulk/viewed · GET /v1/mobile/posts/{post_id}/view-status suspect
Touches
post_views, user_preferences
Related
feed-service/VIEW_TRACKING_IMPLEMENTATION.md, feed-service/USER_STATE_IMPLEMENTATION.md
Evidence
app/routes/view_tracking_routes.py:63, :25, :101 · both POSTs documented for the app at docs/flutter_user_state_integration.md:132, :158

Live feed updates while the app is open dead

mobile

The app can hold an open stream and receive new posts and celebrations the moment they happen, instead of pulling to refresh. The stream itself connects, authenticates and stays alive — but nothing is ever pushed into it, because the code that subscribes to the events is registered on a startup hook this app never runs. In practice a client receives one "connected" frame and a keepalive comment every 20 seconds, forever.

Entry points
GET /v1/feed/sse
Touches
In-process connection map only; no tables
Related
Cross-tenant leak in the broadcaster was fixed defensively even though the path is dormant — bug-hunt-reports/feed-service.md #6; multi-device overwrite still open (#5/#12)
Evidence
route app/routes/sse_routes.py:74; subscriptions registered at app/routes/sse_routes.py:212 via @router.on_event("startup"), which does not fire under the pinned fastapi 0.109.2 / starlette 0.36.3 when a custom lifespan= is supplied (app/main.py:43, :132) — reproduced empirically in bug-hunt-reports/feed-service.md #2

Internal (other services)

Hand over everything we hold about one person live

internal

When someone exercises a GDPR access or portability request, users-auth-service asks every service that holds personal data for its slice and merges them. This is feed-service's slice: the person's posts, reactions, views and preferences. A person with no feed activity gets an empty but successful answer — "nothing here" is a valid reply to an access request.

Entry points
GET /v1/internal/export/{user_id} (X-Oper-Key + X-Tenant-Id)
Touches
posts, reactions, post_views, user_preferences, comments
Related
ARCHITECTURE.md §3.2 edge H23 (GDPR export fan-out, CronJob)
Evidence
app/routes/internal_routes.py:35 · caller users-auth-service/app/services/gdpr_export.py:156

Tell Kubernetes whether the pod can serve live

internal

Two probes the cluster calls: one says the process is alive, the other says the feed read path can actually answer. Only Postgres decides the verdict — Redis and NATS are reported but never gate, because gating on NATS once took the whole service out of rotation.

Entry points
GET /health/live · GET /health/ready · GET /health/startup suspect
Touches
Postgres SELECT 1; cached Redis and NATS state
Related
Rationale in the handler docstring at app/core/health.py:22
Evidence
app/core/health.py:13, :22, :84 · probes wired at feed-service/k8s/deployment.yaml:58 (live) and :64 (ready); no manifest references /health/startup

Background

Heartbeat log line live

background

Every 30 seconds the pod logs that it is still running. Purely an operational breadcrumb — it reads nothing and writes nothing.

Entry points
heartbeat_logger
Touches
Nothing
Related
Evidence
app/main.py:83, started from the lifespan at app/main.py:63

Turn achievements into celebration posts dead

background

The designed centrepiece of the product: when someone finishes a module, earns a certificate, receives recognition, redeems a reward, tops the weekly leaderboard, or joins the company, the feed should celebrate it automatically using an admin-configurable template. The code to do this exists in full — templates, mapping, post writer, event publisher — and is never started. As deployed, only posts an admin writes by hand ever appear in the feed.

Entry points
EventProcessorService.start_consuming (7 subjects, creates the SOCIAL stream)
Touches
Would write posts and publish social.post.created
Related
bug-hunt-reports/feed-service.md #13 (deferred: needs a product/infra decision on the run model) · ARCHITECTURE.md §4.1(5)
Evidence
class defined at app/services/event_processor.py:23; repo-wide search for EventProcessorService returns only that definition and a prose mention in app/services/gdpr_export_service.py:81 — no instantiation. The lifespan (app/main.py:43-80) starts only Redis, the HTTP client and the heartbeat.

4. API reference

Every route the scanner found. "Callers" lists only non-test callers: feed-service's own tests/ smoke scripts hit most of these paths and do not count as evidence.

One real route the scan output does not contain

GET /v1/mobile/feed — the main feed read, mounted as the router's empty path (app/routes/feed_routes.py:63 under the /v1/mobile/feed prefix at app/main.py:158). The scanner emits no row for an empty route path, so it is absent from feed-service.json and from the table below. It is live: the mobile client contract documents it at docs/flutter_user_state_integration.md:30, and it is the busiest endpoint in the service. Treat the table below as "the 21 scanned routes", not "the whole surface".

MethodPathAuthFeatureCallersVerdict
GET/health/livenoneProbeskubelet — k8s/deployment.yaml:58live
GET/health/readynoneProbeskubelet — k8s/deployment.yaml:64live
GET/health/startupnoneProbesnone — no startupProbe in k8s/deployment.yamlsuspect
POST/v1/admin/postsadmin JWT + FEED_WRITEPublish announcementadmin web (not in repo)suspect
PATCH/v1/admin/posts/{post_id}/visibilityadmin JWT + FEED_WRITEModerateadmin web (not in repo)suspect
PATCH/v1/admin/posts/{post_id}/approvaladmin JWT + FEED_WRITEModerateadmin web (not in repo)suspect
POST/v1/admin/posts/{post_id}/pinadmin JWT + FEED_WRITEModerateadmin web (not in repo)suspect
DELETE/v1/admin/posts/{post_id}admin JWT + FEED_WRITEModerateadmin web (not in repo)suspect
GET/v1/admin/announcement-feed/configadmin JWTAuto-post settingsadmin web (not in repo)suspect
PATCH/v1/admin/announcement-feed/config/{event_subject}admin JWT + FEED_ANNOUNCEAuto-post settingsadmin web (not in repo); the setting has no runtime effect while the auto-post pipeline is deadsuspect
GET/v1/mobile/feed/{post_id}user JWTRead the feedmobile app — docs/flutter_user_state_integration.md:73live
GET/v1/mobile/feed/home-highlightsuser JWTHome highlightsnone foundsuspect
POST/v1/mobile/posts/{post_id}/reactions/{reaction_type}user JWTReactmobile app — docs/flutter_user_state_integration.md:93live
DELETE/v1/mobile/posts/{post_id}/reactions/{reaction_type}user JWTReactnone found (the POST is a toggle, so the app may never need this)suspect
GET/v1/mobile/posts/{post_id}/reactionsuser JWTReactnone foundsuspect
GET/v1/mobile/posts/{post_id}/reactions/summaryuser JWTReactnone foundsuspect
POST/v1/mobile/posts/{post_id}/vieweduser JWTSeen trackingmobile app — docs/flutter_user_state_integration.md:132live
POST/v1/mobile/posts/bulk/vieweduser JWTSeen trackingmobile app — docs/flutter_user_state_integration.md:158live
GET/v1/mobile/posts/{post_id}/view-statususer JWTSeen trackingnone foundsuspect
GET/v1/feed/sseuser JWTLive updatesno in-repo caller; and nothing can ever be pushed into the stream (see §8)dead
GET/v1/internal/export/{user_id}X-Oper-Key + X-Tenant-IdGDPR exportusers-auth-service/app/services/gdpr_export.py:156live

5. Async contracts

Consumes

Sixteen registrations: the same eight subjects are registered twice, once by the SSE router and once by the auto-post pipeline. Neither registration site executes in the deployed image, so every row below is dead — but for two different reasons, and each reason is independent of whether the subject has a producer.

SubjectStreamDurablePublished byFeatureVerdict
social.post.createdwould-be SOCIALannouncement_sse_social_post_created (sse_routes.py:197)feed-service itself (event_processor.py:130) — which also never runsLive updatesdead
social.post.createdSOCIAL (created at event_processor.py:322)social-announcement-service__social_post_createdfeed-service itself (event_processor.py:130)Auto-postdead
training.certificates.module_completedMICRO_LEARNINGannouncement_sse_training_certificates_module_completedmicro-learning-service-v2/app/events/events.py:257Live updatesdead
training.certificates.module_completedMICRO_LEARNINGsocial-announcement-service__training_certificates_module_completedmicro-learning-service-v2/app/events/events.py:257Auto-postdead
training.module.publishedMICRO_LEARNINGannouncement_sse_training_module_publishedmicro-learning-service-v2/app/events/events.py:55Live updatesdead
training.module.publishedMICRO_LEARNINGsocial-announcement-service__training_module_publishedmicro-learning-service-v2/app/events/events.py:55Auto-postdead
training.module.completedMICRO_LEARNINGannouncement_sse_training_module_completedmicro-learning-service-v2/app/events/events.py:80Live updatesdead
training.module.completedMICRO_LEARNINGsocial-announcement-service__training_module_completedmicro-learning-service-v2/app/events/events.py:80Auto-postdead
users.createdAUTHannouncement_sse_users_createdusers-auth-service/app/routes/users.py:222Live updatesdead
users.createdAUTHsocial-announcement-service__users_createdusers-auth-service/app/routes/users.py:222Auto-post (welcome post)dead
recognition.createdwould-be SOCIALannouncement_sse_recognition_creatednobodyLive updatesdead
recognition.createdwould-be SOCIALsocial-announcement-service__recognition_creatednobodyAuto-postdead
reward.redeemedwould-be SOCIALannouncement_sse_reward_redeemednobodyLive updatesdead
reward.redeemedwould-be SOCIALsocial-announcement-service__reward_redeemednobodyAuto-postdead
leaderboard.weekly.finalizedwould-be SOCIALannouncement_sse_leaderboard_weekly_finalizednobodyLive updatesdead
leaderboard.weekly.finalizedwould-be SOCIALsocial-announcement-service__leaderboard_weekly_finalizednobodyAuto-postdead

Correcting ARCHITECTURE.md §4.1(5)

§4.1(5) says feed's "4 social subjects have no producer". Verified subject by subject with a repo-wide search: three have no producer anywhere — recognition.created, reward.redeemed and leaderboard.weekly.finalized. The fourth, social.post.created, does have a producer, but it is feed-service's own _publish_post_event (app/services/event_processor.py:130), reachable only from the auto-post pipeline that never starts — so at runtime it is equally silent. The three unproduced subjects match the SDD's expected senders — "recognition-service", "rewards-service", "leaderboard-service" (docs/sdd.md:47-51) — none of which exist in the platform inventory (ARCHITECTURE.md §2).

Publishes

SubjectConsumed byFeatureVerdict
training.notifications.ten.*.feed.*.*
app/events/nats_publisher.py:134, called from app/routes/admin_routes.py:121
notification-worker, pull consumer notification_worker_feed on MICRO_LEARNING, filter training.notifications.ten.*.feed.> — the filter's trailing > matches the two extra tokens, so this one is not a wildcard mismatch
notification-worker/app/services/notification_service.py:2322-2333
Publish announcement → push notification live
social.post.created
app/services/event_processor.py:130 via _publish_post_event (:280)
Only feed-service's own two registrations, both dormant. No other service in the repo subscribes to social.>. Auto-post dead

Background jobs

JobScheduleWhat it doesVerdict
heartbeat_logger
app/main.py:83
Every 30 s, for the pod's lifetimeLogs a liveness breadcrumblive
EventProcessorService.start_consuming
app/services/event_processor.py:299
Would run forever once startedCreates the SOCIAL stream, binds 7 durables, writes celebration postsdead

6. Data it owns

Postgres logical DB feed_db. feed-service is the only writer; no other service reads or writes these tables (ARCHITECTURE.md §3.4).

TableWhat it holdsWritten by
postsEvery feed item: title, body, type, author, visibility scope and optional target, pinned/approved flags, media URI. A DB CHECK enforces that department/location/team posts carry a target and company/private posts do not.Admin create / moderate routes; would also be written by the dead auto-post pipeline
migrations/versions/001_initial_schema.py:94
reactionsOne row per person per reaction type per post.Reaction routes
migrations/versions/001_initial_schema.py:128
commentsComment rows. Designed in the SDD; no HTTP route in this service reads or writes them today.Nothing
migrations/versions/001_initial_schema.py:148 · model app/models/comment.py:6
post_viewsWho saw which post, when, for how long, and how much of it was on screen. UNIQUE(user_id, post_id), written with ON CONFLICT DO NOTHING.View-tracking routes and the feed read path
app/models/user_state.py:9 · migrations/versions/add_user_state_tracking.py
user_preferencesPer-person feed preferences and the soft-delete marker used by the GDPR path.Feed read path
app/models/user_state.py:22 · migrations/versions/add_user_prefs_deleted_at.py
post_templatesReusable wording for automatic celebration posts.Seeded by migration; read by the dead auto-post pipeline
migrations/versions/001_initial_schema.py:73
event_template_mapWhich event subject produces which post type and template.Seeded by migration; re-pointed by migrations/versions/fix_feed_category.py
migrations/versions/001_initial_schema.py:167
auto_post_configPer-tenant on/off switch per event subject for automatic posts.Admin auto-post settings routes
migrations/versions/305ebc942f22_add_auto_post_config_table.py:19

No cross-service writes in either direction. The only shared infrastructure is the Redis instance (users-auth-redis), where feed-service uses its own index 5.

7. Dependencies

flowchart LR
  MOB["Mobile app"] --> FS["feed-service"]
  ADM["Admin web"] --> FS
  UA["users-auth-service (GDPR CronJob)"] --> FS
  K8S["kubelet probes"] --> FS
  FS --> UA2["users-auth-service (profile batch)"]
  FS --> PG[("feed_db")]
  FS --> RD[("Redis index 5")]
  FS --> NW["notification-worker (via NATS)"]
  ML["micro-learning-service-v2"] -.->|"events nobody consumes"| FS

Dotted edge: micro-learning publishes the training subjects feed-service registers for, but the registrations never execute, so no message is ever delivered.

8. Dead-code verdicts

Every entry point with no in-repo caller. Deleting is a separate decision — see the hub roll-up.

Entry pointKindVerdictEvidence
EventProcessorService — the whole auto-post pipeline, including the SOCIAL stream creation and 7 durables Background loop dead No caller: class defined at app/services/event_processor.py:23; a repo-wide search for EventProcessorService finds only that definition plus a prose reference in app/services/gdpr_export_service.py:81. No instantiation anywhere.
Unreachable in principle: the only process entry point is uvicorn app.main:app, whose lifespan (app/main.py:43-80) starts Redis, the HTTP client and the heartbeat and nothing else; there is no second entry point, no worker Deployment and no CronJob in feed-service/k8s/. Matches bug-hunt-reports/feed-service.md #13.
subscribe_to_events() / handle_nats_message() — the 8 SSE push subscriptions NATS consumers dead No caller: the only invocation is the router-level @router.on_event("startup") at app/routes/sse_routes.py:212.
Unreachable in principle: the app is constructed with a custom lifespan= (app/main.py:132, function at :43). Under the pinned fastapi==0.109.2 / starlette==0.36.3 (feed-service/requirements.txt:1) a custom lifespan replaces the default lifespan context that would run on_startup handlers, so the hook never fires — reproduced in an isolated venv against the pinned versions, bug-hunt-reports/feed-service.md #2.
Latent risk, not a fix: a FastAPI upgrade past that boundary would switch these subscriptions on. The tenant filter in broadcast() was therefore hardened anyway (#6, app/routes/sse_routes.py:31-45). Two further hazards survive in that state: all eight durable names are derived from the subject alone (app/routes/sse_routes.py:197), so a second replica would bind the same durables; and connections are keyed by user_id only (app/routes/sse_routes.py:25), so a second device displaces the first (#5/#12).
GET /v1/feed/sse HTTP route dead No caller: repo-wide search for /v1/feed/sse matches only feed-service's own smoke scripts (tests/smoke_test.py:293, tests/smoke_reactions_sse.py:209), the gzip exclusion list (app/main.py:111) and the bug reports.
Unreachable in principle as a feature: the route itself answers, but the only writer into its queues is feed_manager.broadcast, called exclusively from handle_nats_message (app/routes/sse_routes.py:145, :164), which is registered only by the startup hook that never runs. A connected client can therefore receive nothing but the initial CONNECTED frame (app/routes/sse_routes.py:87) and a keepalive comment every 20 s (app/routes/sse_routes.py:63).
recognition.created, reward.redeemed, leaderboard.weekly.finalized NATS subjects (consumed) dead No publisher: a repo-wide search for each subject string returns only feed-service's own consumer lists, mapper and template seed rows (app/routes/sse_routes.py:188-190, app/services/event_processor.py:312-314, app/services/event_mapper.py:32-34, migrations/versions/001_initial_schema.py:178-180) plus feed-service tests. No publish( call anywhere in the repo names them.
And no producer can exist: the SDD names recognition-service, rewards-service and leaderboard-service as the senders (docs/sdd.md:47-51); none of those services is in the platform inventory (ARCHITECTURE.md §2). No stream in ARCHITECTURE.md §3.3 covers recognition.>, reward.> or leaderboard.> either — only feed's own never-executed add_stream("SOCIAL", …) would create one.
social.post.created (published) NATS subject (published) dead Publisher unreachable: published at app/services/event_processor.py:130 via _publish_post_event (:280), only from process_event inside the never-instantiated EventProcessorService.
No live consumer: the only subscribers are feed-service's own two registrations (app/routes/sse_routes.py:186, app/services/event_processor.py:308-315), both dead for the reasons above. A repo-wide search finds no other service subscribing to social.post.created or a social.> filter.
comments table and app/models/comment.py Data model dead Table created at migrations/versions/001_initial_schema.py:148 and modelled at app/models/comment.py:6, but no route, service or repository in app/ reads or writes it — commenting is an SDD goal (docs/sdd.md:33) that was never routed.
GET /health/startup HTTP route suspect app/core/health.py:84. feed-service/k8s/deployment.yaml declares only a livenessProbe (:58) and a readinessProbe (:64) — no startupProbe. Reachable by anything that can reach the pod, so not provably unreachable; it is simply unused by the cluster.
All /v1/admin/* routes · /v1/mobile/feed/home-highlights · GET .../reactions · GET .../reactions/summary · DELETE .../reactions/{reaction_type} · GET .../view-status HTTP routes suspect Searched the whole repo for each path string and for the handler names. Matches come only from feed-service's own tests/ scripts and audit documents. The admin web and the mobile app are not in this repository, so absence of an in-repo caller says nothing about production traffic — resolving these needs either the mobile/admin client repos or access-log sampling per path.

9. Sources