Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
5.2 KiB
5.2 KiB
Tier Gating Developer Guide
How to add a new feature to the tier gate system.
Architecture Overview
The tier system has two tiers: Pro and Premium. The tenants.plan column is the single source of truth. Features are gated at three layers:
- UI — hide/show with
TierGate(client) orServerTierGate(server component) - Navigation — filter sidebar items
- Server actions — enforce with
assertTierAccess()
In Community Edition (CE), all features are unlocked regardless of tier.
Step-by-Step: Adding a New Gated Feature
1. Add to TIER_FEATURES enum
// packages/types/src/constants/tierFeatures.ts
export enum TIER_FEATURES {
ENTRA_SYNC = 'ENTRA_SYNC',
CIPP = 'CIPP',
YOUR_NEW_FEATURE = 'YOUR_NEW_FEATURE', // ← add here
}
2. Add to TIER_FEATURE_MAP
Map which tiers get the feature:
// packages/types/src/constants/tierFeatures.ts
export const TIER_FEATURE_MAP: Record<TenantTier, TIER_FEATURES[]> = {
pro: [],
premium: [
TIER_FEATURES.ENTRA_SYNC,
TIER_FEATURES.CIPP,
TIER_FEATURES.YOUR_NEW_FEATURE, // ← add here
],
};
3. Add to FEATURE_MINIMUM_TIER
// packages/types/src/constants/tierFeatures.ts
export const FEATURE_MINIMUM_TIER: Record<TIER_FEATURES, TenantTier> = {
[TIER_FEATURES.ENTRA_SYNC]: 'premium',
[TIER_FEATURES.CIPP]: 'premium',
[TIER_FEATURES.YOUR_NEW_FEATURE]: 'premium', // ← add here
};
4. Gate UI components
Client-side (in a client component):
import { TierGate } from '@/components/tier-gating/TierGate';
import { TIER_FEATURES } from '@alga-psa/types';
// Wraps children — shows FeatureUpgradeNotice if tier lacks access
<TierGate feature={TIER_FEATURES.YOUR_NEW_FEATURE} featureName="Your Feature">
<YourFeatureComponent />
</TierGate>
Or use the hook directly:
import { useTierFeature } from '@/context/TierContext';
import { TIER_FEATURES } from '@alga-psa/types';
const canUseFeature = useTierFeature(TIER_FEATURES.YOUR_NEW_FEATURE);
// canUseFeature is boolean — true if tier has access OR if CE edition
Server component:
import { ServerTierGate } from '@/lib/tier-gating/ServerTierGate';
import { TIER_FEATURES } from '@alga-psa/types';
// Async server component — reads session directly
<ServerTierGate feature={TIER_FEATURES.YOUR_NEW_FEATURE} featureName="Your Feature">
<YourFeatureComponent />
</ServerTierGate>
5. Gate server actions
import { assertTierAccess, TierAccessError } from '@/lib/tier-gating/assertTierAccess';
import { TIER_FEATURES } from '@alga-psa/types';
export async function yourProtectedAction() {
// Throws TierAccessError if tenant lacks access
// CE edition: always passes
await assertTierAccess(TIER_FEATURES.YOUR_NEW_FEATURE);
// ... your action logic
}
6. Add display name in AccountManagement
// ee/server/src/components/settings/account/AccountManagement.tsx
const FEATURE_DISPLAY_NAMES: Record<TIER_FEATURES, string> = {
[TIER_FEATURES.ENTRA_SYNC]: 'Microsoft Entra Sync — ...',
[TIER_FEATURES.CIPP]: 'CIPP Integration — ...',
[TIER_FEATURES.YOUR_NEW_FEATURE]: 'Your Feature — description here', // ← add
};
7. Write tests
Unit test for feature mapping:
// packages/types/src/constants/tierFeatures.test.ts
it('premium tier has YOUR_NEW_FEATURE', () => {
expect(tierHasFeature('premium', TIER_FEATURES.YOUR_NEW_FEATURE)).toBe(true);
});
it('pro tier does not have YOUR_NEW_FEATURE', () => {
expect(tierHasFeature('pro', TIER_FEATURES.YOUR_NEW_FEATURE)).toBe(false);
});
Unit test for server action gating:
// your-feature.test.ts
it('throws TierAccessError for pro tenant', async () => {
mockGetSession.mockResolvedValue({ user: { plan: 'pro' } });
await expect(assertTierAccess(TIER_FEATURES.YOUR_NEW_FEATURE))
.rejects.toThrow(TierAccessError);
});
CE Bypass Behavior
In Community Edition (NEXT_PUBLIC_EDITION !== 'enterprise'):
TierContext.hasFeature()always returnstrueServerTierGaterenders children unconditionallyassertTierAccess()returns without checking
This means CE users get all features regardless of tenants.plan.
Key Files
| File | Purpose |
|---|---|
packages/types/src/constants/tierFeatures.ts |
Feature enum, tier-to-feature mapping |
packages/types/src/constants/tenantTiers.ts |
Tier types, resolveTier() |
server/src/context/TierContext.tsx |
Client-side tier context + hooks |
server/src/components/tier-gating/TierGate.tsx |
Client-side gate component |
server/src/lib/tier-gating/ServerTierGate.tsx |
Server-side gate component |
server/src/lib/tier-gating/assertTierAccess.ts |
Server action enforcement |
packages/ui/src/components/tier-gating/FeatureUpgradeNotice.tsx |
Upgrade CTA shown when gated |
ee/server/src/components/settings/account/AccountManagement.tsx |
Account page feature display |
Existing Gated Features (for reference)
| Feature | Enum | Gated Where |
|---|---|---|
| Visual Invoice Designer | INVOICE_DESIGNER |
InvoiceTemplateEditor visual tab, BillingPageClient |
| Entra Sync | ENTRA_SYNC |
IntegrationsSettingsPage, SettingsPage |
| CIPP | CIPP |
EntraIntegrationSettings connection options |