Skip to content

Organization Service API (school-service alias)

Current Endpoints

  • GET /healthz
  • GET /readyz
  • GET /v1
  • GET /v1/admin/organizations/roster-summary
  • PUT /v1/organizations/{organizationId}/snapshot
  • GET /v1/organizations
  • POST /v1/organizations
  • GET /v1/organizations/{organizationId}
  • PATCH /v1/organizations/{organizationId}
  • DELETE /v1/organizations/{organizationId}
  • DELETE /v1/organizations/{organizationId}/purge
  • GET /v1/organizations/{organizationId}/academic-years
  • POST /v1/organizations/{organizationId}/academic-years
  • PATCH /v1/organizations/{organizationId}/academic-years/{academicYearId}
  • GET /v1/organizations/{organizationId}/units
  • POST /v1/organizations/{organizationId}/units
  • PATCH /v1/organizations/{organizationId}/units/{unitId}
  • DELETE /v1/organizations/{organizationId}/units/{unitId}
  • GET /v1/organizations/{organizationId}/members
  • POST /v1/organizations/{organizationId}/members
  • PUT /v1/organizations/{organizationId}/members/{userId}
  • PATCH /v1/organizations/{organizationId}/members/{userId}
  • DELETE /v1/organizations/{organizationId}/members/{userId}
  • GET /v1/users/{userId}/memberships
  • POST /v1/tenant/resolve
  • POST /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-204 maps legacy organization, unit, member, archive, purge, and student-import routes.
  • node-platform/apps/api/src/modules/app-data/app-data.organizations.ts:12-461 implements 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-101 wraps organization resolve, membership, manager, and CRUD calls for guards/controllers.
  • node-platform/apps/api/src/common/tenant.guard.ts:20-45 reads X-Organization-Id or query organizationId; admins can select any org, non-admin users must be active members.
  • node-platform/apps/api/src/common/current-organization.decorator.ts:8-14 exposes the resolved organization id to route handlers.
  • node-platform/apps/api/prisma/schema.prisma:124-232 defines Organization, OrganizationUnit, and OrganizationMember with unique slug, unique unit code by organization, and unique member by organization/user.
  • node-platform/packages/shared/src/index.ts:39-52 defines organization type/status/member role/member status enums.
  • node-platform/packages/shared/src/index.ts:1132-1152 defines organization, unit, and member validation schemas.
  • node-platform/apps/web/app/admin/organizations/page.tsx:268-495 shows the admin page consuming /organizations, /organizations/:id, /units, and /members routes.
  • node-platform/apps/web/lib/client-api.ts:52-62 and node-platform/apps/web/components/layout/organization-switcher.tsx:20-64 show frontend persistence of hoctapaz.organizationId and membership-driven switching.

Native contract:

  • GET /v1/admin/organizations/roster-summary returns sourceService=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}/snapshot upserts a service-owned row from legacy backfill or bootstrap data, including status and timestamps when supplied.
  • GET /v1/organizations supports q, status, type, page, limit, userId, and userRole. Admin-style reads return paged data; non-admin user reads are membership-scoped.
  • POST /v1/organizations creates organization metadata. X-User-Id only creates an OWNER read-model membership when the request also carries the internal X-IAM-Membership-Sync marker.
  • GET/PATCH/DELETE /v1/organizations/{organizationId} preserve detail, partial update, and archive semantics.
  • DELETE /v1/organizations/{organizationId}/purge hard 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. POST creates a DRAFT with ISO startsOn/endsOn dates; PATCH permits DRAFT -> ACTIVE|ARCHIVED and ACTIVE -> 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, default type=BRANCH, and block delete while IAM-synced members are assigned to the unit.
  • GET /v1/organizations/{organizationId}/members lists the IAM-synced read model.
  • POST|PUT|PATCH|DELETE /v1/organizations/{organizationId}/members* are internal IAM read-model sync routes. They require X-IAM-Membership-Sync (true, 1, iam-service, or read-model) plus X-Internal-Service: iam-service and the shared X-Internal-Token; ordinary callers return 409 SCHOOL_MEMBERSHIP_NOT_AUTHORITATIVE and spoofed marker requests return 401 SCHOOL_INTERNAL_UNAUTHORIZED. IAM outbox delivery should also send X-IAM-Membership-Event-Id and X-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 local OWNER because IAM is canonical for permission-bearing membership.
  • GET /v1/users/{userId}/memberships returns the IAM-synced membership summary needed by session hydration and tenant validation.
  • POST /v1/tenant/resolve mirrors legacy default resolution: requested id wins, otherwise first active user membership, otherwise org_local_center.
  • POST /v1/tenant/validate mirrors TenantGuard: admins pass, non-admins require active membership, and manager-only checks require OWNER or ORG_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.sql creates organizations, organization_units, and organization_members.
  • services/school-service/migrations/000003_iam_membership_sync_state.sql stores 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.sql creates tenant-scoped academic_years, validates ends_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_id stores the public IAM user id; school-service does not query IAM or profile-service databases.
  • organization_units.code is 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 0 until 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/organizations
  • GET /api/organizations/{organizationId}
  • GET /api/organizations/{organizationId}/units
  • GET /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.

Go-platform documentation is generated from repository Markdown.