System Features Backend Documentation
Backend architecture, data model, services, and runtime rules for system-features.
System Features Backend Documentation
1. Documentation Evidence
| Area | Files Inspected | Verified Details |
|---|---|---|
| Module wiring | apps/api/src/modules/system-features/system-features.module.ts | Imports (RoleModule, IntegrationCoreModule), controller, provider. |
| Controllers | apps/api/src/modules/system-features/system-features.controller.ts | Single handler, no @Permissions(), Cache-Control: no-store. |
| Services | apps/api/src/modules/system-features/system-features.service.ts | Feature-flag read, permission check, two read-only queries. |
| DTOs | apps/api/src/modules/system-features/dto/system-features.dto.ts | Response contract and optional-field visibility. |
| Feature flag | apps/api/src/common/feature-flags/accounting-integration.ts | ACCOUNTING_INTEGRATION_ENABLED, isAccountingIntegrationEnabled(). |
| Schema | packages/db (integrationClients, integrationClientKeys, re-exported from @skoolsewa/db) | Columns this service reads: status on both tables, expiresAt on keys, clientId join key. |
| Guard allowlist | apps/api/src/common/authorization/role.guard.ts:745-754 | Why 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_ENABLEDswitch's enforcement on other routes —common/feature-flags/accounting-integration.tsalso exportsRequiresAccountingIntegration(), a decorator used elsewhere to 404 accounting routes while disabled.system-featuresonly reads the flag; it never enforces it. - Permission resolution itself — delegated to
RoleService(modules/role/).
Source of Truth
| Concern | Source of Truth | Notes |
|---|---|---|
| Whether accounting integration is switched on | ACCOUNTING_INTEGRATION_ENABLED env var, read via ConfigService | A deploy-time product decision, not a per-request value. |
| Whether the server holds integration master keys | IntegrationKeyringService.configured | Technical state, independent of the switch. |
| Whether a client is actively connected | integration_clients joined to integration_client_keys | An "active" client requires an active, unexpired key row. |
| Caller's permissions | RoleService.getPermissionsForRoleId() | Superadmin short-circuits without a lookup. |
3. Module Composition
| Module | Type | Path | Controllers | Providers | Exports | Responsibility |
|---|---|---|---|---|---|---|
SystemFeaturesModule | Leaf | apps/api/src/modules/system-features/system-features.module.ts | SystemFeaturesController | SystemFeaturesService | None | Owns 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| File | Purpose | Key Exports | Notes |
|---|---|---|---|
system-features.module.ts | Wires the controller and service, imports the two modules the service depends on. | SystemFeaturesModule | No providers beyond the service; no exports. |
system-features.controller.ts | Thin 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.ts | All business logic: flag read, permission gate, two read-only queries. | SystemFeaturesService | No writes anywhere in this module. |
dto/system-features.dto.ts | Response contract only — there is no request DTO, since the route takes no input. | SystemFeaturesDto, AccountingIntegrationFeatureDto | Optional 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
integrationClientKeys5.2 Tables Read
integration_clients (read-only from this module)
| Column/Field | Read Here | Used For |
|---|---|---|
id | Yes (implicit, via join) | Join target for integrationClientKeys.clientId. |
status | Yes | clientConnected() filters status = "active"; hasClients() filters status != "revoked". |
integration_client_keys (read-only from this module)
| Column/Field | Read Here | Used For |
|---|---|---|
clientId | Yes | Join key back to integration_clients.id. |
status | Yes | clientConnected() filters status = "active". |
expiresAt | Yes | clientConnected() 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
| Method | Called By | Reads | Writes | Side Effects | Errors |
|---|---|---|---|---|---|
get(actor) | SystemFeaturesController.get | ACCOUNTING_INTEGRATION_ENABLED config, IntegrationKeyringService.configured, RoleService permissions, integration_clients/integration_client_keys (conditionally) | None | None | None thrown by this method |
clientConnected() (private) | get(), only when accountingAccess().setup is true | integration_clients, integration_client_keys | None | None | None |
hasClients() (private) | get(), only when the caller qualifies for the setup fields | integration_clients | None | None | None |
accountingAccess() (private) | get() | RoleService.getPermissionsForRoleId (at most one call per request) | None | None | Returns { 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_keys | None | None | Active 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/hasClientsare 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
SystemFeaturesDtodirectly; the controller does not remap it, only wraps it inResponseDto. - 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
| Step | Code Path | Behavior | Failure Case |
|---|---|---|---|
| 1 | SystemFeaturesController.get | Receives request, forwards actor (possibly with no activeRole). | N/A — no validation to fail. |
| 2 | SystemFeaturesService.get | Reads the env flag, builds the base { enabled } object. | Config unreadable would surface as a NestJS bootstrap failure, not a runtime error here. |
| 3 | SystemFeaturesService.accountingAccess | Returns { 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. |
| 4 | SystemFeaturesService.clientConnected / hasClients | Only 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 (
JwtAuthGuardrefuses 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;RoleGuardruns but short-circuits totruefor this handler via theNO_PERMISSION_ADMIN_HANDLERSallowlist (role.guard.ts:745-754), before it would otherwise readrequest.useror 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,hasClientsonsetup;paymentWriteBackonsetuporwriteBackHint), 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, soActivityAuditInterceptor(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 Code | HTTP Status | Thrown By | Condition | Client Action |
|---|---|---|---|---|
| N/A | 401 | JwtAuthGuard (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
| Signal | Location | Purpose |
|---|---|---|
| Log | None specific to this module | No Logger instance is used in SystemFeaturesService; failures surface through the global exception filter's own logging. |
| Metric | None | No custom metric is emitted. |
| Audit | None | Non-mutating route; excluded from the audit trail by design. |
14. Testing and Validation
| Test Type | Files | Coverage |
|---|---|---|
| Unit | None | No *.spec.ts exists under apps/api/src/modules/system-features/. |
| Integration | None | — |
| E2E | None | — |
| Structure | apps/api/test/structure/route-permissions.spec.ts:127-131 | Asserts 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. |
| Structure | apps/api/test/structure/structure.baseline.json | Confirms the route exists exactly once, at GET /api/system/features, under controllerPath: "system/features". |
Validation commands:
pnpm --filter @skoolsewa/api test:structureSee Also
- API doc: /docs/developer/system-features/api
- Features and flows doc: /docs/developer/system-features/feature