v4.62 — Feedback Tags & Public Board
A logged-out person with no account can read the feedback board at
feedback.objectuve.com, filter it by tag, and open a post with its comments — without a single field of a user's private account data being reachable by an anonymous request.
Summary
Before this milestone, the feedback board was authenticated-only end to end — feedbackPosts/feedbackPost both required a SessionToken, and there was no tag, label, or topic model anywhere in the codebase to organize posts by. Two roadmap items asked for related things: 45b wanted tag-based categorization inside the existing board, and 45c wanted an anonymous, crawlable public board for acquisition. Planning bundled them into one milestone on a simple finding — the public board renders the same post list the in-app board does, so shipping tags second would mean building the tag chips, filter row, and tag-aware query twice.
The milestone's one genuinely dangerous decision sat underneath 45c. FeedbackPostType.user resolves to the full Types::UserType, which exposes email, admin, goals, notifications, sign_in_dates, todays_mood, and private_mode — none of it field-authorized. The obvious way to make the board "anonymous," deleting the two require_auth! calls, would have published every feedback author's email address to any unauthenticated caller on the internet. v4.62 ships a separate, allow-listed PublicFeedbackPostType family instead — one that is structurally incapable of reaching Types::UserType, proven by a schema regression spec rather than by inspection — alongside a hard-cap, admin-managed tag system for the authenticated board, a server-side env-var kill switch for the public read path, and a standalone feedback_site/ Vue app with a crawler-facing SSR path that serves real post content. Every user-facing surface shipped dark on purpose: feedback_tags_enabled sits at 0% PostHog rollout, PUBLIC_FEEDBACK_READ_ENABLED is unset, and feedback.objectuve.com itself was never provisioned in GCP. This milestone is complete build-out, not a launch.
Goal
A person who has never signed up can read the feedback board at
feedback.objectuve.com, see what the community is asking for, and decide Objectuve is worth joining — and every post they read is filed under a tag that makes it findable, without a single field of a user's private account data being reachable by an anonymous request.
Scope — What Shipped
FeedbackTag/FeedbackPostTag(rails_api/app/models/) — an admin-managed tag vocabulary with a hard per-post cap (MAX_TAGS_PER_POST = 3), archive-not-delete semantics viaacts_as_paranoid, and admin CRUD (createFeedbackTag/updateFeedbackTag/archiveFeedbackTag) alongside an admin-gatedfeedbackTagsquery and a non-adminpickableFeedbackTagsquery for consumer surfaces.- Tags through the authenticated API —
tagsonFeedbackPostType, tag assignment at post creation, and tag filtering onfeedbackPosts. - An in-app tag picker, chips, and filter row in
ionic_frontend, behindfeedback_tags_enabled(0% rollout), specified by a UI-SPEC that ruled tags and categories as two independent filters and ruled out any usage-derived tag ordering ("Follow this tag," follower counts) under any flag — an explicit anti-vanity, anti-engagement-farming decision. - A public, unauthenticated GraphQL read path —
publicFeedbackPosts(status:, tags:)/publicFeedbackPost(id:), returningPublicFeedbackPostType/PublicFeedbackAuthorType/PublicFeedbackCommentType, a separate type family whose author field exposes only a display name (first name + last initial, or"a member") and an optional avatar URL — nothing else, and no path from it reachesTypes::UserType. Rate-limited 30/minute per authenticated user or per anonymous IP. Gated byENV['PUBLIC_FEEDBACK_READ_ENABLED'](absent ⇒ off for everyone, admins included) — a server-side kill switch chosen over a PostHog flag becauseFeatureFlagService.enabled?returnsfalseunconditionally for aniluser and would have left the board permanently dark for the anonymous audience it exists to serve. private_modeattribution — an author withprivate_modeon resolves to the same anonymized"a member"/null-avatar identity as an author with no name or photo at all, so the public board can never disclose who has opted out of being found.feedback_site/— a standalone Vue 3 + Vite app (ownpackage.json, Clerk + Apollo, no shared code withionic_frontend) that reads the board anonymously and gates every write (vote, comment, post) behind Clerk sign-in, deployed to its own Firebase Hosting target (enkidu-feedback) via a dedicatedfeedback.ymllane, decoupled from the weekly release train.- A crawler-facing SSR path —
PublicFeedbackControllerserves real, indexable post content in<body>to recognized crawler user agents, unlike thepublic_goalsprecedent's empty-body social-unfurl-only template. - CORS —
feedback.objectuve.comadded to the explicit origin allow-list. - Docs —
docs/features/feedback.md's data model, GraphQL section, and public-board contract reconciled against the shipped tree;docs/product/roadmap.mditems 45b and 45c ticked; the engineeringCHANGELOG.mdentry landed.
Phases
| Phase | Name | Status | Plans | Highlights |
|---|---|---|---|---|
| 1 | The FeedbackTag model and its admin surface | Shipped | 1 | FeedbackTag/FeedbackPostTag, admin CRUD, backend-only. MAX_TAGS_PER_POST shipped at the proposed value 5, later corrected to 3 by Phase 2/3. |
| 2 | UI-SPEC — tags on the in-app board | Shipped | 1 | Desi's spec: independent tag/category filters, admin-managed-only tagging (no user proposals), 3-tag cap with no overflow chip. Verified 2026-09-14; merged 2026-09-16 after a 30h branch-staleness misdiagnosis was corrected. |
| 3 | Tags through the API | Shipped | 1 | tags on FeedbackPostType, tag filtering, the MAX_TAGS_PER_POST = 3 correction, and the non-admin pickableFeedbackTags query Phase 2's spec required. |
| 4 | The in-app tag UI | Shipped | 1 | Picker, chips, filter row behind feedback_tags_enabled. Dispatched against Phase 2's verified-but-unmerged branch; zero spec-fidelity findings. |
| 5 | UI-SPEC — the public board | Shipped | 1 | Desi's spec for the anonymous read experience, the Clerk auth wall on write, and the private_mode attribution ruling. |
| 6 | The public read path | Shipped | 1 | PublicFeedbackPostType family, anonymous+authenticated rate limiting, CORS, and the schema regression spec proving no path reaches Types::UserType. Kill switch corrected from a PostHog flag to a server-side env var. |
| 7 | feedback_site/ and its hosting target | Shipped | 2 | The standalone Vue app, its Firebase Hosting target, and the crawler/SEO path with real body content. |
| 8 | Docs reconciliation | Shipped | 1 | docs/features/feedback.md, roadmap ticks, and the engineering changelog. ionic_frontend/CHANGELOG.md deliberately deferred — nothing in this milestone is reachable by a real user at close. |
Key Decisions
- A separate public GraphQL type, not a de-authed private one. A conditional type on the existing
FeedbackPostType/UserTypefails open — a field added toUserTypenext quarter would be silently exposed to the public resolver.PublicFeedbackPostType's allow-listed field set fails closed — a newUserTypefield stays invisible to the public path until someone writes it in on purpose, proven by a schema regression spec rather than left to inspection. - The public kill switch is a server-side env var, not a PostHog flag.
FeatureFlagService.enabled?returnsfalseunconditionally for aniluser, which would leave an anonymous-only surface permanently dark.ENV['PUBLIC_FEEDBACK_READ_ENABLED']is consulted identically for anonymous and authenticated callers and is deliberately not registered inscripts/sync-posthog-feature-flags.mjs. - Tags stay admin-managed in this milestone, with no user-proposal path. Admins can retroactively tag a post from the detail modal, which covers the practical need a proposal flow would have served without adding new moderation surface.
- 45b and 45c shipped as one milestone, not two. The public board is the same post list as the in-app board with the author narrowed and writes auth-gated; building the tag rail and tag-aware query once, in Phase 1–4, avoided building it twice.
Requirements Coverage
76 / 76 acceptance-criteria checklist items satisfied (per v4.62-feedback-tags-and-public-board-MILESTONE-AUDIT.md) — the ROADMAP's own - [x] bullets across all 8 phases, which also carries 51 formally-numbered requirement IDs under the TAG-/SPEC-/PUB-/SITE-/DOC-* prefixes (TAG-* 15, SPEC-* 13, PUB-* 8, SITE-* 9, DOC-* 6) — a narrower count of only the formally-ID'd items, not the full checklist below.
| Category | Count | Status |
|---|---|---|
Phase 1 — FeedbackTag model and admin surface | 7 | All satisfied |
| Phase 2 — UI-SPEC: tags on the in-app board | 7 | All satisfied |
| Phase 3 — tags through the API | 9 | All satisfied |
| Phase 4 — the in-app tag UI | 7 | All satisfied |
| Phase 5 — UI-SPEC: the public board | 8 | All satisfied |
| Phase 6 — the public read path | 14 | All satisfied |
Phase 7 — feedback_site/ and its hosting target | 17 | All satisfied |
| Phase 8 — docs reconciliation | 7 | All satisfied |
Full phase-by-phase acceptance criteria: v4.62-feedback-tags-and-public-board-ROADMAP.md on GitHub.
Outcomes
The authenticated board gains tags with zero behavior change while feedback_tags_enabled stays at 0% — every existing flow is unaffected until the flag ramps. Once both flags flip and feedback.objectuve.com is provisioned, an anonymous visitor will be able to read the board, filter by tag, and open a post with its comments, with no field of any author's private account data reachable through that path — a guarantee backed by a schema-level regression spec, not a code-review convention. Nothing in this milestone is reachable by a real user today: feedback_tags_enabled is 0%, PUBLIC_FEEDBACK_READ_ENABLED is unset, and feedback.objectuve.com's Firebase Hosting site has never been provisioned (OBJ-3864).
Tech Debt
- (Phase 8, D8-1)
ionic_frontend/CHANGELOG.mddeliberately carries no v4.62 entry — nothing in this milestone is user-visible at close. Owed whenfeedback_tags_enabledramps above 0%. - (Phase 8, D8-2 / OBJ-3897)
feedback_site/has no live schema-compat gate against production (scripts/check-feedback-schema-compat.mjsdoesn't exist). Harmless whilePUBLIC_FEEDBACK_READ_ENABLEDstays off; a precondition on flipping it, not free-floating debt. - (OBJ-3864)
feedback.objectuve.com's Firebase Hosting site was never provisioned in GCP — an operator action outside this milestone's scope. - No e2e/Playwright coverage exists for the public site — a recorded decision, not an oversight, and worth doing before the kill switch flips.
Related Artifacts
- Roadmap: v4.62-feedback-tags-and-public-board-ROADMAP.md
- Milestone Audit: v4.62-feedback-tags-and-public-board-MILESTONE-AUDIT.md
- UI-SPECs: v4.62-feedback-tags.md (Phase 2) · v4.62-public-feedback-board.md (Phase 5)
- Feature doc: Feedback § Public Feedback Board
- Git tag: none yet — no production tag has shipped this milestone.
- Merge PRs: #3188 · #3190 · #3210 · #3260 · #3209 · #3214 · #3230 · #3231 · #3273
Related Commits
b0df9d88df— Phase 1:FeedbackTagmodel and admin surfacef507109037— Phase 2: UI-SPEC — tags on the in-app board542de0cdce— Phase 3: tags through the APIbcf53bb0d7— Phase 4: the in-app tag UI90562f4fc6— Phase 5: UI-SPEC — the public board109fa9b11f— Phase 6: the public read path6ebba883d3/df53e0eff5— Phase 7:feedback_site/and its hosting target362499db69— Phase 8: docs reconciliation
Last updated: 2026-09-16