Appearance
Organization Service API (school-service alias)
Current Endpoints
GET /healthzGET /readyzGET /v1GET /v1/admin/organizations/roster-summaryPUT /v1/organizations/{organizationId}/snapshotGET /v1/organizationsPOST /v1/organizationsGET /v1/organizations/{organizationId}PATCH /v1/organizations/{organizationId}DELETE /v1/organizations/{organizationId}DELETE /v1/organizations/{organizationId}/purgeGET /v1/organizations/{organizationId}/academic-yearsPOST /v1/organizations/{organizationId}/academic-yearsPATCH /v1/organizations/{organizationId}/academic-years/{academicYearId}GET /v1/organizations/{organizationId}/unitsPOST /v1/organizations/{organizationId}/unitsPATCH /v1/organizations/{organizationId}/units/{unitId}DELETE /v1/organizations/{organizationId}/units/{unitId}GET /v1/organizations/{organizationId}/membersPOST /v1/organizations/{organizationId}/membersPUT /v1/organizations/{organizationId}/members/{userId}PATCH /v1/organizations/{organizationId}/members/{userId}DELETE /v1/organizations/{organizationId}/members/{userId}GET /v1/users/{userId}/membershipsPOST /v1/tenant/resolvePOST /v1/tenant/validate
Native Organization Foundation
Phase 8 school-service owns organization and unit metadata plus tenant validation backed by an IAM-synchronized membership read model. IAM owns permission-bearing organization membership writes. This is an internal /v1 foundation, not a public /api/organizations* cutover.
Legacy evidence:
node-platform/apps/api/src/modules/organizations/organizations.controller.ts:14-204maps legacy organization, unit, member, archive, purge, and student-import routes.node-platform/apps/api/src/modules/app-data/app-data.organizations.ts:12-461implements default organization creation, list pagination, manager checks, organization CRUD, unit dependency checks, member upsert/update/remove, and last active OWNER protection.node-platform/apps/api/src/modules/tenant/tenant.service.ts:1-101wraps organization resolve, membership, manager, and CRUD calls for guards/controllers.node-platform/apps/api/src/common/tenant.guard.ts:20-45readsX-Organization-Idor queryorganizationId; admins can select any org, non-admin users must be active members.node-platform/apps/api/src/common/current-organization.decorator.ts:8-14exposes the resolved organization id to route handlers.node-platform/apps/api/prisma/schema.prisma:124-232definesOrganization,OrganizationUnit, andOrganizationMemberwith unique slug, unique unit code by organization, and unique member by organization/user.node-platform/packages/shared/src/index.ts:39-52defines organization type/status/member role/member status enums.node-platform/packages/shared/src/index.ts:1132-1152defines organization, unit, and member validation schemas.node-platform/apps/web/app/admin/organizations/page.tsx:268-495shows the admin page consuming/organizations,/organizations/:id,/units, and/membersroutes.node-platform/apps/web/lib/client-api.ts:52-62andnode-platform/apps/web/components/layout/organization-switcher.tsx:20-64show frontend persistence ofhoctapaz.organizationIdand membership-driven switching.
Native contract:
GET /v1/admin/organizations/roster-summaryreturnssourceService=school-service, organization counts by status, unit counts, and IAM-synchronized membership read-model counts by status and role for admin dashboard/source-map adapters. It is read-only and does not query IAM, profile-service, or another service database; IAM remains authoritative for permission-bearing organization membership.PUT /v1/organizations/{organizationId}/snapshotupserts a service-owned row from legacy backfill or bootstrap data, including status and timestamps when supplied.GET /v1/organizationssupportsq,status,type,page,limit,userId, anduserRole. Admin-style reads return paged data; non-admin user reads are membership-scoped.POST /v1/organizationscreates organization metadata.X-User-Idonly creates anOWNERread-model membership when the request also carries the internalX-IAM-Membership-Syncmarker.GET/PATCH/DELETE /v1/organizations/{organizationId}preserve detail, partial update, and archive semantics.DELETE /v1/organizations/{organizationId}/purgehard deletes service-owned org data for admin-only adapters; public gateway cutover must keep role enforcement outside this endpoint.- Academic years belong only to the organization database.
POSTcreates aDRAFTwith ISOstartsOn/endsOndates;PATCHpermitsDRAFT -> ACTIVE|ARCHIVEDandACTIVE -> ARCHIVED. There can be one active year per organization. An active year cannot have its name/dates changed and archived years are immutable. This policy never reads IAM, classroom, course, or exam data. - Unit routes preserve organization-scoped uniqueness for
code, defaulttype=BRANCH, and block delete while IAM-synced members are assigned to the unit. GET /v1/organizations/{organizationId}/memberslists the IAM-synced read model.POST|PUT|PATCH|DELETE /v1/organizations/{organizationId}/members*are internal IAM read-model sync routes. They requireX-IAM-Membership-Sync(true,1,iam-service, orread-model) plusX-Internal-Service: iam-serviceand the sharedX-Internal-Token; ordinary callers return409 SCHOOL_MEMBERSHIP_NOT_AUTHORITATIVEand spoofed marker requests return401 SCHOOL_INTERNAL_UNAUTHORIZED. IAM outbox delivery should also sendX-IAM-Membership-Event-IdandX-IAM-Membership-Occurred-At; school-service records that state so newer deletes tombstone a member and stale older upserts cannot recreate it. IAM-sync deletes are replay-safe for an already-absent member in an existing organization and can remove the last localOWNERbecause IAM is canonical for permission-bearing membership.GET /v1/users/{userId}/membershipsreturns the IAM-synced membership summary needed by session hydration and tenant validation.POST /v1/tenant/resolvemirrors legacy default resolution: requested id wins, otherwise first active user membership, otherwiseorg_local_center.POST /v1/tenant/validatemirrors TenantGuard: admins pass, non-admins require active membership, and manager-only checks requireOWNERorORG_ADMIN.
Envelope:
json
{
"success": true,
"data": {
"id": "org_local_center",
"name": "HOCTAPAZ Local Center",
"slug": "hoctapaz-local",
"type": "CENTER",
"status": "ACTIVE",
"_count": {
"members": 1,
"units": 0,
"classrooms": 0
}
},
"message": "OK"
}Database:
services/school-service/migrations/000002_organizations.sqlcreatesorganizations,organization_units, andorganization_members.services/school-service/migrations/000003_iam_membership_sync_state.sqlstores the last IAM membership sync event and tombstone per(organizationId,userId)so the read model can reject stale replays.services/school-service/migrations/000004_academic_years.sqlcreates tenant-scopedacademic_years, validatesends_on > starts_on, enforces a case-insensitive organization/name key, and uses a partial unique index for exactly one active year per organization.organization_members.user_idstores the public IAM user id; school-service does not query IAM or profile-service databases.organization_units.codeis nullable; native PostgreSQL uniqueness uses a partial unique index so multiple null codes are allowed per organization, matching expected optional-code behavior.- Classroom counts are returned as
0until classroom-service owns a native read model or school-service receives classroom membership events.
Validation queries:
sql
SELECT id, legacy_id, name, slug, type, status
FROM organizations
ORDER BY created_at DESC
LIMIT 20;
SELECT organization_id, id, name, code, type
FROM organization_units
ORDER BY created_at DESC
LIMIT 20;
SELECT organization_id, user_id, role, status, unit_id, joined_at
FROM organization_members
ORDER BY joined_at DESC
LIMIT 20;
SELECT organization_id, id, name, starts_on, ends_on, status
FROM academic_years
ORDER BY organization_id, starts_on DESC;Rollback for this native slice:
- Keep
/api/organizations*routed to legacy. - Keep public session membership hydration on legacy or IAM-owned membership until gateway/IAM adapters consume
/v1/users/{userId}/memberships. - Disable gateway callers for
/v1/tenant/*and/v1/organizations*. - Drop school-service local organization tables with the migration down step if local test data must be reset.
Gateway read rehearsal:
deploy/gateway/routes.organizations-read-native-example.json and deploy/gateway/routes.organizations-read-native-localhost-example.json provide non-default route-table rehearsals for read-only organization paths. They route only:
GET /api/organizationsGET /api/organizations/{organizationId}GET /api/organizations/{organizationId}/unitsGET /api/organizations/{organizationId}/members
to school-service with gateway identity and organization header injection. Organization writes, purge, unit/member mutations, and /api/organizations/{organizationId}/student-imports stay legacy-proxied because public RBAC/tenant enforcement, IAM membership sync, and user-profile hydration need separate adapters.
Run make test-organization-routes before any live or browser rehearsal.
The bounded IAM membership and school read-model proof is recorded in docs/qa/organization-iam-membership-school-sync-proof.md. It confirms IAM write authority, school-service sync markers/order protection, and rollback notes without promoting default member-write routes.
Academic-year candidate routes are defined in deploy/gateway/routes.academic-years-native-example.json and its localhost variant. They expose only list/create/patch beneath one organization id with require_auth, require_organization, and required_roles=[ADMIN]. make test-academic-year-route-runtime starts only temporary memory-backed owner/Gateway processes, verifies anonymous 401, teacher 403, Admin create/activate/list headers, then restarts that temporary Gateway with deploy/gateway/routes.json and observes broad organizations / legacy_proxy rollback. make test-academic-year-postgres applies all school-service migration Up sections to a fresh initdb cluster and proves the active year survives a service restart. make test-academic-year-browser adds an opt-in Admin Vite browser list/draft/activate candidate against a separate fresh PostgreSQL cluster, then confirms the default route remains legacy. The current artifacts are output/qa/academic-year-route-runtime-20260715-042017/ and output/qa/academic-year-postgres-isolated-20260715-042021/, plus output/qa/academic-year-browser-local-runtime-20260715-043216/.
Non-goals for P8-003:
- Public gateway adapter/cutover for
/api/organizations*. - Student import job ownership.
- Classroom/course membership enforcement beyond organization membership.
- Grade taxonomy, curriculum, or branch-specific classroom counts.
- Legacy academic-year parity, default-route promotion, deployed target evidence, or production data migration for academic years. The Admin UI proof is an opt-in local candidate, not parity or default-route evidence.
- User profile hydration inside member rows; this remains profile-service or gateway adapter work.