---
title: "Feature gating"
description: "Gate UI and functions by granted benefits and active plans — useFeatures, FeatureBoundary, and friendly benefit keys that never leak provider UUIDs into your app."
canonical_url: "https://nuxt-backend.dev/docs/billing/feature-gating"
---
# Feature gating

> Gate UI and functions by granted benefits and active plans — useFeatures, FeatureBoundary, and friendly benefit keys that never leak provider UUIDs into your app.

Feature gating reads the entitlement cache the billing webhooks keep fresh: the entity's **active plans** (product ids) and **granted benefits**. Every read is live — a `benefit_grant.*` webhook updates every subscriber.

## Friendly names, not UUIDs

`useFeatures().has(feature)` matches a granted benefit by its benefit id, grant id, `type`, **or any value in the benefit's metadata**. The [catalog](https://nuxt-backend.dev/docs/billing/catalog) creates each feature benefit tagged `metadata: { managedBy: 'nuxt-backend', key }`, so gating with the catalog key just works:

```ts [backend/billing.catalog.ts]
features: {
  premium: { description: 'Premium features' },
},
plans: {
  pro: { /* … */, features: ['premium'] },
},
```

```vue
<script setup lang="ts">
const { has, hasPlan, plans, benefits, isLoading } = useFeatures()
const { products } = useBilling()   // product ids by catalog key — undefined while loading
</script>

<template>
  <PremiumPanel v-if="has('premium')" />
  <ProBadge v-if="products?.pro && hasPlan(products.pro.id)" />
</template>
```

For benefits managed in the dashboard instead, set a stable key in the benefit's metadata (e.g. `{ key: 'premium' }`) and gate on it the same way. The cache reads benefit metadata **live** (joined at read time and patched by the `benefit.updated` webhook, with a 15-minute snapshot TTL on sync), so renaming a key takes effect without re-granting.

## `<FeatureBoundary>`

Renders its default slot only when the current billing entity has the required feature (any granted-benefit match) or plan (any active-subscription match); `placeholder` while entitlements load, `fallback` when the check fails. The billing counterpart of [`<RoleBoundary>`](https://nuxt-backend.dev/docs/auth/access/roles#roleboundary).

```vue
<script setup lang="ts">
const { products } = useBilling()
</script>

<template>
  <FeatureBoundary feature="premium">
    <AdvancedEditor />
    <template #fallback><UpgradePrompt /></template>
    <template #placeholder><Skeleton /></template>
  </FeatureBoundary>

  <FeatureBoundary v-if="products" :plan="[products.pro!.id, products.ultra!.id]">
    <ProDashboard />
  </FeatureBoundary>
</template>
```

## Gating on the server

Client gates are for UI. Enforce in the function: the cache is one query away, and the `authed` tier from [`createFunctions`](https://nuxt-backend.dev/docs/auth/access/roles) gives you the caller.

```ts [backend/reports.ts]
import { backendError } from 'nuxt-backend/functions'
import { api } from './_generated/api'
import { authed } from './functions'

export const exportReport = authed.action({
  args: {},
  handler: async (ctx) => {
    const features = await ctx.runQuery(api.billing.getFeatures, {})
    const premium = features?.benefits.some(b => Object.values(b.metadata ?? {}).includes('premium'))
    if (!premium) throw backendError('forbidden', { reason: 'plan', message: 'Upgrade to export reports.' })
    // …
  },
})
```

## Where grants come from

<table>
<thead>
  <tr>
    <th>
      Source
    </th>
    
    <th>
      Cache update
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Checkout completes
    </td>
    
    <td>
      <code>
        order.paid
      </code>
      
       → <code>
        subscription.*
      </code>
      
       / <code>
        benefit_grant.created
      </code>
      
       refresh the entity's plans, benefits, and meters
    </td>
  </tr>
  
  <tr>
    <td>
      Plan change / cancellation
    </td>
    
    <td>
      <code>
        subscription.updated
      </code>
      
       / <code>
        subscription.canceled
      </code>
      
       / <code>
        subscription.revoked
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Benefit renamed or re-keyed in the dashboard
    </td>
    
    <td>
      <code>
        benefit.updated
      </code>
      
       patches the metadata snapshot in place
    </td>
  </tr>
  
  <tr>
    <td>
      Gift claimed
    </td>
    
    <td>
      The $0 claim order's <code>
        order.paid
      </code>
      
       refreshes the recipient's entity
    </td>
  </tr>
  
  <tr>
    <td>
      Webhooks not configured yet
    </td>
    
    <td>
      <code>
        syncEntitlements
      </code>
      
       (<code>
        useCredits().refresh()
      </code>
      
      ) on demand
    </td>
  </tr>
</tbody>
</table>

Fresh grants surface in the DevTools [Backend tab](https://nuxt-backend.dev/docs/start/develop/devtools#billing) (Billing → Entitlements) and the playground.

<page-keys>

- Try it live in the playground: [/playground/platform/features](https://nuxt-backend.dev/playground/platform/features)
- API reference: [/docs/reference/api/use-features](https://nuxt-backend.dev/docs/reference/api/use-features)
- Source: [https://github.com/qruto/nuxt-backend/blob/main/src/runtime/vue/composables/use-features.ts](https://github.com/qruto/nuxt-backend/blob/main/src/runtime/vue/composables/use-features.ts)

</page-keys>


## Sitemap

See the full [sitemap](https://nuxt-backend.dev/sitemap.md) for all pages.
