Skip to content

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 via acts_as_paranoid, and admin CRUD (createFeedbackTag/updateFeedbackTag/archiveFeedbackTag) alongside an admin-gated feedbackTags query and a non-admin pickableFeedbackTags query for consumer surfaces.
  • Tags through the authenticated APItags on FeedbackPostType, tag assignment at post creation, and tag filtering on feedbackPosts.
  • An in-app tag picker, chips, and filter row in ionic_frontend, behind feedback_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 pathpublicFeedbackPosts(status:, tags:) / publicFeedbackPost(id:), returning PublicFeedbackPostType / 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 reaches Types::UserType. Rate-limited 30/minute per authenticated user or per anonymous IP. Gated by ENV['PUBLIC_FEEDBACK_READ_ENABLED'] (absent ⇒ off for everyone, admins included) — a server-side kill switch chosen over a PostHog flag because FeatureFlagService.enabled? returns false unconditionally for a nil user and would have left the board permanently dark for the anonymous audience it exists to serve.
  • private_mode attribution — an author with private_mode on 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 (own package.json, Clerk + Apollo, no shared code with ionic_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 dedicated feedback.yml lane, decoupled from the weekly release train.
  • A crawler-facing SSR pathPublicFeedbackController serves real, indexable post content in <body> to recognized crawler user agents, unlike the public_goals precedent's empty-body social-unfurl-only template.
  • CORSfeedback.objectuve.com added to the explicit origin allow-list.
  • Docsdocs/features/feedback.md's data model, GraphQL section, and public-board contract reconciled against the shipped tree; docs/product/roadmap.md items 45b and 45c ticked; the engineering CHANGELOG.md entry landed.

Phases

PhaseNameStatusPlansHighlights
1The FeedbackTag model and its admin surfaceShipped1FeedbackTag/FeedbackPostTag, admin CRUD, backend-only. MAX_TAGS_PER_POST shipped at the proposed value 5, later corrected to 3 by Phase 2/3.
2UI-SPEC — tags on the in-app boardShipped1Desi'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.
3Tags through the APIShipped1tags on FeedbackPostType, tag filtering, the MAX_TAGS_PER_POST = 3 correction, and the non-admin pickableFeedbackTags query Phase 2's spec required.
4The in-app tag UIShipped1Picker, chips, filter row behind feedback_tags_enabled. Dispatched against Phase 2's verified-but-unmerged branch; zero spec-fidelity findings.
5UI-SPEC — the public boardShipped1Desi's spec for the anonymous read experience, the Clerk auth wall on write, and the private_mode attribution ruling.
6The public read pathShipped1PublicFeedbackPostType 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.
7feedback_site/ and its hosting targetShipped2The standalone Vue app, its Firebase Hosting target, and the crawler/SEO path with real body content.
8Docs reconciliationShipped1docs/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/UserType fails open — a field added to UserType next quarter would be silently exposed to the public resolver. PublicFeedbackPostType's allow-listed field set fails closed — a new UserType field 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? returns false unconditionally for a nil user, 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 in scripts/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.

CategoryCountStatus
Phase 1 — FeedbackTag model and admin surface7All satisfied
Phase 2 — UI-SPEC: tags on the in-app board7All satisfied
Phase 3 — tags through the API9All satisfied
Phase 4 — the in-app tag UI7All satisfied
Phase 5 — UI-SPEC: the public board8All satisfied
Phase 6 — the public read path14All satisfied
Phase 7 — feedback_site/ and its hosting target17All satisfied
Phase 8 — docs reconciliation7All 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.md deliberately carries no v4.62 entry — nothing in this milestone is user-visible at close. Owed when feedback_tags_enabled ramps above 0%.
  • (Phase 8, D8-2 / OBJ-3897) feedback_site/ has no live schema-compat gate against production (scripts/check-feedback-schema-compat.mjs doesn't exist). Harmless while PUBLIC_FEEDBACK_READ_ENABLED stays 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.
  • b0df9d88df — Phase 1: FeedbackTag model and admin surface
  • f507109037 — Phase 2: UI-SPEC — tags on the in-app board
  • 542de0cdce — Phase 3: tags through the API
  • bcf53bb0d7 — Phase 4: the in-app tag UI
  • 90562f4fc6 — Phase 5: UI-SPEC — the public board
  • 109fa9b11f — Phase 6: the public read path
  • 6ebba883d3 / df53e0eff5 — Phase 7: feedback_site/ and its hosting target
  • 362499db69 — Phase 8: docs reconciliation

Last updated: 2026-09-16

Loading…