Components that make it easy to use Supabase as a backend for your Plasmic app.
These components allow you to use the auto-generated Supabase API for database, storage & auth, so you can leverage all of Supabase's powerful features in your Plasmic app. Note that this is DIFFERENT from the built-in Plasmic supabase integration which uses direct database connection.
These components support use of Supabase auth without Plasmic auth.
Need help with your project? Contact one of the contributors using their contact details above.
We provide general support for this package, as well as paid coaching & development in Plasmic & Supabase.
You can find the changelog for this project here
Important note: this repo currently only works with a Plasmic project that uses the NextJS pages router with the Loader API.
Support for NextJS pages router with codegen will be added later.
This sections covers how to create a new Plasmic project and make the plasmic-supabase
component available in the project.
After completing this section, you will be able to use the plasmic-supabase
components in your Plasmic project to:
However, you will NOT yet be able to limit access to pages based on user authentication status. This is covered in the next section.
In the Plasmic web interface:
On your local machine:
npm install
to install plasmic & it's dependencies# Supabase Project
NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
npm install plasmic-supabase
to install this package./plasmic-init.ts
. It should look like this to start with (default Plasmic comments removed for brevity)
import { initPlasmicLoader } from "@plasmicapp/loader-nextjs";
export const PLASMIC = initPlasmicLoader({ projects: [ { id: "your-plasmic-project-id", token: "your-plasmic-project-token", }, ],
preview: false, });
6. Modify `plasmic-init.ts` to import components from `plasmic-supabase`
```ts
import { initPlasmicLoader } from "@plasmicapp/loader-nextjs";
import {
SupabaseProvider,
SupabaseProviderMeta,
SupabaseUserGlobalContext,
SupabaseUserGlobalContextMeta,
SupabaseUppyUploader,
SupabaseUppyUploaderMeta,
SupabaseStorageGetSignedUrl,
SupabaseStorageGetSignedUrlMeta,
} from "plasmic-supabase"
export const PLASMIC = initPlasmicLoader({
projects: [
{
id: "your-plasmic-project-id",
token: "your-plasmic-project-token",
},
],
preview: true,
});
//Register global context
PLASMIC.registerGlobalContext(SupabaseUserGlobalContext, SupabaseUserGlobalContextMeta)
//Register components
PLASMIC.registerComponent(SupabaseProvider, SupabaseProviderMeta);
PLASMIC.registerComponent(SupabaseUppyUploader, SupabaseUppyUploaderMeta);
PLASMIC.registerComponent(SupabaseStorageGetSignedUrl, SupabaseStorageGetSignedUrlMeta);
./pages
directory add a new file called _app.tsx
and add the following content. Save your file
import type { AppProps } from 'next/app';
//Import the CSS required for SupabaseUppyUploader globally import "@uppy/core/dist/style.min.css"; import "@uppy/dashboard/dist/style.min.css";
function MyApp({ Component, pageProps }: AppProps) { return <Component {...pageProps} />; }
export default MyApp;
8. In terminal: `npm run dev` to start your Dev server
### 03 - Configure custom app host
In Plasmic studio:
1. Configure you Custom App host to point to http://localhost:3000/plasmic-host
2. When the page reloads, the registered components should be available in Add component -> Custom Components. You'll also see global actions available for login/logout etc & a global context value of logged in SupabaseUser.
### 04 - Add login and logout functionality
In Plasmic studio:
1. Create a login page
* Create page at path `/login`.
* Add a form component to the page
* Configure the form fields so it contains an email and password input
* In Plasmic studio, top right next to the triangle button, click "view" and select "Turn off design mode"
* Turn on `Interactive` mode in the studio
* Fill in the form with a valid email & password of a Supabase user in your Supabase project but **don't submit it yet**
* Attach an interaction to the form for `onSubmit`:
* Action 1: `SupabaseUserGlobalContext -> login`. Fill in the fields that appear (`Email` and `Password`) with the dynamic values from the form: `form.value.email` & `form.value.password`. Also fill in the `Success redirect` field with the home page `/`
* Close the form configuration popups and submit the login form with a valid email & password. You should have logged in but won't yet be able to tell.
2. Check that login worked by showing logged in user email on the home page
* Ensure you've already logged in while viewing your app in Plasmic studio (see previous step 2)
* Go to your project's home `/` page using the page dropdown in Plasmic studio.
* Click the "refresh arena" button (because Plasmic studio caches page context between visits so login status sometimes will not be available until you refresh the arena)
* Add a text element to the page
* Assign dynamic content to the text element and pick `SupabaseUser.user.email` with fallback "You are not logged in"
* If login succeeded in step 2, you should see the logged in user's email address on the page
3. Add a logout button to the home page
* Add a button to the homepage of your app
* Change the button text to "Logout"
* Attach an interaction to the button: `onClick`:
* Action 1: `SupabaseUserGlobalContext -> logout`. Leave the `Success redirect` field blank
4. Check that you can log out
* Make sure you are currently logged in (see step 1 & 2) and have added a logout button to the homepage (see step 3)
* Turn on `Interactive` mode in the studio
* Click the logout button
* If logout succeeded, you should no longer see the logged in user's email address on the page. Instead you should see the fallback content from your text block "You are not logged in"
### 05 - Test that you can access your Supabase database
In Plasmic studio:
1. Create a new page
2. Add a `SupabaseProvider` component to the page
3. Configure the `SupabaseProvider` component as per the on-screen instructions
4. Add a text element inside the `SupabaseProvider` component
5. Assign a dynamic value provided by the `SupabaseProvider` to this text element.
6. If everything worked, you'll see a real value from your database on the page!
You're now done with basic setup!
## Login-protecting pages in your app
The previous section allowed you to login and logout, however we don't yet have a way to prevent non-logged-in users from accessing certain pages of our app.
In this section, we'll fix this issue so that we can define both public and login-protected pages in our app.
1. On your local machine (cloned repo from github, see previous section `02 - Download & modify your project code`):
1. delete the file `pages/[[...catchall]].tsx`
2. Create a new file `pages/index.tsx` with this content
<details>
<summary>
<strong>
Content of pages/index.tsx
</strong>
</summary>
```tsx
// ./pages/index.tsx
// Load & render the '/' (home) page from Plasmic studio
// we do this outside of normal catchall routes so it can be publicly accessible without having '/public/' at front of route path
const pageToLoad = '/';
import * as React from "react";
import {
PlasmicComponent,
extractPlasmicQueryData,
ComponentRenderData,
PlasmicRootProvider,
} from "@plasmicapp/loader-nextjs";
import type { GetStaticProps } from "next";
import Error from "next/error";
import { useRouter } from "next/router";
import { PLASMIC } from "@/plasmic-init";
export default function PlasmicLoaderPage(props: {
plasmicData?: ComponentRenderData;
queryCache?: Record<string, any>;
}) {
const { plasmicData, queryCache } = props;
const router = useRouter();
if (!plasmicData || plasmicData.entryCompMetas.length === 0) {
return <Error statusCode={404} />;
}
const pageMeta = plasmicData.entryCompMetas[0];
return (
<PlasmicRootProvider
loader={PLASMIC}
prefetchedData={plasmicData}
prefetchedQueryData={queryCache}
pageParams={pageMeta.params}
pageQuery={router.query}
>
<PlasmicComponent component={pageMeta.displayName} />
</PlasmicRootProvider>
);
}
export const getStaticProps: GetStaticProps = async () => {
const plasmicPath = pageToLoad;
const plasmicData = await PLASMIC.maybeFetchComponentData(plasmicPath);
if (!plasmicData) {
// non-Plasmic catch-all
return { props: {} };
}
const pageMeta = plasmicData.entryCompMetas[0];
// Cache the necessary data fetched for the page
const queryCache = await extractPlasmicQueryData(
<PlasmicRootProvider
loader={PLASMIC}
prefetchedData={plasmicData}
pageParams={pageMeta.params}
>
<PlasmicComponent component={pageMeta.displayName} />
</PlasmicRootProvider>
);
// Use revalidate if you want incremental static regeneration
return { props: { plasmicData, queryCache }, revalidate: 60 };
}
</details>
3. Create a new file `pages/login.tsx` with this content
<details>
<summary>
<strong>
Content of pages/login.tsx
</strong>
</summary>
```tsx
// ./pages/login.tsx
// Load & render the '/login' page from Plasmic studio
// we do this outside of normal catchall routes so it can be publicly accessible without having '/public/' at front of route path
const pageToLoad = '/login';
import * as React from "react";
import {
PlasmicComponent,
extractPlasmicQueryData,
ComponentRenderData,
PlasmicRootProvider,
} from "@plasmicapp/loader-nextjs";
import type { GetStaticProps } from "next";
import Error from "next/error";
import { useRouter } from "next/router";
import { PLASMIC } from "@/plasmic-init";
export default function PlasmicLoaderPage(props: {
plasmicData?: ComponentRenderData;
queryCache?: Record<string, any>;
}) {
const { plasmicData, queryCache } = props;
const router = useRouter();
if (!plasmicData || plasmicData.entryCompMetas.length === 0) {
return <Error statusCode={404} />;
}
const pageMeta = plasmicData.entryCompMetas[0];
return (
<PlasmicRootProvider
loader={PLASMIC}
prefetchedData={plasmicData}
prefetchedQueryData={queryCache}
pageParams={pageMeta.params}
pageQuery={router.query}
>
<PlasmicComponent component={pageMeta.displayName} />
</PlasmicRootProvider>
);
}
export const getStaticProps: GetStaticProps = async () => {
const plasmicPath = pageToLoad;
const plasmicData = await PLASMIC.maybeFetchComponentData(plasmicPath);
if (!plasmicData) {
// non-Plasmic catch-all
return { props: {} };
}
const pageMeta = plasmicData.entryCompMetas[0];
// Cache the necessary data fetched for the page
const queryCache = await extractPlasmicQueryData(
<PlasmicRootProvider
loader={PLASMIC}
prefetchedData={plasmicData}
pageParams={pageMeta.params}
>
<PlasmicComponent component={pageMeta.displayName} />
</PlasmicRootProvider>
);
// Use revalidate if you want incremental static regeneration
return { props: { plasmicData, queryCache }, revalidate: 60 };
}
```
</details>
4. Create a new file `pages/[...catchall].tsx` with this content. (note: single square brackets as opposed to double)
<details>
<summary>
<strong>
Content of pages/[...catchall].tsx
</strong>
</summary>
```tsx
// ./pages/[...catchall].tsx
/*
Catchall page that runs for every page EXCEPT /, /login, and /public/*
These pages are login protected by default
The logic for checking authorization & where to redirect if a user is not authorized is controlled
by @/authorization-settings.ts.
The authorization-settings.ts file should export:
- authorizationCheckFunction: a function that returns true if the user is authorized to view the page
- loginPagePath: where to redirect to if authorization fails eg '/login'
The routes that render through this page are rendered on-demand (getServerSideProps instead of getStaticProps)
because they are login protected. This ensures that the user's session is checked on every request
and avoids login-protected pages being cached and related issues.
This pages is a modified various of the standard Plasmic NextJS loader API catchall page.
Pages created in Plasmic studio will render using this catchall if it's:
Page Settings -> URL path does NOT start with '/public/' and is not "/" or "/login"
*/
import type { GetServerSideProps } from "next";
import { createClient } from 'plasmic-supabase/dist/utils/supabase/server-props'
import * as React from "react";
import {
PlasmicComponent,
extractPlasmicQueryData,
ComponentRenderData,
PlasmicRootProvider,
} from "@plasmicapp/loader-nextjs";
import Error from "next/error";
import { useRouter } from "next/router";
import { PLASMIC } from "@/plasmic-init";
import { authorizationCheckFunction, loginPagePath } from "@/authorization-settings";
export default function PlasmicLoaderPage(props: {
plasmicData?: ComponentRenderData;
queryCache?: Record<string, any>;
}) {
const { plasmicData, queryCache } = props;
const router = useRouter();
if (!plasmicData || plasmicData.entryCompMetas.length === 0) {
return <Error statusCode={404} />;
}
const pageMeta = plasmicData.entryCompMetas[0];
return (
<PlasmicRootProvider
loader={PLASMIC}
prefetchedData={plasmicData}
prefetchedQueryData={queryCache}
pageParams={pageMeta.params}
pageQuery={router.query}
>
<PlasmicComponent component={pageMeta.displayName} />
</PlasmicRootProvider>
);
}
//This runs on the server while rendering
//Unlike the pages in the root directory, we run this every time the page is requested with no cache
//This is appropriate because these pages are login protected and only work with a valid session
//We also need to recheck each time the page is requested to ensure the user is still authenticated
export const getServerSideProps: GetServerSideProps = async (context) => {
//Get the catchall parameter from the page context
const { catchall } = context.params ?? {};
//Get the path of the current page
let plasmicPath = typeof catchall === 'string' ? catchall : Array.isArray(catchall) ? `/${catchall.join('/')}` : '/';
//Determine if the user is authorized to view this page
const supabase = createClient(context);
const { data: { user } } = await supabase.auth.getUser();
const isAuthorized = authorizationCheckFunction(plasmicPath, user);
if(isAuthorized !== true) return {
redirect: {
destination: loginPagePath,
permanent: false,
}
}
//Fetch data for the current page/component from plasmic
const plasmicData = await PLASMIC.maybeFetchComponentData(plasmicPath);
//If there's no plasmic data, the page does not exist in plasmic. So return nothing
//This will ultimately cause a 404 error to be shown by default
if (!plasmicData) {
// non-Plasmic catch-all
return { props: {} };
}
//Get the metadata for the current page
const pageMeta = plasmicData.entryCompMetas[0];
//Prefetch any data for the page
const queryCache = await extractPlasmicQueryData(
<PlasmicRootProvider
loader={PLASMIC}
prefetchedData={plasmicData}
pageParams={pageMeta.params}
>
<PlasmicComponent component={pageMeta.displayName} />
</PlasmicRootProvider>
);
//Return the plasmic data and the query cache data
return { props: { plasmicData, queryCache } };
}
```
</details>
5. Create a new directory `pages/public`
6. Create a new file `pages/public/[[...catchall]].tsx` with this content. (note: double square brackets as opposed to single)
<details>
<summary>
<strong>
Content of pages/public/[[...catchall]].tsx
</strong>
</summary>
```tsx
// ./pages/public/[[...catchall]].tsx
/*
Catchall page that runs for all routes that start with /public (ie /public/*)
These pages are publicly accessible (no logged in user required)
The routes that render through this page are rendered with Incremental Static Regeneration (ISR)
using getStaticProps.
This pages is a modified various of the standard Plasmic NextJS loader API catchall page.
Pages created in Plasmic studio will render using this catchall if it's:
Page Settings -> URL path starts with "/public/"
*/
import * as React from "react";
import {
PlasmicComponent,
extractPlasmicQueryData,
ComponentRenderData,
PlasmicRootProvider,
} from "@plasmicapp/loader-nextjs";
import type { GetStaticPaths, GetStaticProps } from "next";
import Error from "next/error";
import { useRouter } from "next/router";
import { PLASMIC } from "@/plasmic-init";
export default function PlasmicLoaderPage(props: {
plasmicData?: ComponentRenderData;
queryCache?: Record<string, any>;
}) {
const { plasmicData, queryCache } = props;
const router = useRouter();
if (!plasmicData || plasmicData.entryCompMetas.length === 0) {
return <Error statusCode={404} />;
}
const pageMeta = plasmicData.entryCompMetas[0];
return (
<PlasmicRootProvider
loader={PLASMIC}
prefetchedData={plasmicData}
prefetchedQueryData={queryCache}
pageParams={pageMeta.params}
pageQuery={router.query}
>
<PlasmicComponent component={pageMeta.displayName} />
</PlasmicRootProvider>
);
}
export const getStaticProps: GetStaticProps = async (context) => {
const { catchall } = context.params ?? {};
//Add "/public" at the start of the catchall parameter
//Since we're in the public folder
//This ensures the page is available at the same path as configured in Plasmic studio
let plasmicPath = typeof catchall === 'string' ? catchall : Array.isArray(catchall) ? `/${catchall.join('/')}` : '/';
plasmicPath = `/public${plasmicPath}`
//Continue with normal Plasmic loading logic
const plasmicData = await PLASMIC.maybeFetchComponentData(plasmicPath);
if (!plasmicData) {
// non-Plasmic catch-all
return { props: {} };
}
const pageMeta = plasmicData.entryCompMetas[0];
// Cache the necessary data fetched for the page
const queryCache = await extractPlasmicQueryData(
<PlasmicRootProvider
loader={PLASMIC}
prefetchedData={plasmicData}
pageParams={pageMeta.params}
>
<PlasmicComponent component={pageMeta.displayName} />
</PlasmicRootProvider>
);
// Use revalidate if you want incremental static regeneration
return { props: { plasmicData, queryCache }, revalidate: 60 };
}
export const getStaticPaths: GetStaticPaths = async () => {
//Get all pages from plasmic
const pageModules = await PLASMIC.fetchPages();
//Filter out only the pages who's url starts with "/public"
const pageModulesWithPublicPath = pageModules.filter(mod => mod.path.startsWith("/public"));
return {
paths: pageModulesWithPublicPath.map((mod) => ({
params: {
//Remove "/public/" from the path to get the catchall parameter
//Would usually be just removing "/" here but since we are in a folder called "public" we need to remove "/public/"
catchall: mod.path.split("/public/")[1].split("/"),
},
})),
fallback: "blocking",
};
}
```
</details>
7. Create a new file in the root of your project called `authorization-settings.ts` with the content below. The `authorizationCheck` function is imported by the `./pages/[...catchall].tsx` file to determine if a user is authorized to view a page. The `loginPagePath` is the path to redirect to if a user is not authorized to view a page. Modify this file to suit your needs.
<details>
<summary>
<strong>
Content of authorization-settings.ts
</strong>
</summary>
```ts
// ./authorization-settings.ts
import type { AuthorizationCheckFunction, RoutePath } from "plasmic-supabase";
//Run this function to check if the user is authorized to view any page
//This will run server-side before render of all pages EXCEPT /, /login and /public/*
//due to the setup of catchall pages in the pages directory
export const authorizationCheckFunction : AuthorizationCheckFunction= (pagePath, user) => {
if(!user || user.role !== 'authenticated') {
return false;
} else {
return true;
}
}
export const loginPagePath : RoutePath = '/login';
```
</details>
/
. This is your homepage and is automatically made publicly accesible/
or /login
and not starting with /public/
. This will automatically make it a login protected page./public/
. This will automatically make it a publicly accessible page.plasmic-init.ts
file has preview: true
enabled (as shown in the basic setup instructions above) cntrl + c
or cmd + c
and then
npm run dev
localhost:3000
. Check that Authorization and Authentication logic is working as expected:
/
should load without any login requiredThere are various techniques for login protecting pages in a NextJS app. This method was chosen because it was most reliable.
The other main method, using middleware, was not chosen because it was not reliable when navigating between pages using <Link>
components and similar. The author's understanding from much experimentation is that use of middleware to login-protect pages is more suited to the NextJS App Router, rather than the Pages router which Plasmic projects use.