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,361
shipped

Motir · Roadmap

MOTIR-4217Implemented

(motir-core) The public contract resolves a host — GET /api/public/hosts/{host}, and the subject DTO carries the project's addresses and its primary

Opened by Zhu Yue ·

The producer card for the other repository — the one anonymous read motir-marketing's router needs to turn a Host header into a project, and the address fields the renderer needs to name a canonical. Without this route the router has nothing to call, and a consumer card describing "resolve the host through the public contract" is describing an integration with two ends (plan-rules/phase-deepen.md's cross-repo limb) — this is the end that lives here.

What ships

  • GET /api/public/hosts/{host}app/api/public/hosts/[host]/route.ts, in the shape of app/api/public/p/[identifier]/route.ts: publicSurfaceUnavailable() FIRST, then the rate-limit guard, no session read (this route is always anonymous — it is called server-side by the router before any page renders), one publicAddressesService.resolveHost(host) call, error mapping. Answers PublicHostResolutionDto:
    • for a live workspace_subdomain: { kind, workspace: { name }, projects: [{ identifier, name }] } — the public projects the host lists at its root (ADR Q2), each with its path;
    • for a workspace_subdomain_alias: { kind, redirectTo: <live hostname> } — the router 301s;
    • for an issued custom_domain: { kind, project: { identifier, name }, primary: boolean };
    • for a custom domain in any other status, an unknown host, or the base domain itself: 404 { code } — the surface's one refusal shape, no existence leak (a bot enumerating hosts learns nothing).
    • Cache-Control: public, max-age=60, stale-while-revalidate=300 — the router will read this on EVERY request to a tenant host, and a resolution changes only when a customer acts.
  • The subject DTO gains its addressesPublicProjectOverviewDto.addresses: { primary: string (absolute URL), alternates: string[] } computed by the ADR's Q6 rule, in lib/dto/publicProjects.ts and the service mapper; the renderer names primary in its canonical and treats every request on an alternates host as a redirect.
  • The crawl index gains primaryHostGET /api/public/projects rows carry the host the project's primary lives on, so motir.co's sitemap can omit projects whose canonical is elsewhere (a sitemap may list only its own host's URLs) and a tenant host's sitemap can list only its own.
  • Contract: the operation and the two schema changes declared in lib/api/public/openapi/operations.ts; a MINOR bump in lib/api/public/contractVersion.tsread that file on origin/main immediately before opening the PR and renumber if a sibling took the number (its own header names the hazard); contract-coverage, contract-drift, cloud-gate-totality and anonymous-posture all green with no exemption added.
  • Service: publicAddressesService.resolveHost (read-only, db singleton path as findPublicByIdentifier), lower-casing and punycode-normalising the host, refusing anything with a port, path or scheme.
  • Tests: every branch of the DTO union; the 404 arms (unknown, non-issued, the base domain, motir.co itself); the cache header; the subject DTO's addresses under each of the Q6 cases (no address, subdomain only, custom primary); the contract tests.

Boundary

Reads only — no writes, no session, no CORS change (that card), no rendering. The route is the PRODUCER; the consumer is the router in motir-marketing, which is blocked_by this card.

Acceptance criteria

  • GET /api/public/hosts/{host} answers each of the three 200 shapes above for fixtures of each kind, and 404 { code: 'NOT_FOUND' } for an unknown host, a non-issued custom domain, the base domain and motir.co; the gate runs before the rate limit and no getSession() call exists in the file.
  • The response carries the Cache-Control value above, asserted in a test.
  • PublicProjectOverviewDto.addresses.primary follows the ADR's Q6 default rule in a table-driven test (no address → motir.co/p/<id>; subdomain only → the subdomain path; custom primary set → that host), and alternates lists every other live address.
  • GET /api/public/projects rows carry primaryHost; the OpenAPI document declares the new operation and both schema changes; contractVersion.ts is bumped MINOR against the value on origin/main at merge time.
  • The four public-surface guard tests are green with no exemption entries added.
  • No file outside motir-core is touched.

Context refs

  • the decision — Q2, Q3, Q6; the storefindByHostname, listForWorkspace
  • motir-core/app/api/public/p/[identifier]/route.ts · app/api/public/projects/route.ts — the shapes to copy (gate first, { code } refusals, index paging)
  • motir-core/lib/api/public/openapi/operations.ts · lib/api/public/contractVersion.ts — the contract and its serialized version
  • motir-core/tests/api/public/contract-coverage, contract-drift, cloud-gate-totality, anonymous-posture
  • motir-core/lib/dto/publicProjects.tsPublicProjectOverviewDto; lib/services/publicProjectsService.tsgetOverview, listPublicForSitemap
  • motir-core/docs/decisions/public-surface-hosts.md §3 + AMENDMENT 1 §D — the contract's deprecation policy this addition lives under

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