Per Permission Standards, which names this file. Part of the module spec.
P02d-4 Step 3 consumer implemented — 2026-10-04. Education consumes this internal projection under its public boundary. Customization adds no HTTP route, permission key or audit intent; field requiredness is internal metadata, and schema validation remains write-only.
P02d-3 Step 3 implemented — 2026-10-02; both review rounds passed.
ICustomizationDefinitionProjectionReader is internal and unrouted, uses trusted
ambient scope and adds no HTTP endpoint or permission key. Public admission belongs to
P02d-4. The module contract owns its scope.
No permission keys yet. The matrix below is a forward declaration in the
{module}.{resource}.{action} form with the closed action set of
Permission Standards. Its adopted module-source
contract registers scoped definitions separately from finite grants; no IModule
API is implied. Identity's foundation definitions/Role templates are implemented.
Customization keys and their live consumers require their owning operation
decision; these examples register or grant nothing.
| Resource | read | write | delete | admin | Default role grants |
|---|---|---|---|---|---|
ContentType |
✓ | ✓ | ✓ | – | tenant-admin: read+write+delete; editor: read |
ContentTypePublication |
✓ | ✓ | – | – | tenant-admin: read+write; editor: read |
LevelTaxonomy |
✓ | ✓ | ✓ | – | tenant-admin: read+write+delete; editor: read |
LevelTaxonomyPublication |
✓ | ✓ | – | – | tenant-admin: read+write; editor: read |
Publication is a sub-resource, because the action set is closed. Authoring a schema and making it the live answer for a key are two decisions — the module models them as two commands for exactly that reason — and they carry different blast radii. A wrong draft constrains nothing: it exists, it is addressable, and no content row is validated against it. A wrong publication retires the incumbent in the same transaction, and every subsequent write of that content type is validated against the new shape.
The way to say that is
Permission Standards § Closed Action Set's
own: there is no publish action, and "if a verb does not fit, model the verb as
a distinct sub-resource" — its first worked example being "publish a course" →
education.course_publication.write. So customization.content_type.write
covers Register and the additive revision, and
customization.content_type_publication.write covers Publish. An earlier
version of this matrix put publication on admin instead, which read as the same
split and was not one: admin means "full control including ownership transfer",
no default role held it, and the result was a matrix in which nobody could
publish anything.
delete is a real action here, and it is soft. A Deprecated revision is
never removed — content rows pin the version they were written against — but a
Draft is removable, and the partial index is partial on deleted_at IS NULL
precisely so that a retired definition releases its key to a successor.
Permission Standards defines delete as
"remove or soft-delete the resource", and audit.md classifies the
same operation as MUST-audit; a matrix with no delete disagreed with both.
Publication has none, because unpublishing is not deletion: the successor's
publish is what retires a revision.
Six write commands are unrouted and have finite source admission.
CustomizationOperationPolicySource binds four authoring commands and four
verification queries to Seed authority. The two default initializers admit only
their declared Seed or transport-owned event-initializer source/request bindings
under the live contract.
Hub provisioning emits the completed-tenant event and does not directly invoke a
Customization HTTP writer. The planned Studio editors below own human-facing
permission keys and admission.
Every command takes its tenant from context rather than request input. RLS and
Write_With_Foreign_TenantId_Is_Rejected_By_WithCheck independently protect the
database write. Finite source admission registers no assignable permission key.
The Phase 03 Status block owns current
source-bound qualification.
Every key in the {module}.{resource}.{action} form. The resources above are
written PascalCase as the template writes them; the keys they generate are
lowercase snake_case — customization.content_type.read,
customization.content_type_publication.write,
customization.level_taxonomy.delete. editor is
Permission Standards § Built-in Roles's
Editor, named there rather than invented here.
The Admin Studio editors are what make these keys load-bearing. The
TenantContentType editor lands in
Phase 04, the TenantLevelTaxonomy
editor in Phase 05, and
Phase 06 consolidates them —
each one is the first caller with a human behind it, and therefore the first that
needs a key rather than an absence of endpoints.
Scope is Tenant, not Organization. Both aggregates are tenant-owned
tenant-wide: a content type is what the whole tenant's content looks like, and a
branch publishing its own shape would make one organization's rows unreadable to
another's renderer. There is no organization_id on either table and no
restrictive write guard, which is
Database Standards § Table classes's answer for
this class rather than an omission.