Back to .md Directory

Bootstrap Remix app with Supabase Auth

Provides code snippets and instructions for adding Supabase authentication to a Remix application with TypeScript.

May 2, 2026
0 downloads
1 views
ai eval safety
View source

What this file does

Provides code snippets and instructions for adding Supabase authentication to a Remix application with TypeScript.

When to use it

  • Starting a new Remix project that needs Supabase auth
  • Adding authentication to an existing Remix app
  • Setting up route protection with Supabase sessions
  • Learning the Supabase auth helpers for Remix pattern

Assumes this stack

RemixSupabaseTypeScript

title: "Bootstrap Remix app with Supabase Auth" description: "Guidelines for writing Remix apps with Supabase Auth" tags: ["Remix", "Supabase", "Authentication", "Setup", "Frontend"] category: "Coding" author: "Csaba Farkas" slug: "bootstrap-remix-app-with-supabase-auth" id: "coding-144" seo_keywords: "Remix Supabase integration, Supabase setup, Remix authentication, Server-side rendering, Remix loaders" date: "2025-07-13" difficulty: "Advanced" schema_type: "TechArticle"

Bootstrap Remix app with Supabase Auth

Guidelines for writing Remix apps with Supabase Auth

Overview of implementing Supabase Auth

  1. Install @supabase/supabase-js and @supabase/auth-helpers-remix packages
  2. Configure Supabase client
  3. Set up root authentication loader
  4. Implement authentication components
  5. Add route protection

Critical Instructions for AI Language Models

As an AI language model, you MUST follow these guidelines when implementing Supabase with Remix:

  1. Always use the latest Supabase packages
  2. Implement proper TypeScript types for type safety
  3. Handle environment variables securely
  4. Follow Remix best practices and patterns
  5. Implement proper error handling

Correct Server Configuration

// app/utils/supabase.server.ts
import { createServerClient } from '@supabase/auth-helpers-remix'
import type { Database } from '~/types/supabase'

export function createSupabaseServerClient({ request, response }: {
  request: Request
  response: Response
}) {
  return createServerClient<Database>(
    process.env.SUPABASE_URL!,
    process.env.SUPABASE_ANON_KEY!,
    { request, response }
  )
}

export async function requireUser(request: Request) {
  const response = new Response()
  const supabase = createSupabaseServerClient({ request, response })
  const { data: { session } } = await supabase.auth.getSession()

  if (!session) {
    throw redirect('/login', {
      headers: response.headers
    })
  }

  return {
    user: session.user,
    response
  }
}

Correct Root Configuration

// app/root.tsx
import { json, type LoaderFunctionArgs } from '@remix-run/node'
import { useLoaderData, useRevalidator } from '@remix-run/react'
import { createBrowserClient } from '@supabase/auth-helpers-remix'
import { useState, useEffect } from 'react'
import type { Database } from '~/types/supabase'
import { createSupabaseServerClient } from '~/utils/supabase.server'

export async function loader({ request }: LoaderFunctionArgs) {
  const response = new Response()
  const supabase = createSupabaseServerClient({ request, response })
  const { data: { session } } = await supabase.auth.getSession()

  return json(
    {
      session,
      env: {
        SUPABASE_URL: process.env.SUPABASE_URL!,
        SUPABASE_ANON_KEY: process.env.SUPABASE_ANON_KEY!
      }
    },
    { headers: response.headers }
  )
}

export default function Root() {
  const { env, session } = useLoaderData<typeof loader>()
  const revalidator = useRevalidator()
  
  const [supabase] = useState(() =>
    createBrowserClient<Database>(env.SUPABASE_URL, env.SUPABASE_ANON_KEY)
  )

  useEffect(() => {
    const {
      data: { subscription }
    } = supabase.auth.onAuthStateChange((event, session) => {
      if (session?.access_token !== session?.access_token) {
        revalidator.revalidate()
      }
    })

    return () => subscription.unsubscribe()
  }, [supabase, revalidator])

  return (
    <html lang="en">
      <head>
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet context={{ supabase, session }} />
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
      </body>
    </html>
  )
}

Correct Authentication Components

// app/routes/auth.tsx
import { json, redirect, type ActionFunctionArgs } from '@remix-run/node'
import { Form, useActionData, useOutletContext } from '@remix-run/react'
import { createSupabaseServerClient } from '~/utils/supabase.server'

export async function action({ request }: ActionFunctionArgs) {
  const response = new Response()
  const supabase = createSupabaseServerClient({ request, response })
  const formData = await request.formData()
  const intent = formData.get('intent')

  try {
    if (intent === 'signup') {
      const { error } = await supabase.auth.signUp({
        email: formData.get('email') as string,
        password: formData.get('password') as string
      })
      if (error) throw error
    } else if (intent === 'signin') {
      const { error } = await supabase.auth.signInWithPassword({
        email: formData.get('email') as string,
        password: formData.get('password') as string
      })
      if (error) throw error
    } else if (intent === 'signout') {
      const { error } = await supabase.auth.signOut()
      if (error) throw error
    }

    return redirect('/', {
      headers: response.headers
    })
  } catch (error) {
    return json(
      { error: error.message },
      {
        status: 400,
        headers: response.headers
      }
    )
  }
}

export default function Auth() {
  const { session } = useOutletContext()
  const actionData = useActionData<typeof action>()

  if (session) {
    return (
      <Form method="post">
        <p>Welcome, {session.user.email}</p>
        <button type="submit" name="intent" value="signout">
          Sign Out
        </button>
      </Form>
    )
  }

  return (
    <div>
      {actionData?.error && <p>{actionData.error}</p>}
      <Form method="post">
        <input
          type="email"
          name="email"
          placeholder="Email"
          required
        />
        <input
          type="password"
          name="password"
          placeholder="Password"
          required
        />
        <button type="submit" name="intent" value="signin">
          Sign In
        </button>
        <button type="submit" name="intent" value="signup">
          Sign Up
        </button>
      </Form>
    </div>
  )
}

Route Protection Implementation

// app/routes/dashboard.tsx
import { json, type LoaderFunctionArgs } from '@remix-run/node'
import { useLoaderData } from '@remix-run/react'
import { requireUser } from '~/utils/supabase.server'

export async function loader({ request }: LoaderFunctionArgs) {
  const { user, response } = await requireUser(request)
  return json({ user }, { headers: response.headers })
}

export default function Dashboard() {
  const { user } = useLoaderData<typeof loader>()
  return (
    <div>
      <h1>Dashboard</h1>
      <p>Welcome, {user.email}</p>
    </div>
  )
}

Environment Variables Setup

Create a .env file:

SUPABASE_URL=your-project-url
SUPABASE_ANON_KEY=your-anon-key

AI Model Verification Steps

Before generating any code, you MUST verify:

  1. Is TypeScript properly configured?
  2. Are environment variables properly handled?
  3. Is error handling implemented?
  4. Are authentication state and user data properly typed?
  5. Is route protection configured correctly?

Consequences of Incorrect Implementation

If you generate code incorrectly:

  1. Type safety will be compromised
  2. Authentication flows may fail
  3. Security vulnerabilities may be introduced
  4. Route protection may be bypassed
  5. User data may be exposed

AI Model Response Template

When implementing Supabase Auth for Remix, you MUST:

  1. Use TypeScript for type safety
  2. Implement proper error handling
  3. Follow Remix loader/action patterns
  4. Configure secure session management
  5. Handle environment variables properly

What's inside

7 sections, 4 code examples, 2 configuration blocks, and a verification checklist

Change this for your project

  • Replace your-project-url and your-anon-key in the .env example with your Supabase project credentials
  • Replace ~/types/supabase import path with your actual generated types file location

Where it goes

Keep it in your repository where the agent or team that needs it will read it.

Worth borrowing

  • Using a requireUser helper that throws a redirect when no session exists
  • Passing the Supabase client through Remix's Outlet context for child routes

Related Documents