WakaStart
Security

Access control — Feature Flags

App/Network/Customer feature cascade, conditional activation and integration in guards.

Version v1.04 min read

Access control — Feature Flags

This chapter covers the feature cascade and its practical use. Read Rights model and Guards & Hooks first.


Cascade model

A feature is the result of three independent activations. If even one is false, the feature is unavailable to the user:

Loading diagram…

Practical consequence: a Partner can enable a feature globally at the App level, but a specific Customer can disable it for their users. This mechanism allows fine-grained control without custom code.


Standard platform features

FeatureDescription
invitationsUser invitation flow (email sending, acceptance, provisioning)
antivirusAntivirus scanning of uploaded files (ClamAV via the antivirus module)
hdsHealthcare Data Security features (the HDS module)
ai-translationAI automatic translation (consumes credits)
exportExport + Stellar blockchain anchoring (see Stellar module for the evidentiary value limits depending on the configured network)
auditViewing audit logs
billingBilling and credit management module

Frontend usage

Simple check

typescript
import { useEffect, useState } from "react"; import { useAuth } from "@/lib/auth-context"; function InvitationsSection() { const { user } = useAuth(); const [enabled, setEnabled] = useState<boolean | null>(null); useEffect(() => { if (!user?.customer?.wid) return; fetch(`/api/proxy/config/app-features/check/${user.customer.wid}/invitations`) .then((r) => r.json()) .then((data) => setEnabled(data.enabled)); }, [user?.customer?.wid]); if (enabled === false) { return null; // Hide the section entirely } return <InvitationsList />; }

This is NOT a synchronous field of useAppRights()//me — the check goes through a network call to GET /api/config/app-features/check/{customerWid}/{featureCode} (see Guards & Hooks).

Combining feature + right

tsx
// Resolve the feature upstream (see example above), then combine with the right: {enabled && ( <Protected right="invitations.create"> <InviteUserButton /> </Protected> )}

Recommended order: check the feature first. If the feature is disabled, there's no point testing rights — the API will return 403 Feature disabled anyway.


Activation via the API

The actual endpoints live under /api/config/app-features — there is no dedicated route per customerWid/appWid + feature name:

http
GET /api/config/app-features/check/{customerWid}/{featureCode} Authorization: Bearer <wakaToken>

Checks whether a specific feature is enabled in cascade (App ∧ Network ∧ Customer) for that Customer — this is the endpoint to call from the frontend/backend for a one-off check (see Guards & Hooks).

http
GET /api/config/app-features/enabled/{customerWid} Authorization: Bearer <wakaToken>

Lists all features active for that Customer.

http
GET /api/config/app-features/by-app/{appWid} Authorization: Bearer <wakaToken>

Lists the features defined for an App (Partner level).

Activation/deactivation itself (creating, modifying, deleting an AppFeature/NetworkAppFeature/CustomerAppFeature entry) goes through a generic CRUD keyed by internal id: GET/POST/PATCH/DELETE /api/config/app-features/{id}. See the API reference or the OpenAPI spec for the exact body of these requests — there is no route that takes {customerWid}/{appWid} + a feature name directly as a parameter for a PATCH activation.


NestJS backend guard — RequireFeature

If your Wakapp exposes endpoints that require an active feature:

typescript
// decorators/require-feature.decorator.ts import { SetMetadata } from "@nestjs/common"; export const REQUIRED_FEATURE_KEY = "requiredFeature"; export const RequireFeature = (feature: string) => SetMetadata(REQUIRED_FEATURE_KEY, feature);
typescript
// guards/feature.guard.ts import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common"; import { Reflector } from "@nestjs/core"; @Injectable() export class FeatureGuard implements CanActivate { constructor(private reflector: Reflector) {} async canActivate(context: ExecutionContext): Promise<boolean> { const requiredFeature = this.reflector.getAllAndOverride<string>( REQUIRED_FEATURE_KEY, [context.getHandler(), context.getClass()] ); if (!requiredFeature) return true; const request = context.switchToHttp().getRequest(); const enrichedPayload = request.enrichedPayload; // set by AuthMiddleware const customerWid = enrichedPayload?.cid; // No "features" field in the token/enrichedPayload: we query // the dedicated API on every request (consider a short cache on the Wakapp side). const res = await fetch( `${process.env.WAKASTART_API_URL}/api/config/app-features/check/${customerWid}/${requiredFeature}`, { headers: { "x-api-key": process.env.WAKASTART_API_KEY! } }, ); const { enabled } = (await res.json()) as { enabled: boolean }; if (!enabled) { throw new ForbiddenException( `The ${requiredFeature} feature is not enabled for this customer` ); } return true; } }

Usage:

typescript
@Controller("invitations") @UseGuards(FeatureGuard, AppRightGuard) export class InvitationsController { @Post() @RequireFeature("invitations") @RequireAppRight("invitations.create") async createInvitation(@Body() dto: CreateInvitationDto) { // ... } }

Best practices

  • Check the feature before rendering the UI: don't wait for the API's 403 to hide a button.
  • No feature enabled by default: if GET app-features/check/... returns enabled: false (or if the feature doesn't appear in app-features/enabled/{customerWid}), treat it as disabled.
  • Logging: log access attempts to disabled features to identify UI bugs.

Common pitfalls

  • Missing feature ≠ error: if app-features/enabled/{customerWid} returns an empty array, it's not a bug — the customer simply has no feature enabled.
  • Don't confuse feature and AppRight: an active feature does not guarantee the right — both are required.
  • No automatic cache: unlike adminLevel/appRights (in /me, cached client-side), a feature's state is NOT in /me — every check calls app-features/check/.... Set up your own short cache on the Wakapp side if call volume justifies it.

Go further