# Social Auth flow for React Router and Supabase

> Social Auth flow for React Router and Supabase

A shadcn/ui block from the [@supabase](https://supabase.com/ui?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/supabase/social-auth-react-router).

- Page, with a live preview: https://shadcn.exchange/r/supabase/social-auth-react-router
- Screenshot: https://shadcn.exchange/shot/supabase/social-auth-react-router/light.png
- Registry item: https://supabase.com/ui/r/social-auth-react-router.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/supabase/social-auth-react-router.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@react-router/dev`, `@react-router/fs-routes`, `@supabase/ssr`, `@supabase/supabase-js`
- Registry items: `button`, `card`, `https://supabase.com/library/r/safe-next-path.json`

## Notes from the registry

You'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.

## Files

### `registry/default/blocks/social-auth-react-router/app/routes/auth.error.tsx`

```tsx
import { useSearchParams } from 'react-router'

import { Card, CardContent, CardHeader, CardTitle } from '@/registry/default/components/ui/card'

export default function Page() {
  let [searchParams] = useSearchParams()

  return (
    <div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
      <div className="w-full max-w-sm">
        <div className="flex flex-col gap-6">
          <Card>
            <CardHeader>
              <CardTitle className="font-medium text-lg tracking-normal">
                Sorry, something went wrong.
              </CardTitle>
            </CardHeader>
            <CardContent>
              {searchParams?.get('error') ? (
                <p className="text-sm text-muted-foreground">
                  Code error: {searchParams?.get('error')}
                </p>
              ) : (
                <p className="text-sm text-muted-foreground">An unspecified error occurred.</p>
              )}
            </CardContent>
          </Card>
        </div>
      </div>
    </div>
  )
}
```

### `registry/default/blocks/social-auth-react-router/app/routes/auth.oauth.tsx`

```tsx
import { redirect, type LoaderFunctionArgs } from 'react-router'

import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'

export async function loader({ request }: LoaderFunctionArgs) {
  const requestUrl = new URL(request.url)
  const code = requestUrl.searchParams.get('code')
  const _next = requestUrl.searchParams.get('next')
  const next = _next?.startsWith('/') ? _next : '/'
  if (code) {
    const { supabase, headers } = createClient(request)

    const { error } = await supabase.auth.exchangeCodeForSession(code)
    if (!error) {
      return redirect(next, { headers })
    } else {
      return redirect(`/auth/error?error=${error?.message}`)
    }
  }
  // redirect the user to an error page with some instructions
  return redirect(`/auth/error`)
}
```

### `registry/default/blocks/social-auth-react-router/app/routes/login.tsx`

```tsx
import { redirect, useFetcher, useSearchParams, type ActionFunctionArgs } from 'react-router'

import { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path'
import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'
import { Button } from '@/registry/default/components/ui/button'
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from '@/registry/default/components/ui/card'

export const action = async ({ request }: ActionFunctionArgs) => {
  const { supabase, headers } = createClient(request)
  const origin = new URL(request.url).origin

  const formData = await request.formData()
  const next = safeNextPath(formData.get('next'), '/protected', origin)

  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: 'github',
    options: {
      redirectTo: `${origin}/auth/oauth?next=${encodeURIComponent(next)}`,
    },
  })

  if (data.url) {
    return redirect(data.url, { headers })
  }

  if (error) {
    return {
      error: error instanceof Error ? error.message : 'An error occurred',
    }
  }
}

export default function Login() {
  const fetcher = useFetcher<typeof action>()
  const [searchParams] = useSearchParams()

  const error = fetcher.data?.error
  const loading = fetcher.state === 'submitting'

  return (
    <div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
      <div className="w-full max-w-sm">
        <div className="flex flex-col gap-6">
          <Card>
            <CardHeader>
              <CardTitle className="font-medium text-lg tracking-normal">Welcome!</CardTitle>
              <CardDescription>Sign in to your account to continue</CardDescription>
            </CardHeader>
            <CardContent>
              <fetcher.Form method="post">
                <input type="hidden" name="next" value={searchParams.get('next') ?? ''} />
                <div className="flex flex-col gap-6">
                  {error && <p className="text-sm text-destructive-500">{error}</p>}
                  <Button type="submit" className="w-full" disabled={loading}>
                    {loading ? 'Signing in...' : 'Continue with GitHub'}
                  </Button>
                </div>
              </fetcher.Form>
            </CardContent>
          </Card>
        </div>
      </div>
    </div>
  )
}
```

### `registry/default/blocks/social-auth-react-router/app/routes/logout.tsx`

```tsx
import { redirect, type ActionFunctionArgs } from 'react-router'

import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'

export async function loader({ request }: ActionFunctionArgs) {
  const { supabase, headers } = createClient(request)

  const { error } = await supabase.auth.signOut()

  if (error) {
    console.error(error)
    return { success: false, error: error.message }
  }

  // Redirect to dashboard or home page after successful sign-in
  return redirect('/', { headers })
}
```

### `registry/default/blocks/social-auth-react-router/app/routes/protected.tsx`

```tsx
import { redirect, useLoaderData, type LoaderFunctionArgs } from 'react-router'

import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'
import { Button } from '@/registry/default/components/ui/button'

export const loader = async ({ request }: LoaderFunctionArgs) => {
  const { supabase } = createClient(request)

  const { data, error } = await supabase.auth.getUser()
  if (error || !data?.user) {
    return redirect('/login')
  }

  return data
}

export default function ProtectedPage() {
  let data = useLoaderData<typeof loader>()

  return (
    <div className="flex items-center justify-center h-screen gap-2">
      <p>
        Hello <span className="text-primary font-semibold">{data.user.email}</span>
      </p>
      <a href="/logout">
        <Button>Sign out</Button>
      </a>
    </div>
  )
}
```

### `registry/default/blocks/social-auth-react-router/app/routes.ts`

```ts
import { type RouteConfig } from '@react-router/dev/routes'
import { flatRoutes } from '@react-router/fs-routes'

export default flatRoutes() satisfies RouteConfig
```

### `registry/default/clients/react-router/lib/supabase/client.ts`

```ts
/// <reference types="vite/types/importMeta.d.ts" />
import { createBrowserClient } from '@supabase/ssr'

export function createClient() {
  return createBrowserClient(
    import.meta.env.VITE_SUPABASE_URL!,
    import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!
  )
}
```

### `registry/default/clients/react-router/lib/supabase/server.ts`

```ts
import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'

export function createClient(request: Request) {
  const headers = new Headers()

  const supabase = createServerClient(
    process.env.VITE_SUPABASE_URL!,
    process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,
    {
      cookies: {
        getAll() {
          return parseCookieHeader(request.headers.get('Cookie') ?? '') as {
            name: string
            value: string
          }[]
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) =>
            headers.append('Set-Cookie', serializeCookieHeader(name, value, options))
          )
        },
      },
    }
  )

  return { supabase, headers }
}
```
