Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,366
shipped

Motir · Roadmap

MOTIR-3908Done

Public projects are a CLOUD capability — with MOTIR_CLOUD false the feature is ABSENT, and a self-hosted Motir is single-tenant

Opened by Zhu Yue ·

⚠️ RE-SCOPED 2026-08-29 — the gate is the whole CAPABILITY, not the pages

This story was written to gate motir-core's /explore and /p/* PAGES. In the arrangement decided the same day, motir-core has no public pages to gate — every one of them moves to motir.co, rendered by motir-marketing. So the gate moves down a layer and gets simpler and stricter:

With MOTIR_CLOUD false there is NO public-projects feature at all. Not a hidden page — an absent capability. The publish affordance, the accessLevel = 'public' path, app/api/public/* in its entirety, the project square's services and the follow / request / vote surfaces: none of it is offered. Self-hosting is a team doing project management for itself — single-tenant, with no directory of anybody else's work and nothing published to strangers.

That is a cleaner line than gating pages ever was, and it is the one to build to. The sections below describe the page-gating framing and are kept only for the mechanism they cite, which is unchanged.


Build-in-public is a CLOUD feature. A self-hosted Motir is a team doing project management for itself — single-tenant, no directory of other people's projects, no public reading surface for strangers. This story makes that true in the product instead of true only in intent.

What is true today (read from origin/main, 2026-08-29)

motir-core ships app/(public)/explore/ (the square + topic/[slug]) and app/(public)/p/ (nine routes: the project, board, roadmap, tree, items, an item, changelog, the changelog feed, requests), over publicProjectsService (~14 read methods), projectSquareService and projectTagsService, plus app/api/public/ (10 anonymous routes). All of it is on by default in every build, self-hosted included. A self-hoster who deploys motir-core today gets a cross-tenant project directory they did not ask for and cannot turn off.

The mechanism already exists — this story uses it, it does not invent one

lib/billing/availability.ts carries isCloudBilling(), reading an explicit MOTIR_CLOUD env flag, decided in docs/decisions/billing-tiering.md §6: "Billing AND the §4 entitlement caps exist ONLY on Motir cloud; a self-hosted (GPL-3.0) build is uncapped and shows no checkout, no paywall, no caps." Its own comment states the discipline this story inherits: the flag is EXPLICIT and default false, deliberately NOT inferred from the presence of other config, "so a self-hoster who connects their OWN motir-ai is never force-billed."

The naming needs one decision this story makes rather than assumes: isCloudBilling() answers "is this a billing build?", and this story asks "is this a cloud build?". Read the ADR's §6 rule that two questions get two functions even when they read one variable, and follow it — a shared isCloud() beside it, or a second named predicate, but not a caller of isCloudBilling() from a non-billing surface.

The gate is PRESENT-but-off, not absent

This follows the shape already decided for the AI on 2026-06-16 and recorded in the chat front-door design: present-but-gated, NOT absent, NOT silently broken. A self-hosted build does not ship a broken route or a stack trace; it ships a deliberate answer. What that answer IS — a 404, or a short page saying this is a Motir Cloud feature — is this story's to decide and to state, because a 404 is a decision, not a default.

Boundary — where this story ENDS

  • It does not move any URL. Which hostname /explore and /p/* answer on is MOTIR-3876's and MOTIR-3877's. This story is about whether they answer AT ALL on a self-hosted build, and the two are independent: the gate is correct wherever the routes live.
  • It does not touch /docs. Those pages describe the software and a self-hoster needs them for their own build.
  • It does not touch /legal — that content leaves the repository entirely, which is its own story.
  • It does not make motir-core single-tenant. Workspaces, RLS and the tenancy model are unchanged; this is about which SURFACES a non-cloud build serves, not about the schema.
  • It does not gate app/api/public/* by accident and it must not gate it by omission either — those 10 routes are the same feature. Decide them explicitly and say so.

Verification recipe

  1. Build with MOTIR_CLOUD unset. Request /explore, /explore/topic/<slug>, /p/<identifier> and each of its tabs — every one gives the decided not-available answer, and none gives a stack trace or a 500.
  2. Request each app/api/public/* route on the same build — the decided answer, consistently with the pages.
  3. Set MOTIR_CLOUD=true and repeat — every surface renders exactly as it does today.
  4. Confirm the authenticated application is untouched in both builds: sign in, open a project, a board and an item.

Acceptance criteria

  • Every child is done; the repository set is motir-core alone.
  • ⚠️ With MOTIR_CLOUD unset, the public-projects CAPABILITY is absent, not merely hidden: every app/api/public/* route, the publish affordance, and any surface that offers a project to an anonymous reader. A test enumerates that surface from the filesystem so a route added later cannot escape the gate by being forgotten — the derived-not-remembered idiom tests/navigation/proxy-matcher.test.ts and tests/seo/robots.test.ts both use.
  • With MOTIR_CLOUD unset, /explore, /explore/topic/*, /p/* and the app/api/public/* routes all give the decided unavailable answer; with it set, all render as today. Both arms are tested — the self-host arm is the one that has never existed, so it is the one the suite must actually exercise.
  • The predicate is a named function beside isCloudBilling(), reading MOTIR_CLOUD, and no non-billing surface calls isCloudBilling() — asserted by a test, in the shape tests/hosting/appUrlSeam.test.ts already uses for single-reader rules.
  • The unavailable answer is the same on every gated surface, and the card records WHAT it is and why.
  • Nothing in app/(authed)/ changes behaviour in either arm.
  • ≥90% coverage on every file the story's PRs touch.

Context refs

  • motir-core/lib/billing/availability.tsisCloudBilling(), MOTIR_CLOUD, and the explicit-flag discipline
  • motir-core/docs/decisions/billing-tiering.md §6 — the cloud-vs-self-host build gate and its two-questions-two-functions rule
  • motir-core/lib/ai/planningConfig.tsisAiPlanningConfigured(), the OTHER flag this must not be confused with
  • motir-core/app/(public)/explore/ · app/(public)/p/ · app/api/public/ — the surfaces gated
  • motir-core/lib/services/{publicProjectsService,projectSquareService,projectTagsService}.ts — the services behind them, which are NOT deleted

Advisory dispositions

  • referenceMOTIR-3876 and MOTIR-3877 (severity advisory): not consumed, and no blocked_by is owed. Both are named in this story's BOUNDARY, which exists to say they are somebody else's — the gate is correct wherever those routes are addressed, and those stories are correct whether or not the gate has landed. The two are genuinely parallel and wiring an edge would serialise work that shares no output. validate_work_item on this story reads valid: true, blockers: [], which is the intended shape rather than an oversight.

Discussion

No comments yet.

Adding to this discussion signs you in on app.motir.co and brings you back to this request.

Add a comment