Analysis of the ChurchTools OpenAPI spec (openapi.json, OpenAPI 3.1.0, 487 paths).
- The spec's own
info.versionis0.1.0— that is the API-doc version, not the CT release. - The real CT version is exposed by the
GET /infoendpoint, whose response schema documents:version= "ChurchTools Version", example3.123.0build= "Database Build Version", example31843
3.123.0>>3.96, so the Notion design's requirement that group-hierarchy / metadata CRUD needs CT v3.96+ is comfortably met on this installation. (The/infoendpoint is also live, so the CLI can assert the minimum version at runtime.)- Note: the ChurchTools API doc is self-trimming —
info.descriptionstates it "will always show only those endpoints you can use with your ChurchTools installation." So the presence of the write endpoints below is itself evidence they exist on this version.
Methods marked only if they actually exist on the matched path. "Update" = PUT or PATCH (noted). Collection paths (list/create) vs item paths (/{id}) are separated.
| # | Resource | Matched path(s) | GET (list / by-id) | POST (create) | PUT/PATCH (update) | DELETE | Verdict |
|---|---|---|---|---|---|---|---|
| 1 | ct_campus |
/campuses, /campuses/{id} |
list ✅ / by-id ✅ | ✅ | ✅ PUT | ✅ | Full CRUD |
| 2 | ct_group_type |
/group/grouptypes, /group/grouptypes/{groupTypeId} |
list ✅ / by-id ✅ | ✅ | ✅ PUT | ✅ | Full CRUD |
| 3 | ct_group |
/groups, /groups/{groupId} |
list ✅ / by-id ✅ | ✅ | ✅ PATCH | ✅ | Full CRUD |
| 4 | ct_group_hierarchy |
/groups/hierarchies (GET), /groups/{groupId}/children (GET), /groups/{groupId}/parents (GET), /groups/{groupId}/parents/{parentGroupId} (PUT/DELETE) |
list ✅ (hierarchies/children/parents) | — (no collection POST) | ✅ PUT links a parent | ✅ unlinks a parent | Writable — parent/child edges created & removed via PUT/DELETE on the item path (no POST needed) |
| 5 | ct_group_role |
/group/roles, /group/roles/{roleId} |
list ✅ / by-id ✅ | ✅ | ✅ PUT | ✅ | Full CRUD (master-data roles). Per-group assignment lives separately at /groups/{groupId}/roles GET + /groups/{groupId}/roles/{roleId} PATCH. |
| 6 | ct_dynamic_group |
/dynamicgroups (GET), /dynamicgroups/{groupId}/ruleset (GET/PUT/DELETE), /dynamicgroups/{groupId}/status (GET/PUT), /dynamicgroups/refresh & /dynamicgroups/{groupId}/refresh (POST) |
list ✅ / ruleset & status by-id ✅ | ✅ PUT ruleset & status | ✅ ruleset DELETE | Partial — ruleset is fully updatable/deletable; the group entity itself is created via /groups (POST) then given a ruleset. No dedicated create/delete of the dynamic-group record. |
|
| 7 | ct_permission |
/permissions/global (GET), /permissions/{domainType} (GET), /permissions/{domainType}/{domainId} (GET/PUT/DELETE); /permissions/internal/... (GET) |
list ✅ / by-id ✅ | — (no collection POST) | ✅ PUT sets permission | ✅ | Writable — assign/revoke via PUT/DELETE on /{domainType}/{domainId} |
| 8 | ct_group_status |
none — see note | ❌ | ❌ | ❌ | ❌ | No REST endpoint at all → manual, permanently (#67). /group/memberstatus looks like a match but is a DIFFERENT dimension — member statuses ({id: "active", name: "Active"}, STRING ids, assignable per-membership), not group statuses (groupStatusId, numeric, e.g. 1 = active / 4 = archived). Live-verified 2026-07-10 on eqrm prod: /groups/statuses parses as /groups/{groupId} (400), /group/statuses and /groupstatuses both 404 — no read OR write endpoint for group statuses exists. (/statuses + /statuses/{id} DO offer full CRUD, but that is the person/community Status master data, tag Status, a third, unrelated dimension — do not conflate any of the three.) groupStatusId stays a plain numeric field on ct_group, authored directly — never resolved by name. |
| 9 | ct_age_group |
/group/agegroups, /group/agegroups/{ageGroupId} |
list ✅ / by-id ✅ | ✅ | ✅ PUT | ✅ | Full CRUD |
| 10 | ct_target_group |
/group/targetgroups, /group/targetgroups/{targetGroupId} |
list ✅ / by-id ✅ | ✅ | ✅ PUT | ✅ | Full CRUD |
| 11 | ct_meeting_point |
— none — | ❌ | ❌ | ❌ | ❌ | Not in API → fully manual. Zero matches for treffpunkt/meetingpoint/meeting point anywhere in the spec. Closest neighbours are meeting templates (/group/meetingtemplates, full CRUD) and group meetings (/groups/{groupId}/meetings, CRUD) — different concepts; confirm with product whether "meeting point" was meant to be one of those. |
| 12 | ct_relationship_type |
/person/relationshiptypes, /person/relationshiptypes/{id} |
list ✅ / by-id ✅ | ✅ | ✅ PUT | ✅ | Full CRUD |
| 13 | ct_person_status |
/statuses, /statuses/{id} |
list ✅ / by-id ✅ | ✅ | ✅ PUT | ✅ | Full CRUD, live-verified 2026-08-13 (eqrm prod, CT 3.135.2, read from the instance OpenAPI spec): /statuses → GET, POST; /statuses/{id} → GET, PUT, DELETE. POST requires name, shorty, isMember; PUT requires ALL of name, shorty, isMember, isSearchable, sortKey, securityLevelId — uniquely strict among managed types (every other managed PUT declares no required fields), and since PUT is a full replace the registry manages all six rather than a subset. The person/community Status master data (tag Status): "0 - First", "3 - Group Active", …, the domain a ct.status permission grant hangs off. Adoptable since #96, which is what makes a config using that domain self-sufficient across hosts. Do NOT conflate with ct_group_status (row 8, no endpoint at all) or /group/memberstatus (member statuses, string ids). Master data — never a person record; the people guard is unaffected. |
| 14 | ct_department |
/departments |
list ✅ / by-id ❌ | ❌ | ❌ | ❌ | No REST write path — a ref catalog here, not a managed resource. Live-probed 2026-08-13 (eqrm prod, CT 3.135.2): GET /departments returns [{id, name, nameTranslated, sortKey, shorty}]; no POST/PUT/DELETE on /departments exists in the spec, and there is no /departments/{id} path at all. Bereiche are the cdb_bereich permission scope dimension (churchdb:view alldata), so ct resolves them BY NAME for a scope: [{ department: "…" }] reference (#98) and surfaces them via ct get departments — but ct.department declares one and ct adopt department <id> adopts one. Managed since #108 — ct writes Bereiche through the legacy POST /index.php?q=churchdb/ajax func=saveMasterData interface the admin UI uses (create/update/delete all verified live on eqrm-dev 2026-08-14). It appears in no OpenAPI spec, which is why an OpenAPI-only audit could never have found it (#111). Note there is no /departments/{id} at all, so a single Bereich is read by filtering the collection. |
| 15 | ct_group_member_field | /groups/{groupId}/memberfields (GET), /groups/{groupId}/memberfields/group (POST), /groups/{groupId}/memberfields/group/{groupMemberFieldId} (PATCH/PUT/DELETE) | list ✅ (per group) / by-id ❌ | ✅ | ✅ PATCH (PUT fallback) | ✅ | Full CRUD, but GROUP-SCOPED — not a standalone resource (#135). A member field belongs to exactly one group and is not globally reusable, so it has no collection path of its own and no entry in the resource registry: it is a synthetic owned sub-resource of ct_group, declared inside ct.group({ memberFields: [...] }), exactly like hierarchy parents and the dynamic ruleset. Its portable identity is the managed group key plus a local field key (ojbp_2026_27_praktikum_1::wahl); a ChurchTools field id never reaches authored config or an adopted blueprint. Reads are narrowed to rows the /memberfields/group endpoints own — the same GET also returns fields sourced from person master data / group-type defaults, which are neither adoptable nor writable here. apply NEVER deletes one (a field dropped from config produces no desired diff key at all); removal is the explicit ct destroy --member-field <group>::<field>. Update uses PATCH (partial, so unmanaged siblings survive) and falls back to PUT on a 405/501. Definitions only — never a per-person value; assertNotPeople still guards every path. Distinct from the group CUSTOM fields of #48/#60 (/dbfields, fieldCategory.table == "cdb_gruppe"), which describe the group RECORD rather than what its members are asked. |
Create-time required fields (#73). The "POST ✅" marks above were spec-derived, not live-exercised for create. CT's POST validators require fields the tool does not manage for diffing:
group-typeneedsnamePlural/shorty/color/permissionDepth/isLeaderNecessary/availableForNewPerson(+sortKey/postsEnabled), andgroup-role+person-statusneedshorty. These are supplied as deterministic create-only defaults (AdoptableResource.createDefaults, derived from the declaredname) — merged into the POST body only, never recorded in state, so they stay unmanaged.campus(name+shorty, live-verified),group,age-group, andtarget-groupneed onlynamefrom their managed set and require no defaults.
Standard collection-POST + item-GET/PUT(-or-PATCH)/DELETE shape, safe to drive from the CLI:
ct_campusct_group_typect_group(update is PATCH, not PUT)ct_group_rolect_age_groupct_target_groupct_relationship_typect_person_status
No collection POST; state is set/removed through PUT/DELETE on the item path. Model these as "declare desired edge/assignment, reconcile via PUT/DELETE":
ct_group_hierarchy— manage parent links viaPUT/DELETE /groups/{groupId}/parents/{parentGroupId}ct_permission— assign/revoke viaPUT/DELETE /permissions/{domainType}/{domainId}ct_group_member_field— full CRUD verbs, but only ever under a group (#135), so it is a synthetic owned sub-resource rather than a registry type: declared asmemberFieldsonct.group(...), applied inline with that group (after it exists, before its ruleset), and deleted only by the explicitct destroy --member-field <group>::<field>
ct_dynamic_group— ruleset & status are updatable (PUT) and ruleset deletable (DELETE), but the group record is created through/groups. Treat as: create shell group viact_group, then manage its ruleset. No first-class create/delete of the dynamic-group entity.
ct_group_status— no REST endpoint at all, read or write (#67; corrected 2026-07-10 — a prior version of this table wrongly matchedGET /group/memberstatus, which is actually member statuses, a different dimension with string ids).groupStatusIdremains a plain numeric field. (Do not substitute/statuseseither — that's person-status master data, a third dimension.)ct_meeting_point— no endpoint at all; cannot be automated until CT ships one (or until "meeting point" is redefined onto meeting-templates/meetings, both of which are full CRUD).
The installation reports CT 3.123.0 via /info, so every write endpoint above is available and the v3.96+ hierarchy/metadata requirement is satisfied. Recommend the CLI call GET /info on startup and hard-fail below 3.96.0.
Not part of the Phase 0 structural matrix above; audited separately for the
field-definition schema surface. Caveat: unlike the Phase 0 matrix (audited
against a live openapi.json), these were verified against ChurchTools' public
API-client libraries (5pm-HDH churchtools-api @ CT 3.104, bensteUEM
ChurchToolsAPI @ CT 3.101) and CT Academy docs, because this repo's generated
src/api/schema.d.ts is git-ignored and was not available offline. Re-verify per
the runbook's re-audit procedure once the schema is regenerated.
| Resource | Matched path(s) | GET | POST/PUT/PATCH/DELETE | Verdict |
|---|---|---|---|---|
| Person master-data model | /person/masterdata |
✅ (single object) | ❌ | Read-only. Versionable master-data model incl. the securityLevels enumeration. No write endpoint (edited in the CT master-data admin UI). |
| Data-field definitions (Datenfelder) | /dbfields, /dbfields/{id} |
list ✅ / by-id ✅ | ❌ | Read-only. Unified person + group field definitions, discriminated by fieldCategory. Mutation only via legacy churchdb AJAX (db_insert/update/deletefields), not REST. |
| Security levels | /securitylevels, /securitylevels/{id} |
list ✅ / by-id ✅ | ✅ POST, PATCH, DELETE | Full CRUD — but not on the usual paths. Live-probed 2026-08-14 (eqrm-dev, CT 3.135.2): /securitylevels is GET-only, and create is POST /securitylevels/{id} (body {name}), update is PATCH /securitylevels/{id} (body {name, newid, forcereorder} — reordering is first-class), delete is DELETE /securitylevels/{id}. Managed since #110 — the registry gained a createPath hook and a callerAssignedId flag for it; ct.securityLevel({ key, id, name }) declares one, ct adopt security-level <id> adopts one. Changing a declared id is refused at plan time (that is a renumber, which rewrites what every numeric cc_securitylevel scope means). Resolvable by name as a scope ref since #110. The ids are NOT protocol constants — cc_securitylevel is an editable master-data table with an auto-increment id. |
| Comment viewers (Kommentare-Viewer) | /person/commentviewers, /person/commentviewers/{id} |
list ✅ / by-id ✅ | ✅ POST, PUT, DELETE | Full CRUD on conventional paths. Fully live-probed 2026-08-26 (eqrm-dev, CT 3.135.2 — one throwaway row created, read, updated and deleted): GET /person/commentviewers returns a flat [{id, name, nameTranslated, sortKey}] (id: 0 — "Alle" — is a real, built-in row), create is a plain POST /person/commentviewers with CT minting the id, and the item path takes GET/PUT/DELETE (an absent id gives a clean 404 error.notfound). nameTranslated is derived from name, not independently writable. Managed since #151 — ct.commentViewer({ key, name, sortKey }) declares one, ct adopt comment-viewer <id> adopts one; no registry machinery was needed (CT mints the id, and the writes are REST). cdb_comment_viewer is the scope dimension of churchdb:view comments, and it was the last one a config could not express portably: the ids differ across hosts of the same deployment, and before #151 a config had only the raw numeric dataId. The minted id is an auto-increment that does not reuse deleted rows, so it can never be caller-assigned. |
See docs/handbuch/field-definitions.md for the full writability
decision, evidence, and the schema/values boundary.