Skip to content

Resource Kind Naming — The cms.page Convention

Every resource room is keyed by a (Kind, Id) pair. The Kind is a stable, lowercase, dotted discriminator that names what type of thing the room is about — cms.page, documents.file, releases.changeset. This page is the convention for choosing one.

The Id is opaque to the framework (a stringified GUID, a slug, a composite key — whatever the owning module uses) and is not covered here beyond its 256- character cap. The Kind, by contrast, is a shared namespace: it shows up in URLs, in metric tags, and in cache keys across every module that opens rooms. A loose convention there means collisions and cardinality blow-ups.

<module>.<entity>
  • Lowercase only. No capitals — kinds appear verbatim in URLs and metric tags, where case sensitivity is a foot-gun.
  • Dotted segments. A leading module namespace, then the entity. Deeper nesting is allowed (documents.assetmetadata.pdf) but keep it shallow.
  • Stable. The kind is part of the URL contract and the cache key. Renaming it orphans every in-flight room. Treat it like a public API.

| Kind | Room is about | |------|---------------| | cms.page | A CMS page being co-edited | | documents.file | A document open in a preview / annotation pane | | dashboards.dashboard | A dashboard several analysts are watching live | | activities.activity | An activity / task detail view | | releases.changeset | A changeset under review |

The framework validates Kind eagerly in ResourceRef.Validate() — called by JoinAsync/GetAsync/LeaveAsync and by every REST endpoint. The pattern is:

^[a-z][a-z0-9_.-]{0,63}$

In words:

  • Must start with a lowercase letter (az).
  • May then contain lowercase letters, digits, _, ., -.
  • Maximum 64 characters total.
Granit.Presence.Abstractions.ResourceRef
[GeneratedRegex("^[a-z][a-z0-9_.-]{0,63}$")]
private static partial Regex KindPattern();

A kind that fails the pattern throws ArgumentException from the tracker, and surfaces as a 400 Bad Request (RFC 7807 ValidationProblem) from the REST endpoints — never a 500.

Resource rooms are a shared, app-wide keyspace. Every module that opens a room writes into the same (Kind, Id) table. Without a module prefix, two modules that both pick page — a CMS page and a wiki page — would silently share a room: open the wiki page with id 42 and you would see the avatars of people editing the CMS page 42. The <module>. prefix is what makes cms.page and wiki.page distinct.

The prefix also bounds metric tag cardinality. resource_kind is a tag on every room counter and histogram. A disciplined, finite set of kinds keeps the metrics backend healthy; ad-hoc kinds derived from user data would explode it.

Each module owns a <module>. prefix. The table below is the living registry — when you add rooms to a module, claim its prefix here and list the entity kinds you use so other teams can see the keyspace at a glance.

| Prefix | Owning module | Known kinds | |--------|---------------|-------------| | cms.* | granit-website CMS | cms.page | | documents.* | Granit.Documents | documents.file | | dashboards.* | Granit.Dashboards | dashboards.dashboard | | activities.* | Granit.Activities | activities.activity | | catalog.* | Granit.Catalog | — | | entities.* | Granit.Entities | entities.record | | releases.* | release-management (example) | releases.changeset |

  • Singular, concrete noun. cms.page, not cms.pages or cms.content.
  • Match the aggregate. If the module’s EF aggregate is CmsPage, the kind is cms.page. The mapping should be obvious to anyone who knows the domain.
  • One kind per co-editable surface. If a document has both a metadata editor and a renditions panel that should not share a face-pile, model them as two kinds (documents.file, documents.renditions) — not one kind with metadata disambiguation.