Skip to content

Subscription & Plans ​

Overview ​

  1. Feature-flag model: The backend returns a features map inside the subscription response. Each key is a feature name; the value is a SubscriptionFeature object with an enable: boolean field and an optional message string shown to users when the feature is locked.
  2. Single shared query: useSubscriptionQuery wraps TanStack Query around useWorkspaceSettingsApi.getSubscription. All pages that need plan data use this composable so the response is cached once per workspace instead of fetched independently by each page.
  3. Workspace-scoped: The query key includes the workspace ID, so switching workspaces invalidates and re-fetches the subscription automatically.
  4. Component-level gating: Components read subscriptionQuery.data.features directly and derive computed booleans for each gate. There is no separate permissions layer for subscription checks.
  5. Upgrade prompts: When a gated feature is disabled, components render a dedicated upgrade UI section (UpgradePlanSection, StaticPlanWorkspaceBranding, PortalUpgradePlanSection, StaticPlanNotificationSettings) or an inline tooltip. The portal creation limit is handled specifically by PortalLimitDialog.
  6. Real-time plan update: After a subscription event arrives on the Pusher channel user.{id}.getUserSubscriptionChannel, the notification composable calls fetchUser() and refreshes workspace data. TanStack Query cache is invalidated to ensure subscription data re-fetches.

Architecture ​

Subscription data flows from a single backend endpoint (digital-assets/subscription/get) through useWorkspaceSettingsApi.getSubscription, cached by useSubscriptionQuery via TanStack Query. The cache key is queryKeys.settings.subscription(workspaceId) and is enabled only when a workspace ID is present. Components consume subscriptionQuery.data directly — typically destructuring features from the response — and derive computed booleans to control visibility or disabled states of gated UI elements. Upgrade prompts are standalone components placed inline at the point of enforcement. The PortalSubscription type (on the portal object) carries max_instances and max_storage for per-portal limits, while the workspace-level subscription carries the feature flags.

File Structure ​

TypeScript Files ​

  • app/composables/queries/useSubscriptionQuery.ts — TanStack Query wrapper; the canonical way to read subscription data in any component
  • app/composables/api/useWorkspaceSettingsApi.ts — API composable containing getSubscription() which calls digital-assets/subscription/get
  • app/types/workspace-settings.ts — defines SubscriptionData interface
  • app/types/portal.ts — defines SubscriptionFeature, SubscriptionFeatures, and PortalSubscription interfaces
  • app/constants/queryKeys.ts — defines queryKeys.settings.subscription(workspaceId) cache key

Vue Component Files ​

  • app/components/dialogs/PortalLimitDialog.vue — shown when user tries to create a portal beyond the plan's max_instances limit
  • app/components/shared/UpgradePlanSection.vue — generic upgrade CTA section for portal-level gating
  • app/components/dam/portals/PortalUpgradePlanSection.vue — portal-specific upgrade prompt rendered inside portal settings
  • app/components/dam/portals/StaticPlanWorkspaceBranding.vue — upgrade section shown in workspace branding settings when branding features are locked
  • app/components/dam/StaticPlanNotificationSettings.vue — upgrade section for locked notification settings

useSubscriptionQuery Composable ​

File: app/composables/queries/useSubscriptionQuery.ts

Features ​

  • Reads workspace ID from route params with a fallback to useWorkspaceId()
  • Computed queryKey — reactively updates if workspace ID changes
  • Enabled guard — query is disabled when workspace ID is absent
  • Returns the raw SubscriptionData object as returned by the backend

Return Value ​

typescript
{
  subscriptionQuery: UseQueryReturnType<SubscriptionData, Error>
}

Usage ​

typescript
const { subscriptionQuery } = useSubscriptionQuery()

// Access feature flags
const features = computed(() => subscriptionQuery.data.value?.features ?? {})

// Derive a per-feature gate
const brandingEnabled = computed(
  () => features.value.dam_branding?.enable === true
)

// Show an upgrade message from the backend
const brandingMessage = computed(
  () => features.value.dam_branding?.message ?? ''
)

Type Reference ​

SubscriptionData ​

typescript
// app/types/workspace-settings.ts
export interface SubscriptionData {
  features?: Record<string, SubscriptionFeature>
  [key: string]: unknown
}

SubscriptionFeature ​

typescript
// app/types/portal.ts
export interface SubscriptionFeature {
  enable?: boolean    // true = feature is active on this plan
  message?: string    // human-readable upgrade prompt text
  [key: string]: unknown
}

SubscriptionFeatures (named feature keys) ​

typescript
// app/types/portal.ts
export interface SubscriptionFeatures {
  custom_brand_url?: SubscriptionFeature   // white-label / custom domain
  dam_branding?: SubscriptionFeature       // portal branding (colours, logo, favicon)
  [key: string]: SubscriptionFeature | undefined
}

PortalSubscription (per-portal limits) ​

typescript
// app/types/portal.ts
export interface PortalSubscription {
  max_instances?: number           // portal count limit
  max_storage?: number | string    // storage limit in GB
  features?: SubscriptionFeatures  // per-portal feature flags
  [key: string]: unknown
}

useWorkspaceSettingsApi — getSubscription ​

File: app/composables/api/useWorkspaceSettingsApi.ts

typescript
const getSubscription = async (): Promise<ApiResponse<SubscriptionData>> => {
  return await $api.get<SubscriptionData>('digital-assets/subscription/get')
}

Use useSubscriptionQuery rather than calling getSubscription directly. The query composable ensures a single cached fetch is shared across all consumers.

Plan Gating Pattern ​

Components derive computed booleans from the feature flags and use them to conditionally render upgrade prompts or disable controls:

vue
<template>
  <div>
    <template v-if="brandingEnabled">
      <!-- Branding controls -->
      <BrandingColorPicker />
    </template>
    <template v-else>
      <StaticPlanWorkspaceBranding :message="brandingMessage" />
    </template>
  </div>
</template>

<script setup lang="ts">
const { subscriptionQuery } = useSubscriptionQuery()

const features = computed(() => subscriptionQuery.data.value?.features ?? {})

const brandingEnabled = computed(
  () => features.value.dam_branding?.enable === true
)

const brandingMessage = computed(
  () => features.value.dam_branding?.message ?? 'Upgrade to enable workspace branding.'
)
</script>

Avoid duplicating this pattern. If the same feature gate appears in more than one component, extract the computed to a shared composable.

Workflows ​

Subscription Data Load ​

1. Component mounts in a workspace context
   ↓
2. useSubscriptionQuery() called
   queryKey = queryKeys.settings.subscription(workspaceId)
   enabled = true (workspaceId is present)
   ↓
3. TanStack Query checks cache
   Cache hit → return cached SubscriptionData immediately
   Cache miss → call useWorkspaceSettingsApi.getSubscription()
   ↓
4. API call
   GET digital-assets/subscription/get
   Authorization: Bearer {auth_token}
   ↓
5. Response stored in TanStack Query cache
   subscriptionQuery.data available to all consumers on this workspace
   ↓
6. Components derive computed booleans from features map
   Render feature UI or upgrade prompt accordingly

Portal Creation Limit Enforcement ​

1. User clicks "Create Portal"
   Component: CreatePortalDialog
   ↓
2. Dialog reads max_instances from PortalSubscription
   Also reads current portal count from portals list query
   ↓
3. Count check
   current >= max_instances → show PortalLimitDialog
   current < max_instances → proceed with creation form
   ↓
4. PortalLimitDialog shown
   Displays plan limit and current usage
   "Upgrade" button links to billing / plan upgrade page
   ↓
5. User upgrades plan
   Backend webhook updates subscription
   useSubscriptionQuery cache is invalidated
   ↓
6. Cache re-fetches
   subscriptionQuery.data updated with new limits
   CreatePortalDialog now permits creation

Real-time Subscription Update ​

1. User completes payment on billing page
   ↓
2. Backend processes webhook
   Pusher event pushed to:
   user.{id}.getUserSubscriptionChannel
   Event: .GetUserSubscriptionEvent
   Payload: { plan_assign: true }
   ↓
3. useDamNotification listener fires
   Calls fetchUser() to refresh auth user object
   Calls viewWorkspace() to get updated workspace data
   Calls updateSubscriptionEventSent() to acknowledge event
   ↓
4. TanStack Query subscription cache invalidated
   useSubscriptionQuery re-fetches from digital-assets/subscription/get
   ↓
5. New plan limits visible immediately
   Upgrade prompts hide, gated features unlock

API Integration ​

Endpoints ​

MethodEndpointDescription
GETdigital-assets/subscription/getCurrent workspace subscription data and feature flags

Response Shape ​

json
{
  "data": {
    "features": {
      "dam_branding": {
        "enable": true,
        "message": null
      },
      "custom_brand_url": {
        "enable": false,
        "message": "Upgrade to Pro to enable custom domain support."
      },
      "system_notifications": {
        "enable": true,
        "message": null
      }
    }
  }
}

Component Integration ​

Gating a Feature with Upgrade Prompt ​

Feature flags are read from subscriptionQuery.data.value?.features. All upgrade prompt components (UpgradePlanSection, PortalUpgradePlanSection, StaticPlanWorkspaceBranding, StaticPlanNotificationSettings) accept no props — they render hardcoded static UI and a "Contact Support" mailto link.

vue
<template>
  <div>
    <template v-if="usersEnabled">
      <!-- user management controls -->
    </template>
    <UpgradePlanSection v-else />
  </div>
</template>

<script setup lang="ts">
const { subscriptionQuery } = useSubscriptionQuery()

const usersEnabled = computed(
  () => !!subscriptionQuery.data.value?.features?.users?.enable
)
</script>

Checking a Limit Before an Action ​

User-count limits live inside the feature flag object, not at the top level of the subscription response. The actual implementation from usePortalUsersPageSettings.ts:

typescript
const { subscriptionQuery } = useSubscriptionQuery()

const usersEnabled = computed(
  () => !!subscriptionQuery.data.value?.features?.users?.enable
)

const canAddMoreUsers = computed(() => {
  if (!usersEnabled.value) return false
  const maxAllowedUsers = subscriptionQuery.data.value?.features?.users?.users
  if ((maxAllowedUsers + '').trim().toLowerCase() === 'unlimited') return true
  if (totalInstanceUsers.value >= parseInt(String(maxAllowedUsers))) return false
  return true
})

totalInstanceUsers comes from the portal users list query. When canAddMoreUsers is false, the "Add New User" button is disabled with a tooltip explaining the plan limit.

Upgrade Prompt Components ​

These components are placed at the point of enforcement rather than behind a central upgrade modal:

StaticPlanWorkspaceBranding ​

File: app/components/dam/portals/StaticPlanWorkspaceBranding.vue

Rendered inside workspace branding settings when dam_branding.enable === false. Replaces the color picker, logo upload, and favicon controls with a locked panel and an upgrade call to action.

StaticPlanNotificationSettings ​

File: app/components/dam/StaticPlanNotificationSettings.vue

Rendered in notification settings when system notifications are locked on the current plan. Shows the upgrade message from features.system_notifications.message.

PortalUpgradePlanSection ​

File: app/components/dam/portals/PortalUpgradePlanSection.vue

Rendered inside a portal's settings page when a portal-level feature is locked. Includes the feature name and the upgrade CTA.

UpgradePlanSection ​

File: app/components/shared/UpgradePlanSection.vue

Generic upgrade section component. Accepts a message prop from the backend feature flag. Used wherever a generic "upgrade your plan" prompt is needed without portal-specific context.

PortalLimitDialog ​

File: app/components/dialogs/PortalLimitDialog.vue

Modal shown when a user tries to create a new portal after reaching max_instances. It displays the current portal count, the plan limit, and a link to upgrade. It does not block navigation — the user can dismiss it and return to the portal list.

canAddMoreUsers Pattern ​

The portal users page derives a boolean from subscription data to gate the "Add New User" button. From app/composables/core/pages/usePortalUsersPageSettings.ts:

typescript
const { subscriptionQuery } = useSubscriptionQuery()

const usersEnabled = computed(
  () => !!subscriptionQuery.data.value?.features?.users?.enable
)

const canAddMoreUsers = computed(() => {
  if (!usersEnabled.value) return false
  const maxAllowedUsers = subscriptionQuery.data.value?.features?.users?.users
  if ((maxAllowedUsers + '').trim().toLowerCase() === 'unlimited') return true
  if (totalInstanceUsers.value >= parseInt(String(maxAllowedUsers))) return false
  return true
})

The button disables and shows a tooltip: "Please upgrade your subscription plan to add more users". This is an inline enforcement pattern — no modal is shown, the CTA is the tooltip text itself.

Feature Flag Key Reference ​

The features map uses string keys. Currently documented keys in SubscriptionFeatures:

KeyPurpose
custom_brand_urlCustom domain / white-label CNAME
dam_brandingPortal branding (theme, logo, favicon)
system_notificationsIn-app system notification bell

Additional keys exist (e.g. for auto-tagging, insights, file conversion) and are accessed via the [key: string]: SubscriptionFeature | undefined index signature.

Query Key Structure ​

typescript
// From constants/queryKeys.ts
queryKeys.settings.subscription(workspaceId: string)
// Returns a stable array used as the TanStack Query cache key.
// Invalidate this key to force a subscription re-fetch:
queryClient.invalidateQueries({
  queryKey: queryKeys.settings.subscription(workspaceId),
})