Supabase Auth with Plasmic RBAC using Plasmic Codegen

This is sharing of integrating Supabase Auth using Plasmic Codegen in Plasmic. This is based on code in plasmic/examples/supabase-auth-nextjs-pages-codegen at master · plasmicapp/plasmic · GitHub

Supabase Auth accessible from Global Action in Plasmic Studio:

Sign In Action:

Sign Up Action:

Forgot Password Trigger: (call this first as Supabase will sent email with reset link)

Reset Password: (the action to enter new password)

Response and Error Message from Supabase Auth:

[1] npx create-plasmic-app@latest (creating new project!)
[1][0]-[ x] follow the flow. (Make sure to select NEXTJS, CODEGEN, Typescript.)

[2] Codes/ Folder to copy
[2][1] components/PasswordAuth.tsx

import React, { useState, useMemo } from "react";
import { createPagesBrowserClient } from "@supabase/auth-helpers-nextjs";
import { mutate } from "swr";
import { PLASMIC_AUTH_DATA_KEY } from "../utils/cache-keys";
import { GlobalActionsProvider, DataProvider } from "@plasmicapp/host";
import { useRouter } from "next/router";

const PasswordAuth: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const supabaseClient = createPagesBrowserClient();
  const [latestResponse, setLatestResponse] = useState<any>(null);
  const [authError, setAuthError] = useState<{ error: string } | null>(null);
  const router = useRouter();

  const actions = useMemo(
    () => ({
      signIn: async (email: string, password: string, redirectUrl?: string) => {
        try {
          const { data, error } = await supabaseClient.auth.signInWithPassword({ email, password });
          await mutate(PLASMIC_AUTH_DATA_KEY);

          if (error) {
            setAuthError({
              error: error.message || "unknown_error",
            });
          } else {
            setLatestResponse(data);
            setAuthError(null);
            if (redirectUrl) router.push(redirectUrl);
          }

          return { data, error };
        } catch (err: any) {
          console.error("Sign In Error:", err);
          setAuthError({
            error: err.name || "unknown_error",
          });
          throw err;
        }
      },

      signUp: async (email: string, password: string, roleId?: string, redirectUrl?: string) => {
        try {
          const { data, error } = await supabaseClient.auth.signUp({ email, password });
          await mutate(PLASMIC_AUTH_DATA_KEY);

          if (error) {
            setAuthError({
              error: error.message || "unknown_error",
            });
          } else {
            const plasmicAuthDataResponse = await fetch("/api/plasmic-auth", {
              method: "POST",
              headers: {
                "Content-Type": "application/json",
              },
              body: JSON.stringify({ action: "signUp", roleId }),
            });

            const plasmicAuthData = await plasmicAuthDataResponse.json();
            console.log("Plasmic Auth Data:", plasmicAuthData);

            setLatestResponse(data);
            setAuthError(null);
            if (redirectUrl) router.push(redirectUrl);
          }

          return { data, error };
        } catch (err: any) {
          console.error("Sign Up Error:", err);
          setAuthError({
            error: err.name || "unknown_error",
          });
          throw err;
        }
      },

      signOut: async (redirectUrl?: string) => {
        try {
          const { error } = await supabaseClient.auth.signOut();
          await mutate(PLASMIC_AUTH_DATA_KEY);

          if (error) {
            setAuthError({
              error: error.message || "unknown_error",
            });
          } else {
            setLatestResponse(null);
            setAuthError(null);
            if (redirectUrl) router.push(redirectUrl);
          }

          return { error };
        } catch (err: any) {
          console.error("Sign Out Error:", err);
          setAuthError({
            error: err.name || "unknown_error",
          });
          throw err;
        }
      },

      forgotPassword: async (email: string, redirectUrl?: string) => {
        try {
          const origin = typeof window !== "undefined" ? window.location.origin : "";
          const { data, error } = await supabaseClient.auth.resetPasswordForEmail(email, {
            redirectTo: redirectUrl || `${origin}/reset-password`,
          });

          if (error) {
            setAuthError({
              error: error.message || "unknown_error",
            });
          } else {
            setLatestResponse(data);
            setAuthError(null);
          }

          return { data, error };
        } catch (err: any) {
          console.error("Forgot Password Error:", err);
          setAuthError({
            error: err.name || "unknown_error",
          });
          throw err;
        }
      },

      updatePassword: async (newPassword: string, redirectUrl: string = "/sign-in") => {
        try {
          // Now you can safely use the extracted token without the "pkce_" prefix
          const { data, error } = await supabaseClient.auth.updateUser( {
            password: newPassword, // The new password to update
          });
      
          if (error) {
            setAuthError({ error: error.message || "unknown_error" });
          } else {
            setLatestResponse(data);
            setAuthError(null);
            router.push(redirectUrl); // Redirect to the sign-in page after successful password update
          }
      
          return { data, error };
        } catch (err: any) {
          console.error("Update Password Error:", err);
          setAuthError({ error: err.message || "unknown_error" });
          throw err;
        }
      },
    }),
    [supabaseClient, router]
  );

  return (
    <GlobalActionsProvider contextName="PasswordAuth" actions={actions}>
      <DataProvider name="supabaseAuth" data={{ latestResponse, authError }}>
        {children}
      </DataProvider>
    </GlobalActionsProvider>
  );
};

export { PasswordAuth };

[2][2] pages/api/plasmic-auth.ts

import { createPagesServerClient } from "@supabase/auth-helpers-nextjs";
import type { NextApiRequest, NextApiResponse } from "next";
import { getPlasmicAuthData } from "../../utils/plasmic-auth";

// This API endpoint is used to provide the Plasmic user in client-side code.
export default async function getPlasmicAuthDataHandler(
  req: NextApiRequest,
  res: NextApiResponse
): Promise<void> {
  try {
    const { roleId } = req.body;

    const supabaseServerClient = createPagesServerClient({ req, res });

    const plasmicAuthData = await getPlasmicAuthData(supabaseServerClient, roleId);

    res.status(200).json(plasmicAuthData);
  } catch (error) {
    console.error("Error in API handler:", error);
    res.status(500).json({ error: "Internal Server Error" });
  }
}

[2][3] pages/_app.tsx (just copy the relevant portion if you had changes made prior)

import '@/styles/globals.css'
import { PlasmicRootProvider } from "@plasmicapp/react-web";
import type { AppProps } from "next/app";
import Head from "next/head";
import { usePlasmicAuthData } from "../utils/usePlasmicAuth";

function PlasmicRootProviderWithUser(props: { children: React.ReactNode }) {
  const { isUserLoading, plasmicUser, plasmicUserToken } = usePlasmicAuthData();

  return (
    <PlasmicRootProvider
      Head={Head}
      isUserLoading={isUserLoading}
      user={plasmicUser}
      userAuthToken={plasmicUserToken}
      disableLoadingBoundary={true}
    >
      {props.children}
    </PlasmicRootProvider>
  );
}

export default function MyApp({ Component, pageProps }: AppProps) {
  if (pageProps.withoutUseAuth) {
    return (
      <PlasmicRootProvider Head={Head}>
        <Component {...pageProps} />
      </PlasmicRootProvider>
    );
  }
  return (
    <PlasmicRootProviderWithUser>
      <Component {...pageProps} />
    </PlasmicRootProviderWithUser>
  );
}

[2][4] pages/plasmic-host.tsx (copy the “registerGlobalContext(PasswordAuth…” and to import the component also)

import * as React from 'react';
import { PlasmicCanvasHost, registerGlobalContext, registerComponent } from '@plasmicapp/react-web/lib/host';
import {PasswordAuth} from '../components/PasswordAuth'

// You can register any code components that you want to use here; see
// https://docs.plasmic.app/learn/code-components-ref/
// And configure your Plasmic project to use the host url pointing at
// the /plasmic-host page of your nextjs app (for example,
// http://localhost:3000/plasmic-host).  See
// https://docs.plasmic.app/learn/app-hosting/#set-a-plasmic-project-to-use-your-app-host

// registerComponent(...)

registerGlobalContext(PasswordAuth, {
  name: "PasswordAuth",
  props: {},
  providesData: true,
  globalActions: {
    signIn: {
      parameters: [
        { name: "email", type: "string" },
        { name: "password", type: "string" },
        { name: "redirectUrl", type: "string" },
      ],
    },
    signUp: {
      parameters: [
        { name: "email", type: "string" },
        { name: "password", type: "string" },
        { name: "roleId", type: "string" },
        { name: "redirectUrl", type: "string" },
      ],
    },
    signOut: { 
      parameters:[
        { name: "redirectUrl", type: "string" },
      ],
     },
    forgotPassword: {
      parameters: [
        { name: "email", type: "string" },
        { name: "redirectUrl", type: "string" },
      ],
    },
    updatePassword: {
      parameters: [
        { name: "newPassword", type: "string" },
        { name: "redirectUrl", type: "string" },
      ],
    },
  },
  importPath: "./components/PasswordAuth",
});

export default function PlasmicHost() {
  return <PlasmicCanvasHost />;
}

[2][5] utils folder (create utils folder in your roots and copy over all 3 files from plasmic/examples/supabase-auth-nextjs-pages-codegen at master · plasmicapp/plasmic · GitHub)

[2][6] .env.local (create this file in your root directory and update the following variables accordingly)
[2][6][1] NEXT_PUBLIC_SUPABASE_URL=your-supabase-url
[2][6][2] NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key
[2][6][3] PLASMIC_AUTH_SECRET=your-plasmic-auth-secret (Authentication > Settings > Custom Auth > Token)

[2][7] middleware.ts (create this in your root directory. In this code place those web pages that are limited by access to public to protect it from public access).

import { NextRequest, NextResponse } from "next/server";
import { createMiddlewareClient } from "@supabase/auth-helpers-nextjs";

export default async function middleware(request: NextRequest): Promise<NextResponse> {
  // Initialize Supabase client (res is omitted in middleware context)
  const supabase = createMiddlewareClient({
    req: request,
    res: NextResponse.next(), // Provide a placeholder NextResponse
  });

  try {
    // Get the user's session from Supabase
    const {
      data: { session },
    } = await supabase.auth.getSession();

    // Check if the user is signed in
    if (!session) {
      // Redirect to the homepage if unauthenticated
      return NextResponse.redirect(new URL("/", request.url));
    }

    // Allow the request to proceed
    return NextResponse.next();
  } catch (error) {
    console.error("Middleware error:", error);
    // Redirect to an error page if something goes wrong
    return NextResponse.redirect(new URL("/error", request.url));
  }
}

// List of all secured/ private pages 
export const config = {
  matcher: ["/private-page" , "/secured-page"], // Applies to routes matching "/article"
};

[3] install dependencies
[3][1] npm install @plasmicapp/auth-api
[3][2] npm install @supabase/auth-helpers-nextjs (WARNING! Supabase has deprecated this. Use at your own risks.)
[3][3] npm i swr

[0] Reference
[0][1] New Plasmic codebase (CLI) | Learn Plasmic
[0][2] Auth integration | Learn Plasmic
[0][3] JavaScript API Reference | Supabase Docs
[0][4] Getting Started – SWR
[0][5] Password-based Auth | Supabase Docs
[0][6] plasmic/packages/auth-api at fca35655e7b175e26560ca009cb398060a6ab0d4 · plasmicapp/plasmic · GitHub
[0][7] Sign Up problem
[0][8] plasmic/examples/supabase-auth-nextjs-pages-codegen at master · plasmicapp/plasmic · GitHub
[0][9] Auth integration | Learn Plasmic
[10][0] File Conventions: middleware.js | Next.js

Hey @papan_digital, thanks for the step by step integration.

Could you update the post to use “```ts” instead of “>” for the code parts? This way it doesnt lose indentation.

Yes, @icaro_guerra Sir!

Every time I want to secure a page and re-route accordingly I have to manually entered it in the middleware like such

Is there a way where I can set a page as secured in the Plasmic Studio and the path is then automatically updated to the middleware?

Hi, you can add arbitrary data in the more metadata section of a page, but there is no good way to consume it in the middleware :frowning:.

Hey thanks for this setup! It really helped me after countless hours looking for custom auth setups. The only thing I’m not sure of now is how to store the uid from Supabase to current user’s externalId/userId. What would I need to change from this setup so that I can reference the user id in queries.

1 Like

I think you need to create your own database to store the information. You may refer to documentation at User authentication & permissions | Learn Plasmic

Yea, after checking the code it ended up being pretty easy. I just added this line:
“externalId: user?.id”

const result = await ensurePlasmicAppUser({
      email: user?.email,
      appSecret: PLASMIC_AUTH_SECRET!,
      externalId: user?.id
    });