Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Getting Error Failed to launch... scheme does not have a registered handler when configuring Google OAuth with Remix/Supabase #64

Open
1 task done
kevinreber opened this issue Sep 16, 2024 · 0 comments
Labels
bug Something isn't working

Comments

@kevinreber
Copy link

Bug report

Describe the bug

I have a Remix/postgres/supabase app that I am trying to configure Google OAuth with and am getting this error message when I attempt to login

Failed to launch 
'postgres://postgres......:[email protected]:6543/postgres?pgbouncer=true&connection_limit=1/auth/v1/authorize?provider=google&redirect_to=http%3A%2F%2Flocalhost%3A5173%2Fauth%2Fcallback&code_challenge=v9WEmEh6ZBa132TGXH-_IfxZtXJCf9SYWsiruyaoniA&code_challenge_method=s256' 
because the scheme does not have a registered handler.

CleanShot 2024-09-16 at 01 23 53@2x

It seems like there may be a missing configuration or step I missed... I followed both of these Supabase docs when configuring Google Oath and Supabase

I've confirmed

  • My environment variables are being passed from server to client
  • That I have Google OAuth and Supabase in sync (screenshots below - OAuth login with Google)

CleanShot 2024-09-16 at 01 02 06@2x

CleanShot 2024-09-16 at 00 53 34@2x

My current setup

package.json

{
  "private": true,
  "sideEffects": false,
  "type": "module",
  "scripts": {
    "build": "remix vite:build",
    "dev": "remix vite:dev",
    "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
    "start": "remix-serve ./build/server/index.js",
    "typecheck": "tsc",
    "postinstall": "prisma generate"
  },
  "dependencies": {
    "@paralleldrive/cuid2": "^2.2.2",
    "@prisma/client": "^5.19.0",
    "@radix-ui/react-avatar": "^1.1.0",
    "@radix-ui/react-dropdown-menu": "^2.1.1",
    "@remix-run/express": "^2.11.2",
    "@remix-run/node": "^2.11.2",
    "@remix-run/react": "^2.11.2",
    "@supabase/ssr": "^0.5.1",
    "@supabase/supabase-js": "^2.45.4",
    "@vercel/remix": "^2.11.2",
    "bcryptjs": "^2.4.3",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.1",
    "cross-env": "^7.0.3",
    "date-fns": "^3.6.0",
    "express": "^4.19.2",
    "isbot": "^4.4.0",
    "lucide-react": "^0.435.0",
    "prisma": "^5.18.0",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "remix-auth": "^3.7.0",
    "remix-auth-google": "^2.0.0",
    "remix-utils": "^7.6.0",
    "sonner": "^1.5.0",
    "tailwind-merge": "^2.5.2",
    "tailwindcss-animate": "^1.0.7",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@playwright/test": "^1.46.1",
    "@remix-run/dev": "^2.11.2",
    "@types/node": "^22.5.0",
    "@types/react": "^18.2.20",
    "@types/react-dom": "^18.2.7",
    "@typescript-eslint/eslint-plugin": "^6.7.4",
    "@typescript-eslint/parser": "^6.7.4",
    "autoprefixer": "^10.4.19",
    "eslint": "^8.38.0",
    "eslint-import-resolver-typescript": "^3.6.1",
    "eslint-plugin-import": "^2.28.1",
    "eslint-plugin-jsx-a11y": "^6.7.1",
    "eslint-plugin-react": "^7.33.2",
    "eslint-plugin-react-hooks": "^4.6.0",
    "postcss": "^8.4.38",
    "tailwindcss": "^3.4.4",
    "typescript": "^5.1.6",
    "vite": "^5.4.2",
    "vite-tsconfig-paths": "^4.2.1"
  },
  "engines": {
    "node": ">=20.0.0"
  }
}

supabase.server.ts

import {
  createServerClient,
  parseCookieHeader,
  serializeCookieHeader,
} from "@supabase/ssr";

export const getSupabaseEnv = () => ({
  DATABASE_URL: process.env.DATABASE_URL!,
  DATABASE_ANON_KEY: process.env.DATABASE_ANON_KEY!,
});

export async function getSupabaseWithSessionAndHeaders({
  request,
}: {
  request: Request;
}) {
  const headers = new Headers();
  const { DATABASE_URL, DATABASE_ANON_KEY } = getSupabaseEnv();

  const supabase = createServerClient(DATABASE_URL!, DATABASE_ANON_KEY!, {
    cookies: {
      getAll() {
        return parseCookieHeader(request.headers.get("Cookie") ?? "");
      },
      setAll(cookiesToSet) {
        cookiesToSet.forEach(({ name, value, options }) =>
          headers.append("Set-Cookie", serializeCookieHeader(name, value, options))
        );
      },
    },
  });

  const {
    data: { session: serverSession },
  } = await supabase.auth.getSession();

  return { serverSession, headers, supabase };
}

useSupabaseClient.ts

import React from "react";
import { useRevalidator } from "@remix-run/react";
import { createBrowserClient } from "@supabase/ssr";
import type { Session } from "@supabase/supabase-js";

type SupabaseEnv = {
  SUPABASE_URL: string;
  SUPABASE_ANON_KEY: string;
};

type UseSupabase = {
  env: SupabaseEnv;
  serverSession: Session | null;
};

export const useSupabaseClient = ({ env, serverSession }: UseSupabase) => {
  const [supabase] = React.useState(() =>
    createBrowserClient(env.SUPABASE_URL, env.SUPABASE_ANON_KEY)
  );

  const serverAccessToken = serverSession?.access_token;
  const revalidator = useRevalidator();

  React.useEffect(() => {
    const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
      if (session?.access_token !== serverAccessToken) {
        // Revalidate the app.
        revalidator.revalidate();
      }
    });

    return () => {
      subscription.unsubscribe();
    };
  }, [supabase.auth, serverAccessToken, revalidator]);

  return { supabase };
};

root.tsx

import React from "react";
import { ... } from "@remix-run/react";
import { ... } from "@remix-run/node";
import { getSupabaseEnv, getSupabaseWithSessionAndHeaders } from "./services/supabase.server";
import { useSupabaseClient } from "./hooks/useSupabaseClient";

export async function loader({ request }: LoaderFunctionArgs) {
  const { serverSession, headers: supabaseHeaders } = await getSupabaseWithSessionAndHeaders({ request });

  return json(
    {
      DATABASE_URL: process.env.DATABASE_URL || '',
      DATABASE_ANON_KEY: process.env.DATABASE_ANON_KEY || '',
      serverSession,
      domainUrl: process.env.ORIGIN || '',
    },
    {
      headers: supabaseHeaders,
    }
  );
}

export default function App() {
  const data = useLoaderData<typeof loader>();
  const { supabase } = useSupabaseClient({
    env: {
      SUPABASE_URL: data.DATABASE_URL,
      SUPABASE_ANON_KEY: data.DATABASE_ANON_KEY,
    },
    serverSession: data.serverSession,
  });

  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <ScrollRestoration />
        <Scripts />
        <Outlet context={{ supabase, domainUrl: data.domainUrl }} />
      </body>
    </html>
  );
}

login.tsx

import { ... } from "@remix-run/react";
import { ... } from "@remix-run/node";
import { getSupabaseWithSessionAndHeaders } from "~/services/supabase.server";

export async function loader({ request }: LoaderFunctionArgs) {
  const { headers, serverSession } = await getSupabaseWithSessionAndHeaders({ request });

  if (serverSession) {
    return redirect('/explore', { headers });
  }

  return json({ success: true });
}

export default function Index() {
  const { supabase, domainUrl } = useOutletContext<{ supabase: Record<string, any>; domainUrl: string }>();

  const handleSignIn = async () => {
    try {
      await supabase.auth.signInWithOAuth({
        provider: 'google',
        options: {
          redirectTo: `${domainUrl}/auth/callback`,
        },
      });
    } catch (error) {
      console.error('Error signing in with Google:', error);
    }
  };

  return (
    <button onClick={handleSignIn}>
      Google Login
    </button>
  );
}

auth.callback.tsx

import React from 'react';
import { redirect, type LoaderFunctionArgs } from '@remix-run/node';
import { json, useOutletContext } from '@remix-run/react';
import { createServerClient } from '@supabase/ssr';

export async function loader({ request }: LoaderFunctionArgs) {
  const url = new URL(request.url);
  const code = url.searchParams.get('code');
  const response = new Response();

  if (code) {
    const supabaseClient = createServerClient(process.env.DATABASE_URL!, process.env.DATABASE_ANON_KEY!, { request, response });
    
    await supabaseClient.auth.exchangeCodeForSession(code);
  }

  return redirect('/', { headers: response.headers });
}

export default function Index() {
  const { supabase } = useOutletContext<{ supabase: Record<string, any> }>();

  React.useEffect(() => {
    const handleAuth = async () => {
      const { error } = await supabase.auth.getSessionFromUrl();

      if (error) {
        console.error('Error retrieving session:', error.message);
      } else {
        window.location.replace('/'explore);
      }
    };

    handleAuth();
  }, [supabase]);

  return <div>Logging you in...</div>;
}

Expected behavior

I should be redirected to a google login screen

System information

  • Version of supabase-js: 0.5.1
  • Version of Node.js: 20.0.0

Additional context

Documents used for setup

Supabase Resources

Other Resources

@kevinreber kevinreber added the bug Something isn't working label Sep 16, 2024
@kevinreber kevinreber changed the title Getting Error scheme does not have a registered handler when configuring Google OAuth with Remix/Supabase Getting Error Failed to launch... scheme does not have a registered handler when configuring Google OAuth with Remix/Supabase Sep 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

1 participant