Skoolsewa - School Management System Docs
Developer ResourcesSystem Features

System Features API Reference

Complete API contracts for system-features, including routes, auth, DTOs, responses, errors, examples, and integration notes.

System Features - API Reference

Audience: Admin panel engineers, mobile/portal engineers, QA, and any API consumer that needs to shape navigation around installation-level switches. Scope: One route — GET /api/system/features — that tells any signed-in caller which installation-level features are switched on for this school, and reveals accounting-integration setup detail only to the roles allowed to see it.

1. Documentation Evidence

AreaFiles InspectedWhat Was Verified
Controllerapps/api/src/modules/system-features/system-features.controller.tsRoute, guards, decorators, Swagger metadata.
DTOapps/api/src/modules/system-features/dto/system-features.dto.tsResponse shape, optional fields, descriptions.
Serviceapps/api/src/modules/system-features/system-features.service.tsBehavior, permission check, DB reads, response assembly.
Moduleapps/api/src/modules/system-features/system-features.module.tsImports, controller/provider wiring.
Feature flagapps/api/src/common/feature-flags/accounting-integration.tsACCOUNTING_INTEGRATION_ENABLED semantics, isAccountingIntegrationEnabled().
Schemapackages/db/src/schema (integrationClients, integrationClientKeys, imported via @skoolsewa/db)Columns read by clientConnected()/hasClients(): status, expiresAt, join on clientId.
Guard allowlistapps/api/src/common/authorization/role.guard.ts:745-754 (NO_PERMISSION_ADMIN_HANDLERS)Why the route has no @Permissions() yet still passes RoleGuard.
Structure mirrorapps/api/test/structure/route-permissions.spec.ts:127-131The CI mirror of the same allowlist entry.
Route ground truthapps/api/test/structure/structure.baseline.json (controllerPath: "system/features", GET /api/system/features)The route exists exactly once, under the admin document, not under /api/mobile.
Swagger registrationapps/api/src/config/swagger/swagger-documents.ts:70,1344SystemFeaturesModule is a leaf in ADMIN_MODULES, so the route is documented in /api/api-docs.
Error registryapps/api/src/common/types/error-codes.ts:1966ACCOUNTING_INTEGRATION_DISABLED exists in the registry but is never thrown by this route — see Error Cases.
Permission catalogpackages/db/src/authorization/permission-catalog.ts:612,660,682FeeSync, SalarySync, AssetSync exist as permission modules (their _READ codes gate the setup fields, alongside FeeMappings_READ and FeeIntegrations_READ).
TestsNone — no *.spec.ts exists under apps/api/src/modules/system-features/.

2. Module Summary

FieldValue
Module namesystem-features
Module slugsystem-features
Primary actorsAny authenticated caller (guest/unauthenticated is refused by the global JwtAuthGuard); the response shape then further depends on whether the caller's active role holds an accounting permission
API surfacesadmin document (one route; not mobile-prefixed)
Base route prefix/api/system/features
Auth modelJwtAuthGuard + RoleGuard, no @Permissions() — allowlisted in NO_PERMISSION_ADMIN_HANDLERS
PersistencePostgreSQL only, read-only (integration_clients, integration_client_keys)
Runtime source of truthACCOUNTING_INTEGRATION_ENABLED env var for enabled; IntegrationKeyringService.configured for configured; integration_clients/integration_client_keys rows for clientConnected/hasClients
Sibling docsBackend, Features and flows

3. Concepts and Terminology

TermMeaningSource FileUsed By
ACCOUNTING_INTEGRATION_ENABLEDThe installation-level switch for whether this school uses the separate accounting application. A product decision, not a secret.common/feature-flags/accounting-integration.tsenabled
ConfiguredWhether the server holds the integration master keys (INTEGRATION_SECRET_KEYS) — a technical state distinct from the switch.SystemFeaturesService.get, IntegrationKeyringServiceconfigured
Client connectedAn active integration client exists holding at least one active, unexpired key. Always false while enabled is false.SystemFeaturesService.clientConnectedclientConnected
Has clientsAny integration client that is not revoked exists. Computed even while enabled is false, so an existing connection stays findable for revocation.SystemFeaturesService.hasClientshasClients
Accounting permissionsThe five permission codes whose holders may see the setup detail: FeeSync_READ, FeeMappings_READ, FeeIntegrations_READ, SalarySync_READ, AssetSync_READ.ACCOUNTING_PERMISSIONS in system-features.service.tsaccountingAccess() (setup)

4. API Surface Map

SurfaceMethodPathActorAuth/GuardPermissionControllerPurpose
Admin (installation-level, all audiences)GET/api/system/featuresAny signed-in userJwtAuthGuard, RoleGuardNone — allowlisted, response shape gated internallySystemFeaturesControllerReport which installation features are switched on, for the panel to shape its navigation.

This is the only route the module declares. SystemFeaturesModule is a leaf (its own entry in ADMIN_MODULES), not composed under any aggregate.

5. Auth, Identity, and Permissions

SurfaceGuard/DecoratorIdentity ShapePermissionGuest AllowedNotes
GET /api/system/featuresJwtAuthGuard, RoleGuard, no @Permissions()@CurrentUser() actor: AuthUser | undefinedNone (allowlisted)No — global JwtAuthGuard requires a valid sessionA session that has not yet chosen an active role still reaches the handler and gets enabled only; there is no active-role-required refusal on this route.

RoleGuard returns true for this handler before it reads request.user, because SystemFeaturesController.get is a key in NO_PERMISSION_ADMIN_HANDLERS (role.guard.ts:745). Every audience's navigation depends on this route, and the answer is booleans only — no row data, no ids — which is why it is deliberately left unpermissioned rather than gated behind any one module's read code.

The controller uses @CurrentUser, not @CurrentAdmin, specifically so a caller with no active role yet (mid-onboarding, or a multi-role account that has not selected one) still gets an answer instead of a hard failure; they just get no accounting setup detail, because accountingAccess() returns { setup: false, writeBackHint: false } when actor?.activeRole is absent.

6. DTO and Model Reference

6.1 SystemFeaturesDto (response)

FieldTypeRequiredDefaultValidationExampleSource
accountingIntegrationAccountingIntegrationFeatureDtoYesN/A@ApiProperty({ type: AccountingIntegrationFeatureDto })see belowdto/system-features.dto.ts:29-32

6.2 AccountingIntegrationFeatureDto (nested)

FieldTypeRequiredDefaultValidationExampleSource
enabledbooleanYesN/AAlways present, for every callertruedto/system-features.dto.ts:4-8
configuredbooleanNoomitted@ApiPropertyOptional(); present only when the caller holds an accounting permissiontruedto/system-features.dto.ts:10-14
clientConnectedbooleanNoomittedSame visibility rule; always false while enabled is falsefalsedto/system-features.dto.ts:16-20
hasClientsbooleanNoomittedSame visibility rule; computed regardless of enabledtruedto/system-features.dto.ts:22-26
paymentWriteBackbooleanNoomittedPresent for a role holding one of the accounting permissions or FeePayments_CREATE, and for a superadmin. false while enabled is false or while INTEGRATION_SECRET_KEYS is unset. Otherwise true when an active integration client holding an active, unexpired key has the payments:write scope — only the client named by ACCOUNTING_INTEGRATION_CLIENT_ID when that variable is settruedto/system-features.dto.ts

paymentWriteBack answers one question: do payments recorded in the accounting application reach this school's fee records? It reads the payments:write scope itself, not merely the existence of a client, because an active client can hold read-only scopes. The office staff who record payments (FeePayments_CREATE) receive { enabled, paymentWriteBack } and none of the setup fields.

configured, clientConnected and hasClients are omitted from the response body, not sent as null or false — the service only assigns them onto accountingIntegration inside the accountingAccess().setup branch of SystemFeaturesService.get, and ResponseDto serializes whatever object it is given.

Response envelope: ResponseDto<SystemFeaturesDto> — message, data, with count/currentPage/totalPage/nextCursor/errorCode all present in the Swagger schema but unused by this route (common/dto/response-dto.ts).

7. Enum Reference

None. This module declares no enums; every field is a plain boolean.

8. Endpoint Reference

8.1 GET /api/system/features

Purpose

Call this once per session (or whenever navigation is rebuilt) to learn which installation-level features this school has switched on, so the panel can show or hide the accounting-integration screens and, for the roles allowed to configure it, its setup state. Safe to call before an active role is chosen — it degrades gracefully to enabled only.

Source Evidence

EvidencePath
Controllerapps/api/src/modules/system-features/system-features.controller.ts
DTOapps/api/src/modules/system-features/dto/system-features.dto.ts
Serviceapps/api/src/modules/system-features/system-features.service.ts
Schemapackages/db — integrationClients, integrationClientKeys
TestsN/A — no spec file exists for this module

Auth and Permissions

  • Auth: JWT required (global JwtAuthGuard).
  • Guard chain: JwtAuthGuard, RoleGuard.
  • Permission: none — allowlisted in NO_PERMISSION_ADMIN_HANDLERS.
  • Guest support: none; an unauthenticated request is refused before the handler runs.
  • Rate limit: none declared on this controller.
  • Idempotency: N/A — pure read, Cache-Control: no-store on every response.

Request

PartRequiredDetails
HeadersYesAuthorization: Bearer <token>
ParamsNoNone
QueryNoNone
BodyNoNone
GET /api/system/features HTTP/1.1
Authorization: Bearer TOKEN

Response

200 OK, always Cache-Control: no-store (@Header("Cache-Control", "no-store") on the handler — the answer must never be served stale from an intermediary, since it gates navigation and setup screens).

Caller without an accounting permission (or with no active role yet):

{
  "message": "Features fetched.",
  "data": {
    "accountingIntegration": {
      "enabled": true
    }
  }
}

Caller whose active role holds one of the five accounting permissions:

{
  "message": "Features fetched.",
  "data": {
    "accountingIntegration": {
      "enabled": true,
      "configured": true,
      "clientConnected": false,
      "hasClients": true
    }
  }
}

Installation with the integration switched off entirely, for a caller who holds an accounting permission:

{
  "message": "Features fetched.",
  "data": {
    "accountingIntegration": {
      "enabled": false,
      "configured": false,
      "clientConnected": false,
      "hasClients": true
    }
  }
}

clientConnected is forced to false whenever enabled is false (SystemFeaturesService.get) — it is never computed against the database in that state. hasClients is still computed, because a client from before the switch was turned off must stay visible for revocation.

Side Effects

None. This route performs no writes, no cache writes, no queue enqueues, no realtime events, and no audit log entry — it is a plain GET with a read-only service, and it is not a mutating request, so ActivityAuditInterceptor does not record it.

Error Cases

HTTP StatusError CodeConditionUser-Facing MeaningSource
401N/A (global)No valid sessionLog in again.JwtAuthGuard (global)

No route-specific error code exists for this endpoint. ACCOUNTING_INTEGRATION_DISABLED is registered in error-codes.ts and thrown elsewhere in the codebase when a caller tries to use the accounting integration while it is switched off — this route never throws it, because reporting enabled: false is itself the successful response, not a refusal.

Edge Cases

  • No active role selected yet: actor?.activeRole is undefined, accountingAccess() short-circuits to { setup: false, writeBackHint: false }, response carries enabled only.
  • Superadmin caller: accountingAccess() returns { setup: true, writeBackHint: true } immediately for role.isSuperadmin, without a permission lookup.
  • Non-superadmin caller holding none of the five accounting permissions: response carries enabled only, same shape as "no active role".
  • Integration switched off (enabled: false): clientConnected is hard-coded false without a database read; hasClients still queries the database.
  • No integration client rows exist at all: both clientConnected and hasClients resolve to false (empty result from each query).
  • An integration client exists but every key on it is expired or inactive: clientConnected is false (the join requires status: "active" on the key and expiresAt > now()); hasClients can still be true if the client itself is not revoked.
  • Concurrent requests: no shared mutable state; every call independently re-reads config and the database, so there is no staleness beyond normal read-committed behavior.

Example Requests

GET /api/system/features HTTP/1.1
Authorization: Bearer TOKEN
curl -X GET "$API_URL/api/system/features" \
  -H "Authorization: Bearer TOKEN"

9. Flow Diagrams

9.1 Route Ownership

9.2 Request Sequence

9.3 Error Branch

Not applicable. This is a single-object read with no list, no pagination, no sort or filter parameters.

11. Caching, Jobs, and External Integrations

IntegrationUsed?DetailsSource
Redis cacheNoResponse is marked Cache-Control: no-store; nothing is cached server-side or client-side.system-features.controller.ts:30
BullMQNo——
External APINo——

12. Consumer Integration Notes

ConsumerRequired KnowledgeFailure HandlingContract Stability
Admin panelCall this to decide whether to render accounting-integration navigation and, for permitted roles, its setup state; treat a missing configured/clientConnected/hasClients as "not visible to this role", not as false.A 401 means the session expired; re-authenticate and retry.Stable
Mobile/portal surfacesSame booleans apply to every audience; no audience-specific shape.Same.Stable
QAVerify the omission behavior directly — the setup fields must be absent from the JSON body for a role with none of the five accounting permissions, not null.—Stable

13. API Tradeoffs and Rationale

DecisionChosen BehaviorAlternatives ConsideredWhy This TradeoffRiskMitigation
No @Permissions() on the routeEvery signed-in caller gets enabled; setup detail gated inside the service instead of at the guardA dedicated SystemFeatures_READ permissionEvery audience's navigation needs enabled, including roles that will never hold an accounting permission; gating the whole route would 403 most callers for a booleanA future field added to the DTO might leak more than intended if the service-level gate is forgottenThe gate lives in one place (accountingAccess(), returning { setup, writeBackHint }) and is reused for every optional field
clientConnected forced false while disabledNo database read for that field when enabled is falseAlways compute itAvoids a meaningless read/answer when the integration cannot be connected anywayNone material—
hasClients computed regardless of enabledExisting clients remain discoverable for revocation even after the switch is turned offOmit it while disabled, matching clientConnectedAn operator must still be able to see and revoke a stale client after disabling the integrationNone material—

14. API Change Impact

ChangeAffected ConsumersBackend ImpactData ImpactMigration Needed?Compatibility Plan
Adding a new feature key alongside accountingIntegrationAdmin panel, portal navigationNew field on SystemFeaturesDtoNoneNoAdditive; existing consumers ignore unknown fields
Widening the accounting-permission list that unlocks setup detailAdmin panel (accounting screens)ACCOUNTING_PERMISSIONS in the serviceNoneNoAdditive

15. Zero-Omission API Checklist

  • The one controller route is documented.
  • The route prefix and runtime URL are documented.
  • Every DTO field, nested field, default, and visibility rule is documented.
  • Every response field and its omission behavior is documented.
  • Auth, guard chain, and the no-permission allowlist entry are documented.
  • Success and auth-failure branches are documented; no route-specific error code exists.
  • The module performs no DB writes, no cache, no queue, no realtime, no audit — stated explicitly.
  • The route has minimal and full-detail response examples for both visibility states.
  • Route, sequence, and error diagrams are included.
  • Tradeoffs are documented.
  • Links to backend and features/flows docs are included.

16. Integration Checklist

  • The route from the controller is documented.
  • Every DTO field is documented.
  • No enums exist in this module.
  • The response envelope is documented.
  • No module-specific error code exists; this is stated rather than invented.
  • Auth guard and the no-permission allowlist are documented.
  • No cache key, queue job, realtime event, or external call exists; this is stated rather than invented.
  • Diagrams match the current code.
  • Links to backend and features/flows docs are included.

See Also