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
144
planned
1,362
shipped

Motir · Work items

MOTIR-1851Done

11.1 `/api/v1` foundation — the pinned conventions, PAT bearer auth + scopes, the error envelope, cursor pagination, and per-token rate limiting

The FOUNDATION story of the public-API epic — every other story routes through it, so it ships first (plan-along-the-journey: a capability every step needs is infrastructure, not a late feature).

It delivers the /api/v1 envelope: the conventions pinned as an ADR, the shared route wrapper that authenticates a PAT and enforces its scope, the error shape, the pagination shape, per-token rate limiting — and the two smallest real endpoints that prove the envelope end to end.

The journey step

A developer with a Motir account: mint a token in settings → curl an authenticated endpoint → page through a list → get a clean 401 / 403 / 404 / 422 / 429 with headers that tell them what to do. That is the whole story, and it is independently testable the moment it lands: a person can hold a token and talk to Motir from a terminal, which they cannot do today by any means other than MCP.

Verified: the token-minting surface already existsapp/(authed)/settings/account/api-tokens/page.tsx with ApiTokensManager / CreateTokenModal, real non-route callers — so the journey audit resolves with no missing card. This story adds NO UI.

What to build

1. Pin the conventions as an ADR (the decision subtask). Versioning, auth header, error envelope, pagination, rate limits, resource naming and the deprecation policy — decided ONCE, in docs/decisions/, before a route exists, because every later endpoint inherits them and re-litigating per-endpoint is how an API becomes inconsistent.

2. The shared v1 route wrapper. One helper every /api/v1 route composes: authenticate the bearer PAT, enforce the required scope, map typed service errors to the envelope, stamp a request id. It REUSES authenticateApiToken — it does NOT re-implement auth.

3. Cursor pagination + the list envelope. One shape for every collection, reusing the opaque-cursor idiom already in lib/mcp/searchCursor.ts.

4. Per-token rate limiting. The one genuinely new primitive: grep confirms no rate-limit helper exists anywhere in lib/ (the rateLimit hits are Better-Auth's own config and account-settings copy). Public APIs are rate-limited — Plane enforces 60 req/min per key with X-RateLimit-Remaining / X-RateLimit-Reset headers — and an unlimited public API on a shared Postgres is a denial-of-service surface.

5. The proving endpointsGET /api/v1/me (identity, the read scope, no pagination) and GET /api/v1/workspaces (a real paginated collection). Deliberately the two smallest: they exercise auth + scopes + errors + pagination + rate limits with no resource modelling to argue about. The resource surfaces are 11.2 and 11.3.

The conventions, and the rung-1 evidence for each

Checked against the mirrors rather than asserted — both products' docs were read during this planning pass:

ConventionDecisionEvidence
VersioningPath-versioned /api/v1GitLab: "the path must start with /api/v4"; Plane: https://api.plane.so/api/v1/. Both path-version; neither uses header or query versioning.
AuthAuthorization: Bearer motir_pat_…Plane accepts Authorization: Bearer for OAuth tokens; GitHub uses bearer PATs. Motir's /api/mcp and its acceptance-video route already use exactly this header, and authenticateApiToken already parses it. Do NOT invent an X-API-Key alternative — one auth path.
ScopesThe shipped TokenScope setlib/mcp/scopes.tsread / work_items:write / work_items:archive / work_items:delete / sprints:write / integration, already typed total.
PaginationOpaque cursor, ?cursor=&limit=, default 50 / max 100Plane is cursor-based, page size max 100; GitLab's keyset mode returns X-NEXT-CURSOR. Cursor over offset because Motir's collections are mutable and offset pagination skips/duplicates rows under concurrent writes.
Errors{ code, error } + the HTTP statusAlready the established convention — app/api/work-items/[id]/route.ts returns { code: err.code, error: err.message }. Keep it; do not introduce a second error shape.
Rate limitsPer token, with X-RateLimit-Limit / -Remaining / -Reset + 429Plane: 60 req/min per key, those exact headers.
Resource pathsWorkspace/project-scoped nounsPlane: /workspaces/{slug}/projects/.

Scope BOUNDARY

Ends at the envelope + the two proving endpoints. It does NOT ship any work-item, sprint, project or ready-set resource (11.2 / 11.3); does NOT write the OpenAPI spec or the reference docs (11.4 — the ADR here records the conventions, the spec there publishes them); does NOT touch app/api/**, any service, any repository or any migration; and adds NO UI. It does NOT change /api/mcp (11.6 aligns that).

Type sweep — what this story does NOT need, deliberately

Swept the complete subtask-type set so an omission is a decision, not an oversight: design — no rendered surface, so the design gate does not fire (the reference page is 11.4's and carries its own design subtask). manual/human — no external account, secret, DNS or dashboard step; PAT issuance already ships. deploy — no new env var or infrastructure. copy / translate — no user-facing strings (error codes are machine identifiers, not copy). legal — the API ships under the repo's existing GPL-3.0; no new artifact. research/spike — the conventions decision below carries the grounding. So: one decision, three code, two test.

No acceptance video. This Story has no user-observable surface — it is exempt under the acceptance-video rule's non-UI carve-out and accepts on its tests alone.

Acceptance criteria

  • The conventions ADR exists in docs/decisions/ and every later v1 endpoint can cite it for versioning, auth, pagination, errors, rate limits, naming and deprecation.
  • GET /api/v1/me returns the token owner's identity for a valid PAT carrying read.
  • A missing / malformed / unknown / revoked / expired token returns 401 and the four cases are NOT distinguished in the response (matching the shipped MCP gate's deliberate non-disclosure).
  • A valid token lacking the required scope returns 403, never 401 and never a silent empty result.
  • GET /api/v1/workspaces pages with ?cursor=&limit=, defaults to 50, clamps above 100, and returns a stable next-cursor; paging a mutating collection never silently skips or duplicates a row (the reason cursor beats offset — asserted with a concurrent insert).
  • Every error response carries { code, error } and the correct status; a service's typed error maps to its status without leaking a raw Prisma or stack message.
  • Exceeding the per-token limit returns 429 with X-RateLimit-Limit / -Remaining / -Reset; a successful response carries the same headers. The limit is per TOKEN, not per IP or per user, so one integration cannot exhaust another's budget.
  • Cross-tenant isolation holds: a token bound to workspace A can read nothing from workspace B, and the response is the same 404-not-403 the rest of the product returns.
  • No /api/v1 route calls Prisma or opens a transaction — each is a thin adapter over one service method (the 4-layer contract).
  • motir-core's per-file coverage floor (≥90% branch/fn/line) holds on every new file.

Context refs

  • lib/apiTokens/routeAuth.tsauthenticateApiToken(req, requiredScope), the shipped bearer-PAT gate to REUSE.
  • lib/mcp/scopes.tsTOKEN_SCOPES / TokenScope / TOOL_SCOPES, the scope model and its totality pattern.
  • lib/services/apiTokensService.tsverify(plaintext){ user, workspaceId, scopes }.
  • lib/mcp/searchCursor.ts — the opaque cursor encode/decode to mirror.
  • app/api/work-items/[id]/route.ts — the { code, error } + status envelope convention.
  • app/(authed)/settings/account/api-tokens/ — the shipped PAT-minting surface this story's journey starts at.
  • tests/helpers/mcpHttpServer.ts — the real-HTTP-server harness the conformance suite reuses.
  • Parent epic: the public REST API.