> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hookfish.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom providers

> Implement the OAuthProvider contract for an unsupported service.

Implement `OAuthProvider` from `@hookfish/provider` when a service is not built
in. The provider owns the upstream protocol details; Hookfish owns state,
storage, encryption, resource authorization, and connection lifecycle.

## Required operations

Every provider must create an authorization request and exchange the returned
code.

```ts theme={null}
import type {
  CreateAuthorizationInput,
  ExchangeCodeInput,
  OAuthProvider,
  ProviderTokenResponse,
} from '@hookfish/provider'
import { ProviderRequestError } from '@hookfish/provider'

export class ExampleProvider implements OAuthProvider {
  readonly label = 'Example'
  readonly defaultScopes = ['profile.read']
  readonly availableScopes = ['profile.read', 'projects.read']

  constructor(
    private readonly clientId: string,
    private readonly clientSecret: string,
  ) {}

  createAuthorization(input: CreateAuthorizationInput) {
    const url = new URL('https://accounts.example.com/oauth/authorize')
    url.searchParams.set('response_type', 'code')
    url.searchParams.set('client_id', this.clientId)
    url.searchParams.set('redirect_uri', input.redirectUri)
    url.searchParams.set('state', input.state)
    url.searchParams.set('scope', input.scopes.join(' '))
    return { url: url.toString() }
  }

  async exchangeCode(
    input: ExchangeCodeInput,
  ): Promise<ProviderTokenResponse> {
    const response = await fetch('https://accounts.example.com/oauth/token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        code: input.code,
        redirect_uri: input.redirectUri,
        client_id: this.clientId,
        client_secret: this.clientSecret,
      }),
    })
    const payload: unknown = await response.json()
    if (
      !response.ok ||
      typeof payload !== 'object' ||
      payload === null ||
      Array.isArray(payload) ||
      typeof Reflect.get(payload, 'access_token') !== 'string'
    ) {
      throw new ProviderRequestError('Example token exchange failed.')
    }

    return { payload: Object.fromEntries(Object.entries(payload)) }
  }
}
```

The helper functions in this example belong to your provider package. Keep
client authentication, request encoding, response validation, PKCE generation,
and error translation inside that package.

## Optional capabilities

Implement `refreshToken` when the provider issues refresh tokens. Implement
`revokeToken` when it exposes a revocation API. Hookfish reports these
capabilities in provider discovery and invokes them at the appropriate point in
the connection lifecycle.

Implement `isConfigured` when provider availability depends on credentials.
Return `false` instead of failing the complete provider listing.

## Make a reusable template

Implement `OAuthProviderTemplate` when operators should create dynamic
instances. A template can validate non-secret configuration, replace
credentials, and optionally register OAuth clients.

<Tip>
  Validate every upstream JSON response before returning it. Throw
  `ProviderConfigurationError` for invalid local configuration and
  `ProviderRequestError` for upstream failures.
</Tip>
