Public project pages join the brand host — /p/* answers on motir.co, and leaves the session's origin behind
⚠️ RE-SCOPED 2026-08-29 — the destination is decided, and so is the build
This story asked which hostname a public project is addressed at and left the answer open. It is motir.co (Yue, 2026-08-29), not a namespace of its own — and two further decisions land on it:
- The code stays in
motir-core, which this story already argued for below and measured. Nothing changes in that analysis; it is the reason the answer is an ADDRESS change and not a port. /p/*is CLOUD-ONLY. Build-in-public is a hosted-service feature; a self-hosted Motir is single-tenant, a team doing project management for itself. TheMOTIR_CLOUDgate is the cloud-gate story's deliverable, not this one's — but this story must not assume/p/*renders on every build.
⚠️ And the trade the destination makes, which this story now owns. The research below concluded that tenant-authored content belongs on a host of its own, because a cross-site-scripting hole in user content reaches whatever session shares its origin — the argument GitHub acted on when Pages left *.github.com. motir.co achieves the primary goal: /p/* leaves app.motir.co, so the authenticated session's origin stops rendering tenant markdown. It achieves it at lower isolation than the mirrors chose, because first-party content and tenant content then share one origin. That is acceptable only while the session cookie is host-only on app.motir.co — so this story asserts that, rather than assuming it, and its test gate is where the assertion lives.
A customer's public project stops living inside the application's origin. /p/<identifier> and its five tabs are TENANT-authored content — markdown, comments, feature requests, votes — and today they are served from the same host, and inside the same cookie scope, as the authenticated product.
The question this story answers, and the one it does not
It answers which hostname a public project is addressed at. It does not move any code out of motir-core, and the difference between those two is the whole story.
⚠️ motir-marketing cannot render /p/* from the public API — measured on origin/main
app/api/public/ exists and is more complete than it looks: /api/public/explore, /api/public/categories, /api/public/p/[identifier]/{tree,items,roadmap,changelog,follow,subscribe} and /api/public/projects/[projectId]/requests. Four findings kill it as a contract for a second renderer:
- There is no
app/api/public/p/[identifier]/route.ts. The project itself — name, overview/README, tags, workspace, the hero — has NO public endpoint. A second renderer cannot fetch the page's own subject, only its lists. - Six of those routes call
getSession()(tree,items,roadmap,changelog,follow,subscribe). They are viewer-aware, not anonymous-only, so a second renderer either loses the viewer or needs the session — which is the thing this story is separating. /api/public/*is not in the versioned contract.docs/decisions/public-api-conventions.mdgoverns/api/v1, and the published OpenAPI document is v1 REST only. Making a second repository a consumer promotes an internal BFF to a cross-repo contract with no version, no deprecation policy and no contract test./p/*is an application, not a page. It isPublicWorkItemTree,PublicRoadmap,PublicChangelog,PublicWorkItemList,PublicRequestComments,PublicRoadmapVote,PublicSubmitRequestForm,PublicFollowControlandPublicOverviewEditor, overoverview-actions.tsServer Actions, RLS-bound reads and project access gates. Rebuilding it against JSON is a second motir-core reached through HTTP instead of Prisma.
So the renderer stays where the data and the gates are, and the ADDRESS moves. Repository placement and URL placement are different questions; conflating them is what makes every option in this area look like a hack.
Why it leaves app.motir.co at all
Not for ranking. getSession() appears in twelve files under app/(public)/p/, and what those pages render is content a tenant wrote — MarkdownView over an authored overview, visitor comments, submitted requests. That is precisely the arrangement GitHub dismantled when it moved Pages off *.github.com: its engineering record ("Yummy cookies across domains") describes cookie tossing, session fixation, forced logout and the phishing credibility a parent domain lends, and github.io, notion.site and vercel.app are on the Public Suffix List so that browsers refuse to share cookies with them.
Every mirror that ships tenant public pages does the same thing — Notion <workspace>.notion.site, Canny <company>.canny.io, Statuspage <company>.statuspage.io, Productboard's portal — and each sells a custom domain on top.
The rejected option is worth naming: widening the Better-Auth session cookie to .motir.co. It would make a split trivially easy and it is exactly the wrong direction, because it hands tenant-authored HTML the session's own scope.
Scope
The ADR (the namespace shape, the isolation boundary, and what happens to the session-aware affordances); the routing that makes motir-core answer for the new host; the addressing scheme for a project; the canonical / OpenGraph / JSON-LD / sitemap origin for /p/*; 301s from app.motir.co/p/*; and what PublicTopBar's account menu, PublicAuthDialog, follow, vote and submit-request become once they are cross-origin from the session.
That last clause is the hard part of this story and is deliberately not pre-decided here — the ADR resolves it, having read what the mirrors do (Notion's published pages are read-only and hand you back to the app; Canny's portal carries its own identity).
Boundary
- Ends at one Motir-owned namespace. Per-tenant subdomains and customer custom domains are the next story; this one may choose a scheme that admits them, and must not ship the certificate-provisioning machinery they need.
- Does not touch
/explore. The directory is first-party and moves tomotir.coin the previous story; it will link cross-host into this namespace, exactly asnotion.com/templateslinks intonotion.site. - Does not move the authenticated application, and does not widen the session cookie.
- Does not re-plan
/p/*'s FEATURES. The tabs, changelog, follow and request intake are shipped and stay as they are.
Verification recipe
- Open a public project at its new address — the overview renders for a logged-out visitor.
- Move through Board, Items, Tree, Roadmap and Changelog — each stays on the new host, each canonical and OpenGraph URL names it.
- Submit a feature request as an anonymous visitor, and follow the project — both still work, by whatever mechanism the ADR chose.
- Request the old
app.motir.co/p/<identifier>— a 301 to the new address, tabs included. - From a browser signed in to
app.motir.co, load the public page and confirm the session cookie is NOT sent to the new host.
Acceptance criteria
- Every
/p/*route, its five tabs, its OpenGraph image and its request pages resolve on the new namespace, and their canonical,og:url, JSON-LD@idand sitemap entries all name that namespace. app.motir.co/p/*301s to the new address, path and query preserved.- The Better-Auth session cookie is not sent to the new namespace — asserted in a test, not assumed — and no cookie's
Domainattribute was widened to achieve the split. - The ADR names, per affordance (
UserMenu,PublicAuthDialog, follow, subscribe, vote, submit-request, in-place overview editing), 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 another repository, and no new consumer of
/api/public/*is created outsidemotir-core.
Context refs
motir-core/app/(public)/p/[identifier]/— the twelvegetSession()sites,overview-actions.ts,opengraph-image.tsxmotir-core/app/(public)/_components/—PublicTopBar,PublicAuthDialog,PublicFollowControl,PublicRoadmapVote,PublicSubmitRequestForm,PublicRequestComments,PublicOverviewEditormotir-core/app/api/public/— the BFF this story does NOT promote to a contractmotir-core/lib/publicProjects/urls.ts·lib/baseUrl.ts·app/sitemap.tsmotir-core/docs/decisions/public-projects.md·organization-url.md— the latter's reversal condition ("a shared read-only surface, a public organization page") is the trigger this story fires- GitHub, Yummy cookies across domains; the Public Suffix List — the isolation argument
Comments (0)
No comments yet — be the first to weigh in.