@instructure/platform-cohorts
v1.0.1
Published
Cohort management UI for Canvas.
Maintainers
Keywords
Readme
@instructure/platform-cohorts
Cohort management UI for Canvas.
Cohorts is a new domain model for grouping users into hierarchical groups agnostic of the account structure that powers exactly six things:
- enrollment into courses
- enrollment into programs
- enrollment into collections
- targeted global announcements
- targeted learner dashboards
- filtered reporting
This package is the foundational slice of that work. Per-product extensions belong in the consuming app, not here.
Read more: the tech spec. For Claude Code, the canvas-cohorts plugin carries a mirror of it plus a cross-repo code map.
The REST contract lives in openapi.yaml and is the source of truth for every API operation this package performs.
Development
Everything runs from the monorepo root unless noted.
pnpm install # link the package into the workspace
pnpm storybook # http://localhost:6006 — the dev loop
pnpm --filter @instructure/platform-cohorts test # vitest + coverage
pnpm --filter @instructure/platform-cohorts build # vite lib build + .d.ts
pnpm --filter @instructure/platform-cohorts type-check
pnpm --filter @instructure/platform-cohorts api:docs # browse openapi.yaml in Scalar, http://localhost:4727
pnpm lint # biome + oxlint + ast-grepMounting CohortsApp
CohortsApp is the root export. It mounts no providers and sets no InstUI theme, so the host
supplies all three, outermost first:
<InstUISettingsProvider theme={theme}>
<PlatformUiProvider {...platformProps} queryClient={queryClient}>
<CohortsI18nProvider locale={locale} translations={translations}>
<CohortsApp
accountId={accountId}
onSelectCohort={id => navigate(`/cohorts/${id}`)}
getCohortHref={id => `/cohorts/${id}`}
/>
</CohortsI18nProvider>
</PlatformUiProvider>
</InstUISettingsProvider>The host owns the URL: it reads IDs from its router and passes them in as props. The package
reports intent through onSelectCohort(id) and onBack(), and getCohortHref(id) lets rows be
real links. Stories get the light theme from CohortsStoryHost.
The contract
openapi.yaml is OpenAPI 3.1, 27 operations across 18 paths, with no
server implementing it yet. src/__tests__/openapi.test.ts proves it's
structurally complete: the document validates against the OpenAPI 3.1
schema and every $ref resolves, every operation has a unique
operationId, a 403, and a 404, every POST/PUT/DELETE also has a
400, the path+method matrix matches exactly, the Cohort,
WorkflowState, and ErrorCode schemas carry the fields the rest of the
contract depends on, no path reaches into pseudonym, generator, or sync
territory, and no request body accepts a full user object where an id
would do. If a check fails, the fix belongs in the YAML, not the test.
The contract uses no custom (x-*) vendor extensions. Service limits live in
the schema and response descriptions that enforce them, and which operations
extend an existing Canvas API is documented in each operation's own
description.
Which permission an operation requires and which feature flag gates it are determined by Canvas's real permission registry and feature-flag system, not documented per-operation in this contract.
Conventions consumers rely on
- Pagination: list operations return a
Linkheader in RFC 5988 format, matching the rest of the Canvas API. - Ids: every id is a 64-bit integer, modelled as the shared
Idschema. Canvas serialises ids as JSON strings instead when the request carriesAccept: application/json+canvas-string-ids, so clients should accept both forms. - Error payloads:
Error(message + optional machine-readableerror_code),ValidationError(per-attribute field errors), andUnauthorizedError(the403shape) cover every failure case. The one exception isbulkEnroll, whose pre-existing Canvas validation keeps its string-bodiedLegacyStringError. - Status codes follow Canvas: creates return
200, not201; an authenticated caller without permission gets403withstatus: unauthorized. No operation in this contract returns409. - List filters follow Canvas: lifecycle filtering is the repeatable
workflow_state[](active,deleted,all),per_pagecaps at 100, andsearch_termneeds at least two characters. - 404 means three things on purpose: a missing/deleted resource, the
institutional_cohortsflag being off for the root account, or the resource belonging to a different root account than the caller's. The API never distinguishes these, so it never reveals what it won't show you. - 400
feature_disabled: the three extended Canvas operations (bulkEnroll,createAccountNotification,updateAccountNotification) keep working when theinstitutional_cohortsflag is off, but reject their cohort-specific parameters with400and error codefeature_disabled, rather than 404ing the whole operation. - Form encodings: the three extended Canvas operations accept
application/x-www-form-urlencodedandmultipart/form-datawith the same schema.canvas-fetchemitsFormDatanatively, so multipart is the path of least resistance for clients in this repo.
Key contract decisions
createCohortcreates both kinds of cohort: passparent_cohort_idfor a sub-cohort, ornullfor a root cohort at depth 1 with no parent. An account may anchor several independent root cohorts — Canvas has no/rootroute and neither does this contract.listCohortsfinds them withroots=true.- Depth is 1-based (the root cohort is depth 1) and capped at 10.
- Deletion is soft: cohorts, memberships, and leader assignments move to
workflow_state: deletedrather than being removed. - A Leader's rights always cascade to every cohort beneath the one the assignment lives on — there's no way to scope a Leader to a single level.
- Adding a leader is idempotent: a duplicate
addCohortLeaderreturns the existing assignment with200, the way the Canvas admins API does. - Leader grants carry no ceiling on which roles a leader may hand out. An
earlier revision refused, with
403, any role other than the one that authorised the caller. That rule appeared nowhere in the tech spec, so it was dropped; Canvas instead enforces a subset rule onAccountUser(is_subset_of?: the granted role's permissions must be a subset of the caller's). If a ceiling is wanted, amend the spec first, and prefer the Canvas subset rule over strict equality. updateCohortLeaderchanges a leader's role in place. The tech spec does not describe it and the Canvas admins API has no equivalent (role change is delete plus create there), so it stays pending a spec amendment.getCohortTreedefaults todepth=2(the cohort and its direct children) so a client expands one level at a time unless it asks for more;depth=10returns the full hierarchy.- Bulk course enrolment, program enrolment, and collection enrolment are all one-shot actions: they resolve cohort membership once, when the job runs, and nothing keeps the target roster in sync with the cohort afterwards.
