# Add a credential provider

> Write a CredentialProvider for Nango, Composio or a cloud secret manager, route refs to more than one provider, and check it with the conformance kit.

Source: https://bettersupabase.com/docs/extending/credential-providers

Blocks resolve every `credential_ref` through one `CredentialProvider`
(see [Credentials](/docs/extending/credentials)). Vault and Vercel Connect
ship with better-supabase; anything else is a provider you write against the
same versioned contract. The provider stays in your app or its own
package: better-supabase never depends on Nango, Composio or a cloud SDK.

## The contract [#the-contract]

| Member                                        | Required                               | Does                                                                |
| --------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------- |
| `apiVersion: 1`, `name`                       | yes                                    | the contract version and the `provider` value of the refs it serves |
| `capabilities(ref)`                           | yes                                    | `userSubjects`, `authorization`, `revoke` and `inbound` for the ref |
| `getToken(ref, { subject, scopes })`          | yes                                    | the token, its expiry and the headers that send it                  |
| `revoke(ref, { subject })`                    | yes                                    | forgets the credential; `false` when there was none                 |
| `startAuthorization`, `completeAuthorization` | when `capabilities(ref).authorization` | sends a user to connect an account and finishes the callback        |
| `verifyInbound(request, ref)`                 | when `capabilities(ref).inbound`       | checks a webhook the third party sent                               |
| `set(ref, value, { subject, description })`   | no                                     | stores or replaces a secret the app hands over, such as an API key  |

Every method returns an `AsyncResult` and never throws for a provider
error. Use these error kinds so blocks can react to them:

| Situation                                 | Kind            |
| ----------------------------------------- | --------------- |
| A ref for another provider, or malformed  | `invalid_input` |
| No credential stored for the subject      | `not_found`     |
| The user hasn't connected the account yet | `forbidden`     |
| The provider's API failed                 | `unexpected`    |

Keep the token out of errors, logs and events: only the value `getToken`
returns carries it.

## A cloud secret manager [#a-cloud-secret-manager]

A secret manager holds app credentials. This sketch reads AWS Secrets
Manager, with one secret per ref and, for per-user refs, one per user:

```ts title="src/lib/aws-credentials.ts"
import {
  DeleteSecretCommand,
  GetSecretValueCommand,
  SecretsManagerClient,
} from "@aws-sdk/client-secrets-manager";
import { AsyncResult, dbError, err, ok } from "better-supabase";
import type {
  CredentialProvider,
  CredentialRef,
  CredentialSubject,
} from "better-supabase/credentials";

const client = new SecretsManagerClient({});

function secretId(ref: CredentialRef, subject: CredentialSubject) {
  if (typeof ref.secret !== "string") return undefined;
  const perUser = ref.scope === "user";
  if (perUser && subject.type !== "user") return undefined;
  return perUser && subject.type === "user"
    ? `${ref.secret}/${subject.id}`
    : ref.secret;
}

export const awsCredentials: CredentialProvider = {
  apiVersion: 1,
  name: "aws-secrets",
  capabilities: (ref) => ({
    userSubjects: ref.scope === "user",
    authorization: false,
    revoke: true,
    inbound: false,
  }),
  getToken(ref, { subject, signal }) {
    const id =
      ref.provider === "aws-secrets" ? secretId(ref, subject) : undefined;
    if (id === undefined) {
      return AsyncResult.err(
        dbError("invalid_input", "Not an aws-secrets ref"),
      );
    }
    return AsyncResult.from(async () => {
      try {
        const out = await client.send(
          new GetSecretValueCommand({ SecretId: id }),
          { abortSignal: signal },
        );
        const token = out.SecretString ?? "";
        return ok({ token, headers: { authorization: `Bearer ${token}` } });
      } catch (cause) {
        return cause instanceof Error &&
          cause.name === "ResourceNotFoundException"
          ? err(dbError("not_found", "No secret for this ref"))
          : err(dbError("unexpected", "Secrets Manager failed"));
      }
    });
  },
  revoke(ref, { subject }) {
    const id =
      ref.provider === "aws-secrets" ? secretId(ref, subject) : undefined;
    if (id === undefined) {
      return AsyncResult.err(
        dbError("invalid_input", "Not an aws-secrets ref"),
      );
    }
    return AsyncResult.from(async () => {
      await client.send(
        new DeleteSecretCommand({
          SecretId: id,
          ForceDeleteWithoutRecovery: true,
        }),
      );
      return ok(true);
    });
  },
};
```

Google Secret Manager, Azure Key Vault and HashiCorp Vault follow the same
shape: a ref names the secret, `getToken` reads its latest version, and
`revoke` deletes it or disables the version. Cache tokens for a short time
when the API is slow; clear the cache in `revoke`.

## Nango [#nango]

[Nango](https://nango.dev) holds OAuth connections that users create, and
refreshes their tokens. A ref names the integration
(`{ "provider": "nango", "integration": "github" }`), and the Nango
connection id is derived from the subject, so each user gets their own
connection:

| Method                  | Nango call                                                                             |
| ----------------------- | -------------------------------------------------------------------------------------- |
| `getToken`              | read the connection for the integration and connection id; return its access token     |
| `startAuthorization`    | create a Connect session for the end user and return its connect link as `url`         |
| `completeAuthorization` | nothing to exchange: Nango finishes the OAuth flow; confirm that the connection exists |
| `revoke`                | delete the connection                                                                  |
| `verifyInbound`         | check the `X-Nango-Hmac-Sha256` signature of Nango's webhooks                          |

Return `forbidden` with the hint `CREDENTIAL_AUTHORIZATION_REQUIRED` when
the user has no connection yet, so callers know to start authorization.
Load `@nangohq/node` lazily or call Nango's HTTP API with `fetch`, and keep
the Nango secret key in the server's env.

## Composio [#composio]

[Composio](https://composio.dev) keeps connected accounts per user and
integration. Map the subject to Composio's user id and the ref to an auth
config (`{ "provider": "composio", "authConfig": "ac_github" }`):

| Method               | Composio call                                                      |
| -------------------- | ------------------------------------------------------------------ |
| `getToken`           | read the user's active connected account; return its access token  |
| `startAuthorization` | initiate a connection request and return its redirect URL as `url` |
| `revoke`             | delete the connected account                                       |

When you call tools through Composio itself instead of with the token,
keep the connected account id in the row's `credential_ref` and let the
provider hand the id back as the token. The contract stays the same.

## More than one provider [#more-than-one-provider]

`createServer` takes one `credentials` value. `credentialRouter` from
`better-supabase/credentials` serves several providers as one: each call goes
to the provider whose `name` matches the ref's `provider` field.

```ts title="src/lib/credentials.ts"
import {
  credentialRouter,
  vaultCredentials,
} from "better-supabase/credentials";

export const credentials = credentialRouter([
  vaultCredentials({ transport }),
  nangoCredentials,
  awsCredentials,
]);
```

A ref no provider serves fails with `invalid_input`
(`CREDENTIAL_PROVIDER_UNKNOWN`), and an optional method the chosen provider
lacks, such as `set` on an OAuth-only provider, fails with `unsupported`.
Two providers with the same `name` throw when you build the router.

## Check it [#check-it]

Run `testCredentialProvider` from `better-supabase/testing` against a test
account or a local emulator. It checks the API version, refuses a ref for
another provider, reads a seeded token and a changed one, keeps per-user
credentials apart, revokes, and, with `inbound`, accepts a signed request
and refuses a tampered one:

```ts title="tests/aws-credentials.test.ts"
import { testCredentialProvider } from "better-supabase/testing";
import { it } from "vitest";

it("conforms", () =>
  testCredentialProvider(awsCredentials, {
    ref: { provider: "aws-secrets", secret: "conformance" },
    userRef: { provider: "aws-secrets", secret: "conformance", scope: "user" },
    seed: (ref, subject, value) => putSecret(ref, subject, value),
  }));
```

A table with a `credential_ref` column also needs a lifecycle step that
calls `revoke` when its row goes away, for example when a tenant is
deleted. `revokeIfConfigured(provider, ref, { subject, tenant })` does
that for code where the provider is optional: it resolves `false` without
a provider, when the provider can't revoke the ref, or when the ref belongs
to another tenant.