MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-410

6.12.2 Decision — the `public` access-level semantics: cross-org read, write-grants, visible-vs-hidden, the openness ladder

Done
Description

Estimate: 45m

Type: decision (the keystone ADR the schema + access cards [6.12.3+] build against; no app behavior ships, but the shapes it fixes are load-bearing). Write motir-core/docs/decisions/public-projects.md, EXTENDING (not forking) 6.4’s access-level ADR. It MUST fix:

  1. public extends ProjectAccessLevel (open / limited / private / public). Add public to the existing 6.4 enum; the openness ladder is public > open > limited > private. State that this is a one-value extension of the 6.4 model + the SAME projectAccessService policy — NOT a parallel access system.
  2. public = ANYONE reads CROSS-ORG, no sign-in (the single exception). Decide precisely: 6.4’s canBrowse returns true for ANYONE — INCLUDING an unauthenticated/anonymous request — when the project is public, BYPASSING the 6.10 org/workspace membership gate FOR READ ON PUBLIC PROJECTS ONLY. The public read is server-rendered + crawlable (SEO/GEO). Every other level stays org/workspace-bounded; the 404-not-403 cross-tenant posture is preserved for non-public projects (a cross-org user hitting a non-public project is still not-found, never forbidden). Fix WHERE this exception lives so it is a single, auditable branch in the access policy (not scattered).
  3. Writes limited to triage-submit + upvote + comment — explicit grants, NOT a canEdit relaxation. A public viewer is not a member, so 6.4 canEdit is FALSE for every normal write (create/move/assign/status/field-edit). The three permitted writes are NEW narrow capabilities checked explicitly: canSubmitToTriage, canUpvotePublicRequest, canCommentPublicRequest — each true for any authenticated account on a public project, each independent of canEdit. State that no other write path may ever key off "is on a public project".
  4. Visible vs HIDDEN — the public projection. Decide the EXACT set of internal-only fields stripped from the public read: assignees, estimates, and internal comments are HIDDEN (decide explicitly which comments are "internal" vs public-request comments — the public-request comment thread from 6.12.6 IS public; the work item’s internal discussion is not). Fix that the stripping is a PUBLIC PROJECTION at the read layer (a dedicated read shape / DTO that never includes the hidden fields), NOT a UI that fetches everything and hides it (which would leak over the wire). Enumerate what IS visible: work item key/title/kind/status/description, board columns, the public roadmap, vote counts, public-request comments.
  5. READ anonymous; WRITE requires sign-in (revised 2026-06-14). Fix that READING a public project needs NO account (anyone, logged out, crawlers) — the page is server-rendered + crawlable (SEO/GEO). The three WRITES (submit / upvote / comment) require a signed-in account, so each is attributed + rate-limited by it; a logged-out write surface shows a sign-in-to-act prompt. Anonymous writes are out of scope (future — abuse + anonymous-identity model). State the share-link opens the public project with NO sign-in.
  6. Submission + dedupe + vote model semantics. A public submission reuses 6.11’s intake (born a triage work_item), attributed to the cross-org account. Fix the duplicate-detection contract (a deterministic title/text match over existing PUBLIC requests, surfaced BEFORE create so the user upvotes the existing one — Canny’s behaviour) and the vote model (one vote per account per item, server-enforced; the count is a sort key the 6.11 triage queue reads). Decide the vote storage (a PublicRequestVote join, unique on (workItemId, userId)).

Acceptance criteria

  • The ADR fixes all six sections, naming the FOUR-level ladder (public > open > limited > private) as a one-value extension of 6.4’s enum + the SAME projectAccessService.
  • It states the cross-org READ exception is public-only and lives in a single auditable branch, and that 404-not-403 holds for non-public projects.
  • It enumerates the three explicit write grants (submit / upvote / comment) as independent of canEdit, and the EXACT hidden-field set (assignees, estimates, internal comments) stripped by a public PROJECTION at the read layer.
  • It fixes the anonymous-READ / sign-in-to-WRITE rule (+ SEO/GEO crawlable public read), the duplicate-detection-before-create contract, and the one-vote-per-account model with its storage.
  • It cites the verified mirror (OpenProject/Plane/GitHub public visibility + Canny/Productboard/Featurebase portal set) for the public-project + submit/upvote/comment/status-roadmap shape.

Context refs

  • scripts/plan-seed/data/story-6.4.ts — the shipped ProjectAccessLevel (open/limited/private) + the projectAccessService canBrowse/canEdit policy this extends.
  • scripts/plan-seed/data/story-6.11.ts — the triage intake (6.11.4) + queue (6.11.3) the public submit reuses + the vote-count sort feeds.
  • scripts/plan-seed/data/story-6.10.ts — the org gate the public cross-org READ exception bypasses (for public projects only).
  • motir-core/lib/services/workItemsService.ts — the write authority submit/upvote/comment route through.
  • Canny (https://canny.io/use-cases/feature-request-management) — duplicate detection + upvote-the-existing + status roadmap; OpenProject (https://www.openproject.org/roadmap/) + Plane (https://plane.so/open-source) — public project / public roadmap visibility.