# Show only the OAuth buttons your Supabase project has switched on

> Read the public /auth/v1/settings endpoint on the server, cache it, and fail closed, so a sign-in page never shows a Google button that ends on an error page.

A sign-in page with a "Continue with GitHub" button for a provider that is off
in the Supabase dashboard looks fine until someone clicks it. They leave your
site, land on a Supabase error page that says the provider is not enabled, and
most of them do not come back.

The usual fix is an env flag per provider (`NEXT_PUBLIC_ENABLE_GITHUB=true`).
That is a second switch that drifts from the real one: someone turns GitHub on
in the dashboard and forgets the flag, or turns it off and leaves the button.

Supabase already publishes the real switch.

## The endpoint

```
GET https://<project-ref>.supabase.co/auth/v1/settings
apikey: <publishable (anon) key>
```

It needs only the publishable key, which is public by design, and answers with
the project's sign-in configuration:

```json
{
  "external": {
    "email": true,
    "google": true,
    "github": false,
    "azure": true,
    "apple": false
  },
  "disable_signup": false,
  "mailer_autoconfirm": false
}
```

`external` is keyed by Supabase's provider ids. Microsoft is `azure`.

## Parse it, and only what you read

Keep the parse pure, apart from the fetch, so it is unit tested without a
network:

```ts
import { z } from "zod";

export const OAUTH_PROVIDERS = [
  { id: "google", label: "Google" },
  { id: "github", label: "GitHub" },
  { id: "azure", label: "Microsoft" },
] as const;

const settingsResponse = z.object({
  external: z.record(z.string(), z.unknown()),
  disable_signup: z.boolean().optional(),
});

export function parseAuthSettings(json: unknown) {
  const parsed = settingsResponse.safeParse(json);
  if (!parsed.success) return FALLBACK;
  const { external, disable_signup } = parsed.data;
  return {
    providers: OAUTH_PROVIDERS.filter((p) => external[p.id] === true),
    emailEnabled: external.email !== false,
    signupDisabled: disable_signup === true,
    known: true,
  };
}
```

Three details that matter:

- **`=== true`, not truthy.** A string `"true"` from a proxy or a future format
  change should not light up a button.
- **The display order comes from your list**, not from the response. Google
  first is the order people expect.
- **Unknown keys are ignored.** A GoTrue upgrade that adds a field must not
  break sign-in.

## Fail closed, but not all the way

When the call fails (no network, a wrong URL, an outage), show **no OAuth
buttons** and **keep the email form**:

```ts
export const FALLBACK = {
  providers: [],
  emailEnabled: true,
  signupDisabled: false,
  known: false,
};
```

No buttons, because a button for a provider that might be off is the failure
you set out to avoid. The email form stays, because hiding it would lock
everyone out during a blip. If email really is off, the form's own error says
so.

## Fetch on the server, cached

```ts
export async function getAuthSettings() {
  try {
    const response = await fetch(`${SUPABASE_URL}/auth/v1/settings`, {
      headers: { apikey: SUPABASE_ANON_KEY },
      next: { revalidate: 300 },
      signal: AbortSignal.timeout(3000),
    });
    if (!response.ok) return FALLBACK;
    return parseAuthSettings(await response.json());
  } catch {
    return FALLBACK;
  }
}
```

- `next: { revalidate: 300 }` puts the answer in Next's data cache for five
  minutes: one call per window, not one per visitor. Switching a provider on
  shows its button within five minutes, with no deploy.
- The timeout keeps a slow Supabase from hanging your sign-in page.
- Server-side, so the page arrives with the right buttons. Fetching in the
  browser means a flash of no buttons, then buttons.

The page passes `providers` to a client component that renders one button each
and calls `signInWithOAuth`. The same list drives the sign-up page and a
"Connected accounts" settings card, so all three always agree.

## Use the same list server-side

A server action that links an identity should check the list too, not just
the form value:

```ts
const { provider } = z.object({ provider: z.enum(["google", "github", "azure"]) }).parse(input);
if (!(await getAuthSettings()).providers.some((p) => p.id === provider)) {
  return { error: "That sign-in method is turned off." };
}
```

A form can be posted with any value. The check costs nothing: it is the cached
answer.

## Test it

```ts
it("shows only providers whose flag is literally true", () => {
  const settings = parseAuthSettings({ external: { google: true, github: "true", azure: 1 } });
  expect(settings.providers.map((p) => p.id)).toEqual(["google"]);
});

it("fails closed on anything that is not the settings shape", () => {
  for (const junk of [null, "oops", { external: null }]) {
    expect(parseAuthSettings(junk).providers).toEqual([]);
  }
});
```

Then by hand: switch a provider off in the dashboard, wait five minutes, and
check the button is gone. Point the URL at a host that does not exist and check
the page still renders with the email form.

---

Agentic Boilerplate: A Next.js repo your agent already knows. Free during launch, then $99 once.

- Site map for agents: https://agenticboilerplate.com/llms.txt
- Public API: https://agenticboilerplate.com/openapi.json
- Contact: agenticstudio@gmail.com
