# Gate a feature

Register a server-backed access check and a fallback offer. The JavaScript and React gates open the offer only when access is missing, then give you a chance to refresh the customer's access after checkout. An HTML-only page can show the upgrade checkout, but cannot run the access check for a protected feature by itself.

<scalar-tabs default="HTML">
<scalar-tab title="HTML">

Have your server render an upgrade button when access is missing. Your server must still check entitlement before serving the protected export, including after checkout.

```html title="upgrade.html" lineNumbers lineNumberStart=1 highlight="2"
<script defer src="https://your-org.plandalf.dev/js/plandalf-sdk.js"></script>
<button data-plandalf-present="upgrade">Unlock export</button>
```

This is a checkout trigger, **not** a declarative `gate()`. Use the JavaScript or React tab when the page needs to check access and refresh it after checkout.

</scalar-tab>
<scalar-tab title="JavaScript">

```javascript title="feature-gate.js" lineNumbers lineNumberStart=1 highlight="10-12,15-18"
import { Plandalf } from '@plandalf/sdk'

async function readAccessFromServer() {
  const response = await fetch('/api/access/export')
  if (!response.ok) throw new Error('Could not check access')
  return response.json()
}

const sdk = new Plandalf({ apiBase: 'https://your-org.plandalf.dev' })
let access = await readAccessFromServer()
sdk.addGate('export', 'upgrade', (fresh) =>
  fresh?.canExport ?? access.canExport,
)

const result = await sdk.gate('export', async () => {
  access = await readAccessFromServer()
  return access
})

if (result.access) {
  // Call your protected server endpoint; it must check access again.
}
```

</scalar-tab>
<scalar-tab title="React">

```tsx title="ExportButton.tsx" lineNumbers lineNumberStart=1 highlight="11-12,15-16"
import { useCallback } from 'react'
import { PlandalfProvider, useGate } from '@plandalf/react'

async function readAccessFromServer() {
  const response = await fetch('/api/access/export')
  if (!response.ok) throw new Error('Could not check access')
  return response.json()
}

function ExportButton() {
  const check = useCallback(async () => (await readAccessFromServer()).canExport, [])
  const { access, checking, trigger } = useGate('export', 'upgrade', check)

  async function exportFile() {
    const result = await trigger(readAccessFromServer)
    if (result.access) {
      // Call your protected server endpoint; it must check access again.
    }
  }

  return <button disabled={checking} onClick={exportFile}>{access ? 'Export' : 'Unlock export'}</button>
}

export function ExportFeature() {
  return (
    <PlandalfProvider apiBase="https://your-org.plandalf.dev">
      <ExportButton />
    </PlandalfProvider>
  )
}
```

</scalar-tab>
</scalar-tabs>

For the JavaScript and React gates, if the customer dismisses the offer, `access` is false. A failed check or sync returns `status: 'error'` and `access: false`. A denied gate has already handled its fallback offer; do not call `present()` again. The protected server endpoint must make its own access decision.

See [`GateResult`](https://plandalf.com/docs/packages/sdk/interfaces/GateResult), [`GateCheck`](https://plandalf.com/docs/packages/sdk/type-aliases/GateCheck), [`GateSyncCallback`](https://plandalf.com/docs/packages/sdk/type-aliases/GateSyncCallback), and [`useGate()`](https://plandalf.com/docs/packages/react/functions/useGate) for exact types.

Source: https://plandalf.com/docs/guides/gate-a-feature
