Agora
REST routes

REST routes

The ten built-in endpoints — the full request and response reference, how they are auto-registered, the three levels of control over them, and why every document name travels as a query or body field.

Collaboration is not only a WebSocket. Everything that is not the live document — the credential to open it, its comment threads, its version history, its raw bytes — travels over ordinary HTTP. The provider mounts ten endpoints under /collaboration at boot, and because they are ordinary Adonis routes, Tuyau types them for your frontend like any other.

The endpoints

MethodPathPurpose
GET/collaboration/token?doc=mint a WebSocket credential for a document
GET/collaboration/comments?doc=&space=&page=&size=list a document's comments, optionally one space
POST/collaboration/commentscreate an anchored comment
PATCH/collaboration/comments/:id?doc=resolve or reopen a comment
DELETE/collaboration/comments/:id?doc=remove a comment
GET/collaboration/versions?doc=&page=&size=list a document's versions
POST/collaboration/versionscreate a named version
POST/collaboration/versions/restorerestore a version into the live document
GET/collaboration/state?doc=read the raw document binary
POST/collaboration/state?doc=persist a document binary (worker only)

The document name never rides in the path

researches/42/writing has slashes in it, so putting it in a route segment means escaping it and un-escaping it correctly everywhere forever. Every endpoint takes the name in the query string (?doc=) or the body (docName) instead. The only path parameter in the whole surface is a comment id, which is a UUID.

Reference

GET /collaboration/token

The one endpoint with a security story of its own — see Token flow.

GET /collaboration/token?doc=researches/42/writing
{ "token": "eyJ1c2VySWQ…", "wsUrl": "/collaboration", "engine": "yjs" }
StatusWhen
400the doc query param is missing
401no authenticated user could be resolved
403authorize returned canRead: false

Comments

GET /collaboration/comments?doc=researches/42/writing&space=text

Returns CollabComment[], oldest first. Omit space for every space. page/size page the result — offset-based with a 1-based page (default 1) and a size clamped server-side to 1..200 (default 50), the same ?page=1&size=25 shape every @adonis-agora/* list endpoint uses. Omit both for the first page.

POST /collaboration/comments
Content-Type: application/json

{
  "docName": "researches/42/writing",
  "space": "text",
  "anchor": { "kind": "text-range", "start": 120, "end": 160, "selectedText": "the sentence" },
  "body": "rephrase this",
  "authorName": "Jane"
}

Returns the created comment, with its generated id and createdAt. There is no userId field: the author is the authenticated caller, and a body that supplies one is ignored.

PATCH  /collaboration/comments/:id?doc=researches/42/writing   { "resolved": true }
DELETE /collaboration/comments/:id?doc=researches/42/writing

resolved defaults to true, so an empty PATCH body resolves; pass false to reopen. DELETE answers { "deleted": true }. An id that is not in the document answers 404.

Both mutations are author-or-elevated: the person who wrote the comment may always resolve or delete it, and anyone else needs canWrite on the document — a moderating editor, not a fellow commenter. canComment alone gets you 403 on someone else's thread.

Versions

GET  /collaboration/versions?doc=researches/42/writing
POST /collaboration/versions            { "docName": "…", "label": "before review" }
POST /collaboration/versions/restore    { "docName": "…", "versionId": "…" }

GET /versions takes the same page/size paging as GET /comments. POST /versions returns the created CollabVersion; like a comment, its createdBy is the authenticated caller and a userId in the body is ignored. POST /versions/restore returns 204 and takes effect live — every connected client receives the restored content through the sync protocol, and the state the restore replaced is written to the history first as restored from #<seq>, attributed to the caller. See Versions.

Binary state

GET  /collaboration/state?doc=researches/42/writing   → application/octet-stream
POST /collaboration/state?doc=researches/42/writing   ← the worker's snapshot

Machine-to-machine, used by edge workers. POST requires the worker's shared secret in x-collab-worker-secret and answers 403 without it. See Binary state.

Three levels of control

Level one — leave them alone. The provider registers everything under /collaboration during its boot() — before Server.boot() commits the router, which is the only window in which a route is accepted. Nothing to write.

Level two — keep the handlers, change the wrapper. Prefix, middleware and user resolution are config:

config/collaboration.ts
routes: {
  prefix: '/api/collab',
  middleware: [middleware.auth(), middleware.tenant()],
  resolveUser: (ctx) => ({ id: String(ctx.auth.user!.id), name: ctx.auth.user!.fullName }),
}

Or register them yourself, for full control over placement in start/routes.ts:

start/routes.ts
import router from '@adonisjs/core/services/router'
import { collaborationRoutes } from '@adonis-agora/collaboration'

// with routes.enabled = false in config
collaborationRoutes(router, {
  prefix: '/api/collab',
  middleware: [middleware.auth()],
})

collaborationRoutes is synchronous, so it also composes inside a group you already have — a RouteGroup closes the moment its callback returns, and an await in there would drop the routes outside it:

start/routes.ts
router
  .group(() => {
    collaborationRoutes(router, { prefix: '/collab' })
  })
  .prefix('/api/v2')
  .use(middleware.auth())

Registering them yourself does not weaken the permission rule. With no explicit authorize option the routes resolve it through the manager — the same declared-document-then-global-then-deny resolution the WebSocket handshake runs, straight out of config/collaboration.ts.

Level three — write the controllers. Disable auto-registration and build the surface yourself, keeping the library's token semantics through the exported issueToken. See Custom controllers.

Authentication

The token endpoint resolves the caller through routes.resolveUser, which defaults to reading ctx.auth.user and throwing a 401 when there is none — so an install that forgot to protect these routes fails closed rather than minting anonymous credentials.

Every other endpoint runs the same check. Each resolves the caller through routes.resolveUser and then runs the document's authorize, requiring the permission that route needs:

EndpointRequires
GET /comments, GET /versions, GET /statecanRead
POST /versions, POST /versions/restorecanWrite
POST /commentscanComment
PATCH/DELETE /comments/:idcanComment and authorship — or canWrite to moderate

POST /state is the exception: it is machine-to-machine and authenticated by the worker secret instead. See Binary state.

A comment is attributed to the caller, not to the body

POST /comments takes the author from the authenticated user and ignores any userId in the request body — otherwise anyone could comment as anyone. The same applies to the createdBy on a version.

Authentication: the default resolver tries first

The routes enforce authorization — who may do what to which document. For authentication the default resolveUser calls ctx.auth.check() (or authenticate()) itself before reading ctx.auth.user, so a lazily-resolving guard still produces a caller without a global middleware bolted onto a hardcoded prefix. A request that cannot be authenticated answers 401.

Setting routes.middleware is still the right move when you want the whole group behind your own stack — rate limiting, tenancy, an audit trail — or when your guard is not reachable from ctx.auth. Then supply routes.resolveUser too.

On this page