Launch Week 12: Day 3

Learn more
Back
Plasmic

Plasmic

Plasmic
Plasmic
Plasmic

Overview

By integrating Supabase with Plasmic — a visual builder for the web — you can create data-backed applications without writing code. Although many users leverage Plasmic for rapid landing page development, this tutorial demonstrates its power as a general-purpose visual builder for React, enabling the creation of fully featured read-write applications.

Documentation

In this guide, we’ll walk you through building a crowd-sourced Pokémon Pokédex by connecting Supabase, an open-source Firebase alternative, with Plasmic, a visual web builder.

Live demo (signing up is quick):

https://plasmic-supabase-demo.vercel.app

Repository:

https://github.com/plasmicapp/plasmic/tree/master/examples/supabase-demo

Plasmic project:

https://studio.plasmic.app/projects/66RKaSPCwKxYjCfXWHCxn6

At a high level:

  • Supabase serves as the backend (powered by Postgres) for storing Pokémon data and managing authentication. Our code base includes React components for querying the database, displaying data, and handling user sessions.
  • Plasmic is used to build the application’s pages and design its visual layout. We import our Supabase components into Plasmic Studio, where they can be visually arranged and configured (for instance, to display data).
  • Plasmic-designed pages are rendered back into the Next.js application.

Step 1: Set up your Backend on Supabase

By default, Supabase configures email-based signups, storing users in the users table.

  • Navigate to the Table Editor in the left sidebar. Here, create a New table to store your Pokémon entries. Ensure you are in the schema public view and create a table called entries with six columns:
    • id: A unique identifier for the entry, automatically generated as the primary column.
    • user_id: Create a relation to the user table by clicking the link 🔗 icon next to the column name and selecting the id column from the user table.
    • name, description, imageUrl: These columns store the Pokémon’s name, description, and image URL respectively.
    • inserted_at: Automatically populated with the timestamp when the row is first inserted.

Note: In this tutorial, we’ve turned off Row Level Security (RLS). In a production environment, you should create policies that restrict who can create, edit, and delete posts. With RLS off, any user can modify the database without restrictions.

For convenience, you can import the following CSV file into Supabase to pre-populate your database. To import, select Import data via spreadsheet in the new table dialog box (this does not work on existing tables):

pokedex-export.csv

You’ll also need to create a function in your database to fetch the schema. This function will retrieve the Supabase database schema to display the table and column names in Plasmic Studio.

To do this, navigate to DatabaseFunctions and click Add a new function.

Use get_table_info as the function name and leave the schema as public. In the Return type field, select JSON.

Paste the following function definition:


_83
DECLARE
_83
result json;
_83
BEGIN
_83
WITH tables AS (
_83
SELECT c.oid :: int8 AS id,
_83
nc.nspname AS schema,
_83
c.relname AS name,
_83
obj_description(c.oid) AS comment
_83
FROM pg_namespace nc
_83
JOIN pg_class c
_83
ON nc.oid = c.relnamespace
_83
WHERE c.relkind IN ( 'r', 'p' )
_83
AND
_83
( pg_has_role(c.relowner, 'USAGE')
_83
OR has_table_privilege(c.oid,
_83
'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER')
_83
OR has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES')
_83
)
_83
),
_83
columns AS (
_83
SELECT c.table_schema AS schema,
_83
c.table_name AS table,
_83
c.column_name AS name,
_83
c.column_default AS default,
_83
c.data_type,
_83
c.udt_name AS format,
_83
(c.is_identity = 'YES') AS is_identity,
_83
(c.is_updatable = 'YES') AS is_updatable,
_83
CASE
_83
WHEN pk.column_name IS NOT NULL THEN TRUE
_83
ELSE FALSE
_83
END AS is_primary_key,
_83
array_to_json(array
_83
(SELECT e.enumlabel
_83
FROM pg_enum e
_83
JOIN pg_type t ON e.enumtypid = t.oid
_83
WHERE t.typname = udt_name
_83
ORDER BY e.enumsortorder)) AS enums
_83
FROM information_schema.columns c
_83
LEFT JOIN
_83
(SELECT ku.table_catalog,
_83
ku.table_schema,
_83
ku.table_name,
_83
ku.column_name
_83
FROM information_schema.table_constraints AS tc
_83
INNER JOIN information_schema.key_column_usage AS ku ON tc.constraint_type = 'PRIMARY KEY'
_83
AND tc.constraint_name = ku.constraint_name) pk ON c.table_catalog = pk.table_catalog
_83
AND c.table_schema = pk.table_schema
_83
AND c.table_name = pk.table_name
_83
AND c.column_name = pk.column_name
_83
)
_83
SELECT json_agg(t)
_83
INTO result
_83
FROM (
_83
SELECT
_83
name,
_83
COALESCE(
_83
(
_83
SELECT
_83
array_agg(
_83
row_to_json(columns)
_83
) FILTER (
_83
WHERE
_83
columns.schema = tables.schema AND columns.table = tables.name
_83
)
_83
FROM
_83
columns
_83
),
_83
'{}'::json[]
_83
) AS columns
_83
FROM
_83
tables
_83
WHERE
_83
schema NOT IN (
_83
'information_schema', 'pg_catalog',
_83
'pg_temp_1', 'pg_toast', 'pg_toast_temp_1'
_83
) AND
_83
name NOT IN ('buckets',
_83
'objects', 'migrations', 's3_multipart_uploads', 's3_multipart_uploads_parts', 'schema_migrations', 'subscription', 'messages')
_83
) t;
_83
_83
RETURN result;
_83
END;

Since we’ve disabled RLS for now, ensure that your function is executable by the anonymous user. To do this, navigate to the SQL Editor in the sidebar and run the following query:


_10
GRANT EXECUTE ON FUNCTION get_table_info() TO anon;

Important! Make sure to revert this step when you add RLS to your table later.

Step 2: Set up your codebase

We have a working code example available here. This starter project includes all the code components you need to begin querying Supabase through Plasmic Studio.

Code components are React components defined in your code base that we import into Plasmic Studio. Your project is configured to look for these at http://localhost:3000/plasmic-host. You can use and style these components in your design. See supabase-demo/plasmic-init.ts to understand how they are registered with Plasmic.

First, clone the repo to your development machine:


_10
git clone --depth=1 git@github.com:plasmicapp/plasmic.git
_10
cd plasmic/examples/supabase-demo/

Copy .env.example to .env to store your environment variables for the local development server. Then, add your Supabase project’s URL and public key (found in the API tab on the left pane of your Supabase dashboard).

Install the dependencies and fetch the Supabase schema by running:


_10
yarn

Now, start the development server (it listens on http://localhost:3000):


_10
yarn dev

Step 3: Explore the existing application

Open http://localhost:3000 in your web browser. The project is already set up for user sign-ups and logins and includes an admin interface for adding and editing Pokémon in the database. Feel free to sign up with your email address and add Pokémon entries. Note that Supabase requires email verification before you can log in.

If you pre-populated the database in Step 1, you should see the following homepage after logging in. Otherwise, you can manually add Pokémon via the UI.

Step 4: Clone the Plasmic project

Now, let’s make some enhancements! The code base is initially set up with a read-only copy of the Plasmic project. Let’s create an editable copy first.

Open the default starter Plasmic project here:

https://studio.plasmic.app/projects/66RKaSPCwKxYjCfXWHCxn6

To create an editable copy, click the Copy Project button in the blue bar. This will clone the project and redirect you to your copy.

Step 4a: Configure your code base to use the new Plasmic project

Take note of the project ID and API token. You can find the project ID in the URL:

https://studio.plasmic.app/projects/PROJECTID.

The API token is available by clicking the Code button in the top bar.

Return to .env and update the corresponding project ID and token fields.

Step 4b: Configure your Plasmic project app host

To ensure Plasmic looks for your code components on your development server, update your project’s app host to http://localhost:3000/plasmic-host.

Note: Keep your development server running at http://localhost:3000 for the project to load.

After restarting both the dev server and Plasmic Studio, you should be able to edit both the Studio and your codebase.

Step 4: Deployment (optional)

You can host your application using Vercel, Netlify, AWS, or any other provider you prefer.

In this section, we will cover deployment using Vercel:

  1. First, create a GitHub repository for your project.
  2. Next, log into vercel.app and add a new project.
  3. Point to the repository you created in the first step.
  4. Go to the project settings.
  5. Set the Build & Development settings to the following:

  1. Set the Node.js version to 20.x.
  2. Go to the Environment Variables tab and copy the contents of your .env file.
  3. Trigger a deployment of your project.
  4. (optional) go back to step 4b, and point to your /plasmic-host page using your newly created domain. (for example, https://plasmic-supabase-demo.vercel.app/plasmic-host)

You can also refer to this video to see how another project is configured on Vercel and Netlify.

If you plan to use Plasmic hosting, you will need to disable the Supabase email verification feature in AuthenticationProvidersEmail. Supabase requires you to set up a Next.js API route, which we don’t support yet.

Step 5: Create a new page – Look up Pokémon by name

Let’s create a lookup page for our Pokédex using the code components from the code base.

  1. Create a new page called Guess and set its path to /guess.
  2. Add a NavBar and any additional elements to enhance the layout (we used two vertical containers for background and centering).
  3. Insert a SupabaseDataProvider by opening the AddDrawer (click the blue + button).

For source, see components/CodeComponents/DatabaseComponents/SupabaseDataProvider.

Above the SupabaseDataProvider, add a text input element and a heading. Change the text input’s placeholder to Type your guess. Your layout should resemble the following:

Next, configure the props for the SupabaseDataProvider in the right-hand panel:

  • Table: Set this to the table you created in Supabase.
  • Columns: Provide a comma-separated list of the columns you want to select from the table.
  • Filters: Define which data to fetch (similar to a WHERE clause).
  • Single: Specify whether to fetch a single record or multiple records.

Additionally, we set a visibility condition so that data is fetched only when the input contains more than three characters.

This is how the filter parameter should appear, with the operation set to eq (meaning it will fetch records where the property equals a specific value).

The SupabaseDataProvider passes down the fetched data, leaving it up to you to decide how to use it.

Next, add a Post component (used on the homepage) to display the Pokémon.

If no matching Pokémon exists in the database, configure a nearby text node to inform the user. Control its visibility based on the data from the SupabaseDataProvider—this is how you can access that data in the Studio:

Finally, add your new page as a link in the NavBar component. Try this as an exercise 🙂

Step 6: Check your dev server

If your development server has been running all along, you’ll notice that the site automatically fetches and rebuilds as you make changes in Plasmic Studio. To restart the dev server, run:


_10
yarn dev

Then, view the results at http://localhost:3000/guess

How does this all work under the hood?

All the code components are defined in your codebase, and you’re free to enhance them to support more powerful querying capabilities in Plasmic Studio.

Email Verification API Route

To sign up, users must verify their email address. After signing up, they receive an email with a verification link. Clicking the link directs them to the API route at /pages/api/auth/confirm.ts, which confirms the OTP code and redirects them to the homepage.

Learn more here: https://supabase.com/docs/guides/auth/server-side/nextjs?queryGroups=router&router=pages

SupabaseDataProvider

This simple component executes queries and populates the application's data. If your data is mostly static, consider using usePlasmicQueryData instead of the mutable version.


_85
import { Database } from "@/types/supabase";
_85
import { createSupabaseClient } from "@/util/supabase/component";
_85
import { Filter, applyFilter, isValidFilter } from "@/util/supabase/helpers";
_85
import {
_85
DataProvider,
_85
usePlasmicCanvasContext,
_85
useSelector,
_85
} from "@plasmicapp/loader-nextjs";
_85
import { useMutablePlasmicQueryData } from "@plasmicapp/query";
_85
import { ReactNode } from "react";
_85
_85
export interface SupabaseDataProviderProps {
_85
children?: ReactNode;
_85
tableName?: keyof Database["public"]["Tables"];
_85
columns?: string[];
_85
className?: string;
_85
filters?: any;
_85
single?: boolean;
_85
}
_85
_85
export function SupabaseDataProvider(props: SupabaseDataProviderProps) {
_85
const supabase = createSupabaseClient();
_85
const inEditor = usePlasmicCanvasContext();
_85
// These props are set in the Plasmic Studio
_85
const { children, tableName, columns, className, filters, single } = props;
_85
const currentUser = useSelector("auth");
_85
const validFilters = filters?.filter((f: any) => isValidFilter(f)) as
_85
| Filter[]
_85
| undefined;
_85
_85
const selectFields = columns?.join(",") || "";
_85
_85
// Error messages are currently rendered in the component
_85
if (!tableName) {
_85
return <p>You need to set the tableName prop</p>;
_85
} else if (!selectFields) {
_85
return <p>You need to set the columns prop</p>;
_85
}
_85
_85
// Performs the Supabase query
_85
async function makeQuery() {
_85
// dont perform query if user is not logged in.
_85
// allow to query in editor mode for demo purposes
_85
if (!inEditor && !currentUser?.email) {
_85
return;
_85
}
_85
let query = supabase.from(tableName!).select(selectFields || "");
_85
query = applyFilter(query, validFilters);
_85
// This is where the Single property comes into play—either querying for a single record or multiple records.
_85
const { data, error, status } = await (single
_85
? query.single()
_85
: query.order("id", { ascending: false }));
_85
_85
if (error && status !== 406) {
_85
throw error;
_85
}
_85
return data;
_85
}
_85
_85
// The first parameter is a unique cache key for the query.
_85
// If you want to update the cache - you are able to use the Refresh Data function in the Plasmic Studio.
_85
const { data } = useMutablePlasmicQueryData(
_85
`${tableName}-${JSON.stringify(filters)}`,
_85
async () => {
_85
try {
_85
return await makeQuery();
_85
} catch (err) {
_85
console.error(err);
_85
return {};
_85
}
_85
// As an additional way to control the cache flow - you are able to specify the revalidate options.
_85
// For example, you can revalidate the data on mount and on page focus, to make sure that data is always up-to-date.
_85
// If your data is mostly static - turn these options off.
_85
},
_85
{ revalidateOnMount: true, revalidateOnFocus: true }
_85
);
_85
_85
return (
_85
<div className={className}>
_85
<DataProvider name={tableName} data={data}>
_85
{children}
_85
</DataProvider>
_85
</div>
_85
);
_85
}

How it is registered:


_16
// /plasmic-init.ts
_16
PLASMIC.registerComponent(SupabaseQuery, {
_16
name: "SupabaseQuery",
_16
providesData: true,
_16
props: {
_16
children: "slot",
_16
tableName: tableNameProp,
_16
columns: {
_16
...columnProp,
_16
multiSelect: true,
_16
},
_16
filters: filtersProp,
_16
single: "boolean",
_16
},
_16
importPath: "./components/CodeComponents/DatabaseComponents",
_16
});

The shared props for this registration are defined below. We use the dbSchema variable from an auto-generated file that fetches the Supabase schema. This file refreshes during yarn build or when you run yarn, allowing the Studio to display current tables and columns without hardcoding them.


_37
const tableNameProp = {
_37
type: "choice" as const,
_37
multiSelect: false,
_37
options: dbSchema.map((table) => table.name) || [],
_37
};
_37
_37
const columnProp = {
_37
type: "choice" as const,
_37
options: (props: any) => {
_37
const table = dbSchema.find((t) => t.name === props.tableName);
_37
return table?.columns?.map((column) => column.name) ?? [];
_37
},
_37
};
_37
_37
const filtersProp = {
_37
type: "array" as const,
_37
nameFunc: (item: any) => item.name || item.key,
_37
itemType: {
_37
type: "object" as const,
_37
fields: {
_37
name: {
_37
type: "choice" as const,
_37
options: ["eq", "match"],
_37
},
_37
args: {
_37
type: "array" as const,
_37
itemType: {
_37
type: "object" as const,
_37
fields: {
_37
column: columnProp,
_37
value: "string" as const,
_37
},
_37
},
_37
},
_37
},
_37
},
_37
};

SupabaseForm

This component performs database mutations (such as delete, update, or insert operations). It wraps form elements and calls an action upon submission. In most cases, a submit button triggers the form, and the onSuccess hook is useful for redirecting the user or refreshing page data.


_77
import { Database } from "@/types/supabase";
_77
import { createSupabaseClient } from "@/util/supabase/component";
_77
import { Filter, applyFilter, isValidFilter } from "@/util/supabase/helpers";
_77
import React, { ReactNode } from "react";
_77
_77
export interface SupabaseFormProps {
_77
children?: ReactNode;
_77
tableName?: keyof Database["public"]["Tables"];
_77
method?: "upsert" | "insert" | "update" | "delete";
_77
filters?: any;
_77
data?: any;
_77
onSuccess?: () => void;
_77
className?: string;
_77
}
_77
export function SupabaseForm(props: SupabaseFormProps) {
_77
const { children, tableName, method, filters, data, className, onSuccess } =
_77
props;
_77
const supabase = createSupabaseClient();
_77
_77
if (!tableName) {
_77
return <p>You need to set the tableName prop</p>;
_77
}
_77
if (!method) {
_77
return <p>You need to choose a method</p>;
_77
}
_77
_77
if (method !== "delete" && !data) {
_77
return <p>You need to set the data prop</p>;
_77
}
_77
_77
const validFilters = filters?.filter((f: any) => isValidFilter(f)) as
_77
| Filter[]
_77
| undefined;
_77
_77
async function onSubmit(e: React.FormEvent) {
_77
e?.preventDefault();
_77
try {
_77
const table = supabase.from(tableName!);
_77
let query: any;
_77
switch (method) {
_77
case "update": {
_77
query = table.update(data);
_77
break;
_77
}
_77
case "upsert": {
_77
query = table.upsert(data);
_77
}
_77
case "insert": {
_77
query = table.insert(data);
_77
}
_77
case "delete": {
_77
query = table.delete();
_77
}
_77
default: {
_77
throw new Error("Invalid method");
_77
}
_77
}
_77
_77
query = applyFilter(query, validFilters);
_77
const { error } = await query;
_77
_77
if (error) {
_77
console.error(error);
_77
} else if (onSuccess) {
_77
onSuccess();
_77
}
_77
} catch (error) {
_77
console.error(error);
_77
}
_77
}
_77
_77
return (
_77
<form onSubmit={onSubmit} className={className}>
_77
{children}
_77
</form>
_77
);
_77
}

How it is registered:


_17
PLASMIC.registerComponent(SupabaseForm, {
_17
name: "SupabaseForm",
_17
props: {
_17
children: "slot",
_17
tableName: tableNameProp,
_17
filters: filtersProp,
_17
method: {
_17
type: "choice",
_17
options: ["upsert", "insert", "update", "delete"],
_17
},
_17
data: "exprEditor",
_17
onSuccess: {
_17
type: "eventHandler",
_17
argTypes: [],
_17
},
_17
},
_17
});

SupabaseUserSession

This component provides global user data across the application and is registered as a GlobalContext. If you’d prefer not to log in every time to see content from a specific user’s perspective, you can set a staticToken in the context settings.

You can find the authentication token from the hosted application by inspecting any network request. The token is in the Authentication header (everything after “Bearer”).

Source code:


_57
import { createSupabaseClient } from "@/util/supabase/component";
_57
import {
_57
DataProvider,
_57
usePlasmicCanvasContext,
_57
} from "@plasmicapp/loader-nextjs";
_57
import { User } from "@supabase/supabase-js";
_57
import React from "react";
_57
_57
export function SupabaseUserSession({
_57
children,
_57
staticToken,
_57
}: {
_57
className?: string;
_57
staticToken?: string;
_57
children?: React.ReactNode;
_57
}) {
_57
const supabase = createSupabaseClient();
_57
const [currentUser, setCurrentUser] = React.useState<User | null>(null);
_57
const [isLoaded, setIsLoaded] = React.useState(false);
_57
_57
const inEditor = usePlasmicCanvasContext();
_57
_57
React.useEffect(() => {
_57
if (inEditor) {
_57
if (staticToken) {
_57
supabase.auth
_57
.getUser(staticToken)
_57
.then((res) => {
_57
setCurrentUser(res.data.user);
_57
})
_57
.finally(() => {
_57
setIsLoaded(true);
_57
});
_57
}
_57
return;
_57
}
_57
_57
const {
_57
data: { subscription },
_57
} = supabase.auth.onAuthStateChange((event, session) => {
_57
if (event == "SIGNED_OUT") {
_57
setCurrentUser(null);
_57
} else if (["SIGNED_IN", "INITIAL_SESSION"].includes(event) && session) {
_57
setCurrentUser(session.user);
_57
}
_57
setIsLoaded(true);
_57
});
_57
_57
return subscription.unsubscribe;
_57
}, []);
_57
_57
return (
_57
<DataProvider name="auth" data={currentUser || {}}>
_57
{isLoaded && children}
_57
</DataProvider>
_57
);
_57
}

How it is registered:


_10
PLASMIC.registerGlobalContext(SupabaseUserSession, {
_10
name: "SupabaseUserSession",
_10
importPath: "./components/CodeComponents/GlobalContexts",
_10
providesData: true,
_10
props: { staticToken: "string" },
_10
});

RedirectIf

This component redirects the user based on a condition you specify. In our example, it redirects users from inner pages if they are not logged in.

Source code:


_33
import { usePlasmicCanvasContext } from "@plasmicapp/loader-nextjs";
_33
import React from "react";
_33
_33
export interface RedirectIfProps {
_33
children?: any;
_33
className?: string;
_33
condition?: any;
_33
onFalse?: () => void;
_33
}
_33
_33
export function RedirectIf(props: RedirectIfProps) {
_33
const { children, className, onFalse, condition } = props;
_33
const inEditor = usePlasmicCanvasContext();
_33
_33
React.useEffect(() => {
_33
if (inEditor || !onFalse || condition) {
_33
return;
_33
}
_33
onFalse();
_33
}, [condition, inEditor]);
_33
_33
// Validation
_33
if (typeof condition === "undefined") {
_33
return (
_33
<p>
_33
Condition needs to be a boolean prop. Try to add exclamation marks to
_33
the value.
_33
</p>
_33
);
_33
}
_33
_33
return <div className={className}>{children}</div>;
_33
}

And how it is registered:


_11
PLASMIC.registerComponent(RedirectIf, {
_11
name: "RedirectIf",
_11
props: {
_11
children: "slot",
_11
onFalse: {
_11
type: "eventHandler",
_11
argTypes: [],
_11
},
_11
condition: "exprEditor",
_11
},
_11
});

Details

DeveloperPlasmic
CategoryLow-Code
DocumentationLearn

Third-party integrations and docs are managed by Supabase partners.