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-3875

Motir's public web presence — one host per audience, and an open-core line through motir-core (motir.co · app.motir.co)

To Do
Description

Motir has three public audiences and one working hostname. This epic gives each audience a host, and makes each host answer for exactly what it owns.

The three audiences

audiencehostwhat lives there
someone learning what Motir ismotir.cothe landing, the project square (/explore), the API/CLI/MCP reference (/docs), the published legal set (/legal) — all first-party, none of it session-gated
someone using Motirapp.motir.coeverything behind getSession(). Unchanged by this epic, and it keeps its own cookie scope
someone reading a customer's public projectmotir.co/p/<identifier> and its tabs — TENANT-authored markdown, comments, requests and votes. Decided 2026-08-29: the brand host, not a namespace of its own — see the trade recorded below

⚠️ RE-PLANNED 2026-08-29 — the direction reversed, and the open-core line is now part of the epic

The first version of this epic answered which host and never asked which repository, and its first story was built on "the landing is the smaller half and therefore the half that moves" — porting motir.co's landing into motir-core. That direction is rejected. motir-core is GPL-3.0 and must be a valid standalone product: a team running it for themselves gets a project-management tool, not Motir's marketing site. Where code has to move, it moves OUT of motir-core, and the cost of moving it is accepted rather than traded away.

The four decisions this epic now carries (Yue, 2026-08-29 — recorded in full on MOTIR-3879, which is the ADR that must write them down):

surfacedispositioninstrument
the landingstays in motir-marketing
/legalMOVES OUT of motir-coreit is moooon B.V.'s own contract text. CONTENT, not capability, so removal rather than a flag
/docsstays in motir-coregenerated from this repository's own OpenAPI spec and MCP registry; a self-hoster needs docs for THEIR build
/explore and /p/*code stays, GATED cloud-only; URLs move to motir.coMOTIR_CLOUD, the flag lib/billing/availability.ts already carries. Build-in-public is a cloud feature; self-hosting is single-tenant — a team doing project management for itself

⚠️ And the trade this makes with the security argument below, stated rather than absorbed. That research concluded 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. Putting /p/* on motir.co achieves the PRIMARY goal — it leaves app.motir.co, so the authenticated session's origin no longer renders tenant markdown — at lower isolation than the mirrors chose: first-party content and tenant content then share one origin. That is acceptable only while the session cookie stays host-only on app.motir.co, which therefore stops being a nice property and becomes a load-bearing constraint the ADR must state and the test gates must assert.

⚠️ The composition problem this creates, which is the ADR's central question. /explore and /p/* are served by motir-core, whose application answers app.motir.co. The landing and /legal live in motir-marketing, whose application is meant to answer motir.co. Two Fly applications cannot both answer an apex. So motir.co must be composed from two origins, and choosing how is MOTIR-3879's job — not a detail left to whoever picks up the first code card.

Where the line comes from — measured, not asserted

getSession() under motir-core/app/(public)/ on origin/main sorts the surface by itself: /explore and /explore/topic/* make no session call (explore/layout.tsx says so in as many words, and explore/(square)/page.tsx notes it is the only page there that does not); /docs "gates on nothing"; /legal the same, deliberately, so a procurement reviewer can read the subprocessor list without asking. /p/* calls it in twelve files — the layout, the page, all five tabs, the OpenGraph image, overview-actions.ts and the requests route.

The mirrors draw the same line, and were checked rather than remembered: Notion publishes pages on <workspace>.notion.site with a custom domain as a paid add-on; GitHub on <user>.github.io; Vercel on *.vercel.app; Canny on <company>.canny.io; Statuspage on <company>.statuspage.io. The reason is security before SEO. GitHub's "Yummy cookies across domains" records why Pages left *.github.com: user-controlled HTML under a subdomain of the application domain enables cookie tossing, session fixation and forced logout, and lends a phishing page the parent's credibility. github.io, notion.site and vercel.app are all on the Public Suffix List so browsers treat them as separate sites.

So app.motir.co was never the defect. An application subdomain is the convention and it stays. What is wrong is that the brand root is a one-page site whose every content link leaves it, and that tenant-authored content shares an origin with the session.

The stories

  1. 8.3 Marketing site + brand markMOTIR-656, re-filed here from Epic 8 on 2026-08-29. It built the brand, the landing and the entity-signal root; this epic is where the rest of that question lives.
  2. motir.co is served by motir-core — the landing comes home, /explore · /docs · /legal stop being cross-origin, one robots.txt, one sitemap, one canonical host, and app.motir.co 301s those paths.
  3. /p/* gets its own home — the tenant namespace, and the session-coupling that keeps it where it is today.
  4. Customer-owned addresses — per-tenant subdomains and custom domains, which is where the mirrors put a paid tier.

Boundary — what this epic does NOT own

  • The authenticated application. No route under app/(authed)/ moves, and the Better-Auth session cookie stays scoped to its own host. Widening it to .motir.co is explicitly REJECTED by story 3's premise, not deferred.
  • Marketing CONTENT. Positioning, pricing pages, blog and the SEO calendar are Epic 8's 8.6 (MOTIR-1105, MOTIR-1106, MOTIR-1119). This epic decides where pages are SERVED, not what they say.

⚠️ The one cross-epic edge, and why it is not lifted

8.10 Take Motir public stays in Epic 8 and is blocked_by MOTIR-656, which now lives here — so that story-level edge is cross-parent. No lift is legal. Lifting it to the epic tier would make ALL of Epic 8 — 8.5 production hardening included — wait on this epic, when only 8.10 consumes it, which is exactly the case plan-rules/phase-skeleton.md's no-legal-lift limb names: "Where only one child of a container needs the work, the lifted edge blocks every sibling on something none of them consumes." The edge stays at the story tier and the reason is recorded on both ends.

Acceptance criteria

  • Every public surface Motir serves resolves under exactly one of the three hosts in the table above, and no page's own chrome links to a content page on a different host.
  • motir.co serves one robots.txt and one sitemap.xml covering everything indexable on that host; app.motir.co serves a robots.txt that disallows the authenticated paths.
  • The Better-Auth session cookie's domain is unchanged — host-only on the application host — and no story in this epic widens it.
  • Each story carries its own ADR, and each ADR names which of the mirrors and which shipped file it argued from.
Status
To Do
Type
Epic
Children
9