Access control — Feature Flags
App/Network/Customer feature cascade, conditional activation and integration in guards.
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
| Feature | Description |
|---|---|
invitations | User invitation flow (email sending, acceptance, provisioning) |
antivirus | Antivirus scanning of uploaded files (ClamAV via the antivirus module) |
hds | Healthcare Data Security features (the HDS module) |
ai-translation | AI automatic translation (consumes credits) |
export | Export + Stellar blockchain anchoring (see Stellar module for the evidentiary value limits depending on the configured network) |
audit | Viewing audit logs |
billing | Billing and credit management module |
Frontend usage
Simple check
typescriptimport { 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 toGET /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 disabledanyway.
Activation via the API
The actual endpoints live under /api/config/app-features — there is no dedicated route per customerWid/appWid + feature name:
httpGET /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).
httpGET /api/config/app-features/enabled/{customerWid} Authorization: Bearer <wakaToken>
Lists all features active for that Customer.
httpGET /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
403to hide a button. - No feature enabled by default: if
GET app-features/check/...returnsenabled: false(or if the feature doesn't appear inapp-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 callsapp-features/check/.... Set up your own short cache on the Wakapp side if call volume justifies it.
Go further
- Rights model: adminLevel, appRights
- Invitations: full example combining feature + right