Decision (ADR) — one origin per audience: motir.co is served by motir-marketing over motir-core's public API, and motir-core keeps no public pages
Type: decision · Executor: coding_agent · Write docs/decisions/public-surface-hosts.md in motir-core, and AMEND docs/decisions/marketing-site-hosting.md to point at it. No application behaviour ships in this subtask.
⚠️ THIRD AND FINAL RE-SCOPE, 2026-08-29 — read this before anything below
This card has been authored against three arrangements in one day. Its original body recommended option A (motir.co served by motir-core, the landing moves in) and told the record to reject B, C and D. A was rejected — motir-core is GPL-3.0 and may not carry Motir's marketing material. A second re-scope pointed it at composing the apex from two origins. Both are superseded. The decided arrangement is:
| host | application | serves |
|---|---|---|
motir.co | motir-marketing | the landing, /legal, /explore, /docs, /p/* — every public page, rendered over motir-core's public read API |
app.motir.co | motir-core | everything behind getSession(), plus the anonymous public read API |
motir-core ships NO public rendering at all, and public projects are a CLOUD capability: with MOTIR_CLOUD false the feature is absent, not hidden, because self-hosting is a team doing project management for itself — single-tenant.
The per-surface assignment is a GIVEN, not this record's question. Write it down with its reasoning; do not re-litigate it.
What this record DECIDES
- The seam.
motir-coreexposes an anonymous read API;motir-marketingis its consumer. Record what that makes the API — a published contract with a version and a deprecation policy — and why an internal BFF could not stay internal once a second repository renders from it. - The session cookie. Tenant-authored content now lives on
motir.co, which is the PARENT domain ofapp.motir.co. The cookie stays host-only onapp.motir.co— say so, and cite GitHub's "Yummy cookies across domains" and the Public Suffix List for why the arrangement is a deviation from every mirror. - ⚠️ THE DEVIATION, STATED RATHER THAN GLOSSED. Notion publishes on
<workspace>.notion.site, GitHub on<user>.github.io, Vercel on*.vercel.app, Canny on<company>.canny.io, Statuspage on<company>.statuspage.io— every mirror puts tenant-authored content on a SEPARATE REGISTRABLE DOMAIN, and all three of the first are on the Public Suffix List so browsers treat them as separate sites. Serving/p/*frommotir.cois better than today (it leaves the session's own origin) and weaker than the mirrors. Record the residual exposure — cookie tossing, session fixation, a phishing page borrowing the parent's credibility — and the reversal condition, which is MOTIR-3878's per-tenant addressing: a separate domain there resolves it. - What a SELF-HOSTED build serves at each public path with
MOTIR_CLOUDfalse./exploreand/p/*: the capability is absent./legal: gone from the repository./docs: present. A 404 is a decision, not a default — say which it is for each. - Who owns
robots.txtandsitemap.xmlper host, now that one host serves the public surface and the other serves an application and an API. - The COSTS, written as costs: a network hop where a Prisma read used to be, so empty and error states become real; a cross-repo contract that can break a live site; two implementations of one chrome asset; and
/docsgenerated frommotir-core's registries but rendered in another repository, which is a mirror unless it is a published artifact. - The reversal condition, concretely.
Rung-2 evidence (grep origin/main, not a stale checkout)
app/api/public/holds ten routes, and exactly ONE is session-gated —follow(POST/DELETE, 401), an account relationship.exploreandcategoriesmake zerogetSession()calls;explore/route.tssays so itself: "NOT session-gated: a logged-out visitor / crawler reads it … deliberately nogetSession()call." The/p/*reads take an optionalactorUserId = session?.user.id ?? nulland are "NOT session-gated on READ."- There is no
app/api/public/p/[identifier]/route.ts— the project's own subject has no endpoint while all four of its lists do. The sibling card adds it. app/(public)/p/is 12 files, one of them a Server Actions module.- ⚠️ Earlier readings of this surface were WRONG IN BOTH DIRECTIONS and the record should say so: a count of "six session-gated" and a later "eight" were both artefacts of grepping for the string
getSessionand matching COMMENTS, including the one that says the call is deliberately absent. The correction is on MOTIR-3877. motir-core/lib/baseUrl.ts— one variable answering two questions; the sibling card splits them.motir-core/lib/billing/availability.ts+docs/decisions/billing-tiering.md§6 —MOTIR_CLOUD, the existing explicit flag, and its two-questions-two-functions rule.motir-core/lib/apiDocs/{reference,mcp}.ts— what/docsis generated from, and why its move is a decision rather than a port.
Rung-1 evidence (observe, do not recall)
Vercel and Notion each serve marketing plus first-party content from one origin (vercel.com/docs, notion.com/help); Stripe splits reference docs to docs.stripe.com — a hostname per origin. For TENANT content the mirrors are unanimous and are cited in §3.
Acceptance criteria
motir-core/docs/decisions/public-surface-hosts.mdexists in the directory's Status → Context → Decision → Consequences shape, with a numbered-Q section and a per-Q rejected-alternatives table, matchingmarketing-site-hosting.md's convention.- It carries the per-surface host and repository table above, with the open-core reason per row, and records
/p/*as DECIDED — not deferred. - §3's deviation is stated on the record, with the mirrors named, the residual exposure described, and the reversal condition pointing at per-tenant addressing.
- The session-cookie scope, the self-host behaviour per path, and the
robots/sitemapownership are each stated explicitly. - The costs in §6 are written as costs, not as a case for the decision.
- Every rung-2 claim appears with the file it was read from, including the correction to the earlier miscounts; every rung-1 claim names the product and what was observed.
marketing-site-hosting.mdgains a dated AMENDMENT naming which of its answers stand (Q3 CI, Q4 subprocessor) and which are superseded.- Options rejected along the way — the landing moving into
motir-core, and composing the apex from two origins — each get a rejected-alternatives row with the reason, so a future reader does not re-propose them.
Context refs
motir-core/docs/decisions/{marketing-site-hosting,application-hosting,billing-tiering,platform-staff-auth,public-api-conventions}.mdmotir-core/app/api/public/·lib/baseUrl.ts·lib/billing/availability.ts·lib/apiDocs/- MOTIR-3877 — the corrected measurement of the public API
- MOTIR-3908 — the cloud gate this record describes
- GitHub, Yummy cookies across domains; the Public Suffix List