Skoolsewa - School Management System Docs
Developer ResourcesSystem Features

System Features Backend Documentation

Backend architecture, data model, services, and runtime rules for system-features.

System Features Backend Documentation

1. Documentation Evidence

AreaFiles InspectedVerified Details
Module wiringapps/api/src/modules/system-features/system-features.module.tsImports (RoleModule, IntegrationCoreModule), controller, provider.
Controllersapps/api/src/modules/system-features/system-features.controller.tsSingle handler, no @Permissions(), Cache-Control: no-store.
Servicesapps/api/src/modules/system-features/system-features.service.tsFeature-flag read, permission check, two read-only queries.
DTOsapps/api/src/modules/system-features/dto/system-features.dto.tsResponse contract and optional-field visibility.
Feature flagapps/api/src/common/feature-flags/accounting-integration.tsACCOUNTING_INTEGRATION_ENABLED, isAccountingIntegrationEnabled().
Schemapackages/db (integrationClients, integrationClientKeys, re-exported from @skoolsewa/db)Columns this service reads: status on both tables, expiresAt on keys, clientId join key.
Guard allowlistapps/api/src/common/authorization/role.guard.ts:745-754Why the route needs no permission and how the guard still runs.

2. Backend Scope and Boundaries

Owns

  • One read endpoint reporting installation-level feature switches to any signed-in caller.
  • The visibility rule that hides accounting-integration setup detail from callers who do not hold an accounting permission.

Does Not Own

  • The accounting integration's actual behavior (client management, keyring rotation, sync jobs) — that lives in apps/api/src/modules/integration/. This module only reports on it.
  • The ACCOUNTING_INTEGRATION_ENABLED switch's enforcement on other routes — common/feature-flags/accounting-integration.ts also exports RequiresAccountingIntegration(), a decorator used elsewhere to 404 accounting routes while disabled. system-features only reads the flag; it never enforces it.
  • Permission resolution itself — delegated to RoleService (modules/role/).

Source of Truth

ConcernSource of TruthNotes
Whether accounting integration is switched onACCOUNTING_INTEGRATION_ENABLED env var, read via ConfigServiceA deploy-time product decision, not a per-request value.
Whether the server holds integration master keysIntegrationKeyringService.configuredTechnical state, independent of the switch.
Whether a client is actively connectedintegration_clients joined to integration_client_keysAn "active" client requires an active, unexpired key row.
Caller's permissionsRoleService.getPermissionsForRoleId()Superadmin short-circuits without a lookup.

3. Module Composition

ModuleTypePathControllersProvidersExportsResponsibility
SystemFeaturesModuleLeafapps/api/src/modules/system-features/system-features.module.tsSystemFeaturesControllerSystemFeaturesServiceNoneOwns GET /api/system/features entirely; imports RoleModule (for RoleGuard's dependency on RoleService) and IntegrationCoreModule (for IntegrationKeyringService).

SystemFeaturesModule declares its own controller, so it is itself the entry registered in ADMIN_MODULES (config/swagger/swagger-documents.ts:1344) — there is no separate aggregate module to list instead.

4. File and Directory Map

apps/api/src/modules/system-features/
  system-features.module.ts
  system-features.controller.ts
  system-features.service.ts
  dto/
    system-features.dto.ts
FilePurposeKey ExportsNotes
system-features.module.tsWires the controller and service, imports the two modules the service depends on.SystemFeaturesModuleNo providers beyond the service; no exports.
system-features.controller.tsThin controller: one GET, delegates to the service, wraps the result in ResponseDto.SystemFeaturesController@CurrentUser, not @CurrentAdmin — accepts a session with no active role.
system-features.service.tsAll business logic: flag read, permission gate, two read-only queries.SystemFeaturesServiceNo writes anywhere in this module.
dto/system-features.dto.tsResponse contract only — there is no request DTO, since the route takes no input.SystemFeaturesDto, AccountingIntegrationFeatureDtoOptional fields use @ApiPropertyOptional, matching the service's conditional assignment.

5. Data Model

This module owns no schema of its own. It reads two tables owned by the integration module.

5.1 Schema Source

packages/db/src/schema/  (owned by the integration module, read here)
  integrationClients
  integrationClientKeys

5.2 Tables Read

integration_clients (read-only from this module)

Column/FieldRead HereUsed For
idYes (implicit, via join)Join target for integrationClientKeys.clientId.
statusYesclientConnected() filters status = "active"; hasClients() filters status != "revoked".

integration_client_keys (read-only from this module)

Column/FieldRead HereUsed For
clientIdYesJoin key back to integration_clients.id.
statusYesclientConnected() filters status = "active".
expiresAtYesclientConnected() filters expiresAt > now().

This module never writes to either table. Full column definitions, constraints, and lifecycle belong to the integration module's own documentation.

5.3 Relationship Diagram

6. Services and Responsibilities

6.1 SystemFeaturesService

MethodCalled ByReadsWritesSide EffectsErrors
get(actor)SystemFeaturesController.getACCOUNTING_INTEGRATION_ENABLED config, IntegrationKeyringService.configured, RoleService permissions, integration_clients/integration_client_keys (conditionally)NoneNoneNone thrown by this method
clientConnected() (private)get(), only when accountingAccess().setup is trueintegration_clients, integration_client_keysNoneNoneNone
hasClients() (private)get(), only when the caller qualifies for the setup fieldsintegration_clientsNoneNoneNone
accountingAccess() (private)get()RoleService.getPermissionsForRoleId (at most one call per request)NoneNoneReturns { setup, writeBackHint }: a superadmin gets both; otherwise setup is true for a holder of an accounting permission and writeBackHint is true for setup or a holder of FeePayments_CREATE; no active role gets neither
paymentWriteBack() (private)get(), when setup or writeBackHint is true and the switch is on and the keyring is configured (otherwise the field is false without a query)integration_clients, integration_client_keysNoneNoneActive client, active unexpired key, scopes containing payments:write, and — when ACCOUNTING_INTEGRATION_CLIENT_ID is set — that client's public_id

Explain:

  • Input normalization: none — the route takes no request body, query, or params.
  • Validation order: not applicable; the only branching is the permission check.
  • Transaction boundaries: none — every query is a standalone SELECT, and none depends on another within the same request beyond ordering (configured/clientConnected/hasClients are only computed after the permission check passes).
  • Idempotency: trivially idempotent — pure reads, no state changes.
  • Retry behavior: none implemented or needed; a failed DB read propagates as a normal 500 through the global exception filter.
  • Response mapping: the service returns SystemFeaturesDto directly; the controller does not remap it, only wraps it in ResponseDto.
  • Fail-open or fail-closed: fail-closed on the setup fields — accountingAccess() defaults to { setup: false, writeBackHint: false } for any caller without an active role or a matching permission, so the safe default is "do not reveal setup detail."
  • Logger usage: none in this service.

7. Runtime Flows

7.1 GET /api/system/features

StepCode PathBehaviorFailure Case
1SystemFeaturesController.getReceives request, forwards actor (possibly with no activeRole).N/A — no validation to fail.
2SystemFeaturesService.getReads the env flag, builds the base { enabled } object.Config unreadable would surface as a NestJS bootstrap failure, not a runtime error here.
3SystemFeaturesService.accountingAccessReturns { setup, writeBackHint }: both true for superadmin without a lookup; otherwise setup is true for a holder of one of the five accounting permission codes (checked against RoleService.getPermissionsForRoleId()), and writeBackHint is true for setup or a holder of FeePayments_CREATE.No activeRole returns false immediately.
4SystemFeaturesService.clientConnected / hasClientsOnly run when step 3 is true; two independent SELECT ... LIMIT 1 queries.A DB error propagates as an unhandled rejection to the global exception filter → 500.

Include:

  • Success path: as above, in both branches (with and without accounting-permission detail).
  • Validation failures: none possible — no input to validate.
  • Permission failures: none surfaced as an error; the "failure" to see setup detail is silent field omission, not a 403.
  • Missing entity: not applicable — there is no path-addressed entity.
  • Duplicate request: not applicable — pure read, safe to call repeatedly.
  • Cache miss/hit: not applicable — the route is explicitly no-store.
  • Async enqueue: none — no queue involvement.
  • Race/concurrency: none — each request is independent; no shared mutable state.
  • Guest vs logged-in branch: an unauthenticated caller never reaches the handler (JwtAuthGuard refuses first).

8. Caching

None. The handler sets Cache-Control: no-store explicitly, and the service performs no Redis reads or writes. Every call re-evaluates the feature flag and, when applicable, re-queries the database.

9. BullMQ, Schedulers, and Async Work

None. This module owns no queue, no processor, and no scheduled job.

10. Realtime and Events

None. This module emits no realtime events and is not a consumer of any.

11. Security, Auth, and Abuse Controls

  • Guards: JwtAuthGuard (global) requires a valid session; RoleGuard runs but short-circuits to true for this handler via the NO_PERMISSION_ADMIN_HANDLERS allowlist (role.guard.ts:745-754), before it would otherwise read request.user or refuse a missing active role.
  • Permissions: none required to call the route at all. A separate, service-level check (accountingAccess, returning { setup, writeBackHint }) gates only the four optional response fields (configured, clientConnected, hasClients on setup; paymentWriteBack on setup or writeBackHint), never the route itself.
  • Guest identity: not applicable — no guest path exists; the global guard refuses unauthenticated callers.
  • Admin identity: @CurrentUser, deliberately not @CurrentAdmin — the handler tolerates a session with no chosen active role.
  • Rate limits: none declared on this controller.
  • Anti-abuse rules: none specific to this route.
  • Input normalization: not applicable — no input.
  • Sensitive data redaction: the response never includes ids, client identifiers, or key material — only booleans.
  • Audit logs: none written. This is a non-mutating GET, so ActivityAuditInterceptor (which records every mutating request) does not fire for it.
  • Fail-closed behavior: the setup-detail gate defaults to withholding the fields whenever the permission check cannot prove the caller is entitled to see them.

12. Error Handling

Error CodeHTTP StatusThrown ByConditionClient Action
N/A401JwtAuthGuard (global)No valid session.Re-authenticate.

This module defines and throws no error code of its own. ACCOUNTING_INTEGRATION_DISABLED (common/types/error-codes.ts:1966) exists in the shared registry and is thrown by other modules that enforce the switch (via RequiresAccountingIntegration()), but never by this one — reporting enabled: false here is a normal 200, not a refusal.

13. Observability

SignalLocationPurpose
LogNone specific to this moduleNo Logger instance is used in SystemFeaturesService; failures surface through the global exception filter's own logging.
MetricNoneNo custom metric is emitted.
AuditNoneNon-mutating route; excluded from the audit trail by design.

14. Testing and Validation

Test TypeFilesCoverage
UnitNoneNo *.spec.ts exists under apps/api/src/modules/system-features/.
IntegrationNone—
E2ENone—
Structureapps/api/test/structure/route-permissions.spec.ts:127-131Asserts SystemFeaturesController.get is present in the CI mirror of NO_PERMISSION_ADMIN_HANDLERS, so a future omission fails the build rather than 403ing every caller.
Structureapps/api/test/structure/structure.baseline.jsonConfirms the route exists exactly once, at GET /api/system/features, under controllerPath: "system/features".

Validation commands:

pnpm --filter @skoolsewa/api test:structure

See Also