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 · Work items

MOTIR-1857Done

11.1.1 Decision — the `/api/v1` contract: path versioning, bearer-PAT auth + scope mapping, `{ code, error }` errors, opaque-cursor pagination, rate limits, resource naming, deprecation policy

Repo: motir-core. One PR — a docs-only diff (docs/decisions/**), so branch as docs/MOTIR-<id>-<slug> (CI skips the E2E + integration lanes; the branch prefix follows the DIFF, not the card type).

Records the /api/v1 contract as an ADR before any route exists, because every endpoint in 11.2 / 11.3 inherits these choices and re-deciding them per-endpoint is how a public API becomes inconsistent. This is also the citable artifact later cards point at — a card that says "per the public-API conventions" must be able to resolve that to a real file.

The QUESTION

What exactly does Motir promise a third-party HTTP client: how is the API versioned, how does a caller authenticate and what may each token do, what does a collection response look like, what does a failure look like, what limits apply, how are resources named, and what does v1 guarantee over time?

The RECOMMENDATION, per axis, with its evidence

Each row is a decision to RECORD, not to re-open. Rung-1 evidence was gathered by reading both mirrors' published API docs during the planning pass (2026-07-29), and rung-2 by grepping origin/main — cite both in the ADR.

AxisDecisionGrounding
VersioningPath-versioned: every route under /api/v1. A breaking change mints /api/v2; v1 then keeps working.Rung 1: GitLab — "the path must start with /api/v4"; Plane — https://api.plane.so/api/v1/. Neither header- nor query-versions.
AuthAuthorization: Bearer motir_pat_…, resolved by the SHIPPED authenticateApiToken. No second auth mechanism — no X-API-Key, no query token, no cookie fallback.Rung 2: lib/apiTokens/routeAuth.ts already does exactly this on a REST route, and /api/mcp uses the same header. Rung 1: Plane accepts Authorization: Bearer for OAuth; GitHub uses bearer PATs. A cookie fallback is rejected on purpose — it would make CSRF a concern on a surface that has none.
ScopesReuse TokenScope verbatim; map per operation, not per resource. Reads → read; work-item writes → work_items:write; archive/restore → work_items:archive; sprint writes + membership → sprints:write. work_items:delete stays unexposed in v1's first cut.Rung 2: lib/mcp/scopes.ts — the set is shipped and typed total, and TOOL_SCOPES is the per-operation precedent.
Errors{ code, error } + the HTTP status. code is a stable machine identifier (never localized, never reworded); error is a human sentence. 401 undifferentiated for missing/malformed/unknown/revoked/expired; 403 for scope refusal; 404-not-403 for cross-tenant; 422 for validation.Rung 2: app/api/work-items/[id]/route.ts returns { code: err.code, error: err.message } — the established convention; and the 404-not-403 cross-tenant rule is the product's existing behaviour, which the public API must not weaken into an existence oracle.
PaginationOpaque cursor: ?cursor=&limit=, default 50, hard max 100; response carries the next cursor. No offset pagination.Rung 1: Plane is cursor-based with a 100 max; GitLab's keyset mode returns X-NEXT-CURSOR. Rung 2: lib/mcp/searchCursor.ts already encodes opaque cursors. Offset is rejected because Motir's collections mutate under concurrent writes, where offset silently skips and duplicates rows.
Rate limitsPer TOKEN (not per IP, not per user), returning X-RateLimit-Limit / -Remaining / -Reset on every response and 429 when exceeded. Pick and record the initial budget.Rung 1: Plane enforces 60 req/min per key with those headers. Per-token so one integration cannot exhaust another's budget.
Resource namingPlural, hyphenated nouns, scoped by their parent: /api/v1/projects/{projectKey}/work-items, /api/v1/sprints/{sprintId}/work-items. work-items, never issues — the product noun is work item. Keys are the MOTIR-<n> identifier, not the internal cuid.Rung 1: Plane scopes by /workspaces/{slug}/projects/… and calls them work items. Rung 2: the terminology rename already shipped product-wide.
StabilityAdditive-only within v1: new fields and new endpoints may appear; a field is never removed, renamed or re-typed without a new major. Deprecation is documented + announced before removal.Rung 1: GitLab's v4 has held for years on exactly this promise — the property that makes an API integrable.

What it GATES

11.1.2 (the route wrapper) is blocked_by this card; every other endpoint story cites the ADR rather than re-deriving a convention. If a later story finds a convention here to be wrong, that is an ADR amendment card — not a per-endpoint deviation.

Scope BOUNDARY

A docs-only deliverable — one ADR file. It writes NO code, NO route, NO test, and does not create the /api/v1 directory. It does NOT decide anything about the OpenAPI spec's tooling (11.4 owns that), nor which resources exist (11.2 / 11.3), nor GraphQL (out of the epic entirely). It does NOT introduce a new auth mechanism or a new scope — if a v1 operation genuinely needs a scope the shipped set lacks, that is a separate card against lib/mcp/scopes.ts, flagged here, not decided here.

Acceptance criteria

  • The ADR exists at docs/decisions/public-api-conventions.md, following the format of the ADRs already in that directory.
  • Every axis in the table above is recorded as a DECISION with its rung-1 (mirror) and/or rung-2 (shipped-code) evidence cited by file path or doc URL — not asserted.
  • The scope map is written as an explicit operation → scope table a route author can implement from without re-deciding.
  • The error contract enumerates each status with the condition that produces it, and states that code values are stable machine identifiers.
  • The rate-limit budget is a concrete number with its rationale, not "TBD".
  • The stability section states what additive means and what forces a v2.
  • Rejected alternatives are recorded with WHY: offset pagination, X-API-Key, header/query versioning, a cookie fallback, and exposing work_items:delete in the first cut.
  • No code, route or test file is touched by this PR.

Context refs

  • docs/decisions/ — the ADR directory + its existing format.
  • lib/apiTokens/routeAuth.tsauthenticateApiToken, the shipped bearer gate the auth decision ratifies.
  • lib/mcp/scopes.tsTOKEN_SCOPES / TOOL_SCOPES, the scope set and the per-operation mapping precedent.
  • lib/mcp/searchCursor.ts — the opaque-cursor encoding the pagination decision adopts.
  • app/api/work-items/[id]/route.ts — the { code, error } envelope this ratifies.
  • Parent story: 11.1. Parent epic: the public REST API.