Public project pages move to motir.co — rendered by motir-marketing over motir-core's public contract
⚠️ RE-SCOPED 2026-09-01 — the direction reversed, and this body is the correction
Everything below the line in the previous version of this card argued the REJECTED direction — "the renderer stays where the data and the gates are, and the ADDRESS moves" — and its own comment of 2026-08-29 said so: "the card should not be run until its body says so." This is that rewrite. The four findings that conclusion rested on have each been settled since, on origin/main:
| the card's finding | what is true now |
|---|---|
"there is no app/api/public/p/[identifier]/route.ts" | Shipped — MOTIR-3945 |
"six of those routes call getSession()" | ⚠️ CORRECTED 2026-09-02 (MOTIR-4166). follow, a write).app/api/public/p/* and stated about the whole public write surface. Over both app/api/public and app/api/public-requests it is FIVE of thirteen: follow, the request intake and its duplicate pre-check, and the two request writes. The /p/* READS are anonymous, which is the half that was right and is what this story depends on. See public-surface-hosts.md AMENDMENT 4 §A |
"/api/public/* is not in the versioned contract" | It is — MOTIR-3946 gave it its own version, document and drift guard; MOTIR-3990 took it to totality |
"/p/* is an application, not a page (~25 components)" | Twelve files, one of them a Server Actions module — a real rebuild, not a second motir-core |
The decision is docs/decisions/public-surface-hosts.md §2: motir.co serves every public page, from motir-marketing, over motir-core's public contract. motir-core ships no public rendering at all.
⚠️ AND THE PAGES ARE ALREADY GONE — this story is a restoration, not a migration
Measured on the live hosts, 2026-09-01:
https://app.motir.co/p/MOTIR→ 404. MOTIR-3951 deletedapp/(public)/p(12 files);proxy.ts308s/p/*onto the public origin, but only onceMOTIR_PUBLIC_SITE_URLis configured, which is the cutover's job.https://motir.co/p/MOTIR→ 404. Nothing serves it there yet. That is this story.https://motir.co/explore→ 200, renderinghref="/p/MOTIR"on every card.
So the live directory links into a page no host answers for. Closing that window is this story, and it is why this card is highest. The interim disposition for those dead links is filed separately, under the epic.
What this story delivers
Seven screens on motir.co, four new endpoints on app.motir.co, and one decision that has to come first.
- The decision.
public-surface-hosts.mdsettled the host, the seam, the cookie and the cloud gate. It did not settle what happens to the affordances that read or require a session — follow, subscribe, roadmap vote, request upvote, request comment, submit request, in-place overview editing, and the top bar's account treatment — once the page is cross-origin from the session. §9 does not list it as deferred; it is simply open. AMENDMENT 2 closes it, per affordance, against what the mirrors do. - The four missing reads.
MOTIR-3951's service sweep keptgetBoard,getRoadmap,getWorkItemDetail,getRequestDetailandsetPublicOverviewprecisely because "MOTIR-3877's motir-marketing/p/*pages will re-expose them through new API routes." Those routes are this story's. Two more are owed beside them: the changelog Atom feed, whose builder (lib/publicProjects/atomFeed.ts) has had no caller since the deletion, and a public-project index somotir.co's sitemap can enumerate what it advertises. - The pages, in
motir-marketing, over that contract: the project shell and Overview, the five tabs, the two detail pages and the request intake, the act affordances, and the crawl surface. - The gates: a Vitest gate per repository, and a Playwright walk with an acceptance video — which this repository has no lane for yet, so building that lane is part of the story.
Scope
The ADR amendment; the four-plus-two public read routes and whatever the amendment needs on the motir-core side; the whole /p/* rendering surface in motir-marketing; its canonical / OpenGraph / JSON-LD / sitemap / Atom crawl surface on motir.co; and the assertion — not the assumption — that the Better-Auth session cookie stays host-only on app.motir.co.
Boundary
- The
app.motir.co/p/*→motir.coredirect is ALREADY SHIPPED and is not this story's. MOTIR-3884 putpinproxy.ts'sPUBLIC_REDIRECT_SEGMENTSand its matcher, and MOTIR-3886 asserts it. It fires when the cutover setsMOTIR_PUBLIC_SITE_URL. No card here re-owns it. - Ends at ONE Motir-owned host. Per-tenant subdomains and customer custom domains are MOTIR-3878's, and §4's reversal condition hands it the separate-registrable-domain question. Nothing here ships certificate-provisioning machinery. (The graph edge runs the other way — this story
blocks3878 — so thereferenceadvisory raised on that name is CONTEXT, not a missingblocked_by; recorded here rather than left undisposed.) - Does not touch
/explore,/docsor/legal— MOTIR-3932 moved them. - Does not widen the session cookie. Not deferred — rejected (§4), and the condition the whole arrangement's residual exposure rests on.
/p/*is CLOUD-ONLY. TheMOTIR_CLOUDgate is MOTIR-3908's and is shipped; every new route here inherits it, and none of them re-opens it.- Does not re-plan
/p/*'s FEATURES. The tabs, changelog, follow and request intake are shipped behaviour being re-hosted, not redesigned.
⚠️ The trade this makes, which this story owns
The research that started this epic concluded that tenant-authored content belongs on a registrable domain of its own, because a cross-site-scripting hole in user content reaches whatever session shares its origin — GitHub's "Yummy cookies across domains", and the reason github.io, notion.site and vercel.app are on the Public Suffix List. motir.co achieves the primary goal: /p/* leaves the authenticated session's origin. It achieves it at lower isolation than the mirrors chose, because first-party and tenant content then share one origin, and that origin is the parent of the application's. That is acceptable only while the session cookie is host-only on app.motir.co — so this story asserts it in a test rather than intending it.
Verification recipe
- Open a public project at
motir.co/p/<identifier>— the overview renders for a logged-out visitor. - Move through Board, Items, Tree, Roadmap and Changelog; page one list and expand one tree level.
- Open a work item, and a feature request, from those lists.
- Submit a feature request — sign-in-to-act, not anonymous (⚠️ corrected 2026-09-02, MOTIR-4166:
POST /api/public/projects/{projectId}/requestshas always required a session). Follow the hand-off AMENDMENT 4 row 6 specifies: leave forapp.motir.co, submit through the duplicate step there, and return to the project page, where the new request is visible. - Follow the project, and exercise the vote / comment / overview-edit affordances as the amendment specifies them — including returning to the page you left.
- Subscribe to the project's changelog at
motir.co/p/<identifier>/changelog.xmland get a valid Atom document. - From a browser signed in to
app.motir.co, load the public page and confirm the session cookie is not sent tomotir.co.
Acceptance criteria
- Every
/p/*route — the project, its five tabs, the two detail pages, the request intake and the changelog feed — resolves onmotir.co, rendered bymotir-marketing, reading onlymotir-core's public contract. - Their canonical,
og:url, JSON-LD@idand sitemap entries all namemotir.co; a per-project OpenGraph card is served. - The Better-Auth session cookie is not sent to
motir.co— asserted in a test — and no cookie'sDomainwas widened to achieve the split. docs/decisions/public-surface-hosts.mdAMENDMENT 2 names, per affordance, what happens to it once the page is cross-origin from the session, and which mirror's behaviour that follows.- No public-project rendering code is duplicated into a second repository, and
motir-marketinggains no database client. - Every new
app/api/public/*route carries a contract operation, theMOTIR_CLOUDgate and an anonymous-posture entry, with no exemption added to an existing guard. - The changelog Atom feed answers again, at the path
proxy.ts's redirect produces. motir-marketinghas a Playwright lane gating its deploy, and this story's E2E publishes an acceptance video to this card.
Context refs
motir-core/docs/decisions/public-surface-hosts.md— §2 (the host table), §3 + AMENDMENT 1 (the contract), §4 (the cookie and its reversal condition), §5 (the cloud gate), §7 (this card's row), §8 (the costs), §9motir-core/app/api/public/— the twelve shipped operations, and the reads this story adds beside themmotir-core/lib/services/publicProjectsService.ts—getBoard·getRoadmap·getWorkItemDetail·getRequestDetail·setPublicOverview·listPublicForSitemap, kept by MOTIR-3951 for this storymotir-core/lib/publicProjects/atomFeed.ts— the builder with no callermotir-core/proxy.ts—PUBLIC_REDIRECT_SEGMENTS, and why/p/*is in itmotir-core/app/(public)/p/@95a2d4468^— the twelve deleted files, as prior artmotir-marketing/lib/explore.ts·app/explore/— the shipped pattern for a surface fed by the public APImotir-marketing/.github/workflows/ci.yml— the lane that has noe2ejob yetmotir-core/design/public-projects/·design/public-site/— the surface as it was, andmotir.co's chrome
⚠️ ORDINAL CORRECTED AGAIN, 2026-09-02 — it is AMENDMENT 4. MOTIR-4139 (
b615991c4) merged its own AMENDMENT 3 intopublic-surface-hosts.md— the subprocessor seam — while this story was in flight, so the affordance table is AMENDMENT 4. Every reference to AMENDMENT 2 or AMENDMENT 3 on this card and its siblings means that section. Twice in one day: an ADR ordinal is a serialized resource and nothing in the corpus says so — the amendment's own banner now does.