SupaSync

SupaSync lets you connect your Supabase database directly to your Framer collections, so your data stays updated automatically. It's a no-code bridge between your backend and Framer components.

What is SupaSync?

SupaSync is a Framer plugin that connects your Supabase tables to Framer collections. It keeps your content in sync and removes the need for manual updates. Whether you're showing blog posts, user testimonials, or product data—SupaSync keeps it updated.


Installation
  1. Open your Framer project

  2. Go to Menu → Insert → Plugin

  3. Search for SupaSync


How It Works
SupaSync works in both directions:
  • From Supabase to Framer: Pulls your database rows into a Framer collection

  • From Framer to Supabase (optional): Pushes changes made in Framer back to your database

You choose the direction and what fields to sync.


Step-by-Step Setup


Step

Action

1

Create or select a collection in Framer

2

Right-click the collection and choose "Configure SupaSync"

3

Enter your Supabase Project URL and Service Role Key

4

Copy and run the setup SQL code in your Supabase SQL Editor

5

Choose the Supabase table you want to sync

6

Match each Supabase field with a Framer field

7

Click “Save & Sync” to finish setup and begin syncing


Sync Options


Type

Description

Full Sync

Replaces all collection data with Supabase data

Incremental

Only updates new or modified records

Merge Sync

Combines data, preserving changes from both sides


Database setup

To ensure smooth synchronization between Supabase and Framer using SupaSync, you need to configure your database permissions correctly.

Important: These rules allow SupaSync to access and update the data securely. You must run this only once per project you want to sync.


    -- Drop existing functions first
    DROP FUNCTION IF EXISTS public.get_tables(text);
    DROP FUNCTION IF EXISTS public.get_columns(text);
    DROP FUNCTION IF EXISTS public.get_table_data(text);
    DROP FUNCTION IF EXISTS public.enum_values(text);
    DROP FUNCTION IF EXISTS public.create_table(text, jsonb);

    -- Create enum_values function with explicit parameter naming and type
    CREATE OR REPLACE FUNCTION public.enum_values(p_enum_name text)
    RETURNS TABLE (enumlabel text)
    LANGUAGE sql
    SECURITY DEFINER
    SET search_path = public
    AS $$
        SELECT e.enumlabel
        FROM pg_type t
        JOIN pg_enum e ON t.oid = e.enumtypid
        JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
        WHERE t.typname = p_enum_name
        ORDER BY e.enumsortorder;
    $$;

    -- Create get_tables function
    CREATE OR REPLACE FUNCTION public.get_tables(p_schema_name text DEFAULT 'public')
    RETURNS TABLE (tablename text) 
    LANGUAGE plpgsql
    SECURITY DEFINER
    SET search_path = public
    AS $get_tables$
    BEGIN
        RETURN QUERY
        SELECT t.table_name::text
        FROM information_schema.tables t
        WHERE t.table_schema = p_schema_name
        AND t.table_type = 'BASE TABLE'
        AND t.table_schema NOT IN ('pg_catalog', 'information_schema')
        ORDER BY t.table_name;
    END;
    $get_tables$;

    -- Create get_columns function
    CREATE OR REPLACE FUNCTION public.get_columns(p_full_table_name text)
    RETURNS TABLE (column_name text, data_type text, is_nullable text)
    LANGUAGE plpgsql
    SECURITY DEFINER
    SET search_path = public
    AS $get_columns$
    DECLARE
        v_schema_name text;
        v_table_name text;
    BEGIN
        v_schema_name := split_part(p_full_table_name, '.', 1);
        v_table_name := split_part(p_full_table_name, '.', 2);
        
        IF v_table_name = '' THEN
            v_table_name := v_schema_name;
            v_schema_name := 'public';
        END IF;

        RETURN QUERY
        SELECT 
            c.column_name::text,
            c.data_type::text,
            c.is_nullable::text
        FROM information_schema.columns c
        WHERE c.table_schema = v_schema_name 
        AND c.table_name = v_table_name
        ORDER BY c.ordinal_position;
    END;
    $get_columns$;

    -- Create get_table_data function with dynamic ordering
    CREATE OR REPLACE FUNCTION public.get_table_data(p_full_table_name text)
    RETURNS SETOF json
    LANGUAGE plpgsql
    SECURITY DEFINER
    SET search_path = public
    AS $get_table_data$
    DECLARE
        v_schema_name text;
        v_table_name text;
        v_sql text;
        v_order_by text;
    BEGIN
        -- Split the table name
        v_schema_name := split_part(p_full_table_name, '.', 1);
        v_table_name := split_part(p_full_table_name, '.', 2);
        
        IF v_table_name = '' THEN
            v_table_name := v_schema_name;
            v_schema_name := 'public';
        END IF;

        -- Check for timestamp columns to use for ordering
        SELECT
            CASE
                WHEN EXISTS (
                    SELECT 1 FROM information_schema.columns 
                    WHERE table_schema = v_schema_name 
                    AND table_name = v_table_name 
                    AND column_name = 'created_at'
                ) THEN 'created_at DESC NULLS LAST'
                WHEN EXISTS (
                    SELECT 1 FROM information_schema.columns 
                    WHERE table_schema = v_schema_name 
                    AND table_name = v_table_name 
                    AND column_name = 'updated_at'
                ) THEN 'updated_at DESC NULLS LAST'
                ELSE 'CTID DESC' -- Fallback to system column
            END INTO v_order_by;

        -- Construct and execute dynamic SQL
        v_sql := format(
            'SELECT row_to_json(t) FROM %I.%I t ORDER BY %s LIMIT 100',
            v_schema_name,
            v_table_name,
            v_order_by
        );
        
        RETURN QUERY EXECUTE v_sql;
    END;
    $get_table_data$;

    -- Create create_table function with better validation
    CREATE OR REPLACE FUNCTION public.create_table(
        p_table_name text,
        p_columns jsonb
    ) RETURNS boolean
    LANGUAGE plpgsql
    SECURITY DEFINER
    SET search_path = public
    AS $$
    DECLARE
        v_column jsonb;
        v_sql text := '';
        v_primary_keys text := '';
    BEGIN
        -- Validate table name
        IF p_table_name IS NULL OR p_table_name = '' THEN
            RAISE EXCEPTION 'Table name cannot be empty' USING ERRCODE = '22023';
        END IF;
        
        -- Validate columns input
        IF p_columns IS NULL OR jsonb_typeof(p_columns) != 'array' THEN
            RAISE EXCEPTION 'columns parameter must be a JSON array' USING ERRCODE = '22023';
        END IF;
        
        IF jsonb_array_length(p_columns) = 0 THEN
            RAISE EXCEPTION 'At least one column must be specified' USING ERRCODE = '22023';
        END IF;

        -- Start CREATE TABLE statement
        v_sql := format('CREATE TABLE IF NOT EXISTS %I (', p_table_name);
        
        -- Add columns
        FOR v_column IN SELECT * FROM jsonb_array_elements(p_columns)
        LOOP
            -- Validate column structure
            IF v_column->>'name' IS NULL OR v_column->>'type' IS NULL THEN
                RAISE EXCEPTION 'Each column must have a name and type' USING ERRCODE = '22023';
            END IF;

            -- Accumulate column definitions
            v_sql := v_sql || format(
                '%I %s %s,',
                (v_column->>'name'),
                (v_column->>'type'),
                CASE 
                    WHEN (v_column->>'isNullable')::boolean THEN 'NULL'
                    ELSE 'NOT NULL'
                END
            );

            -- Collect primary key columns
            IF (v_column->>'isPrimary')::boolean THEN
                IF v_primary_keys != '' THEN
                    v_primary_keys := v_primary_keys || ',';
                END IF;
                v_primary_keys := v_primary_keys || format('%I', (v_column->>'name'));
            END IF;
        END LOOP;

        -- Add primary key constraint if any columns are marked as primary
        IF v_primary_keys != '' THEN
            v_sql := v_sql || format('PRIMARY KEY (%s))', v_primary_keys);
        ELSE
            -- Remove trailing comma and close parentheses
            v_sql := rtrim(v_sql, ',') || ')';
        END IF;

        -- Execute the CREATE TABLE statement
        EXECUTE v_sql;
        
        RETURN true;
    END;
    $$;

    -- Grant permissions for all schemas
    GRANT USAGE ON SCHEMA public TO anon, authenticated;
    GRANT USAGE ON SCHEMA auth TO anon, authenticated;
    GRANT USAGE ON SCHEMA storage TO anon, authenticated;
    GRANT USAGE ON SCHEMA realtime TO anon, authenticated;
    GRANT EXECUTE ON FUNCTION public.get_tables(text) TO anon, authenticated;
    GRANT EXECUTE ON FUNCTION public.get_columns(text) TO anon, authenticated;
    GRANT EXECUTE ON FUNCTION public.get_table_data(text) TO anon, authenticated;
    GRANT EXECUTE ON FUNCTION public.enum_values(text) TO anon, authenticated;
    GRANT EXECUTE ON FUNCTION public.create_table(text, jsonb) TO anon,


Supported Data Types


Supabase Type

Framer Type

Notes

text

String

Max 65535 chars

varchar

String

Max 65535 chars

int4

Number

-2147483648 to 2147483647

int8

Number

-9223372036854775808 to 9223372036854775807

float4

Number

6 decimal digits precision

float8

Number

15 decimal digits precision

boolean

Boolean

true/false

json

String

Stored as stringified JSON

jsonb

String

Stored as stringified JSON

timestamp

String

ISO 8601 format

uuid

String

UUID v4 format

supabase storage url new

Image

Auto detection of image fields


Using SupaSync with Framergenie

Framergenie is your go-to tool for creating dynamic, tiered, and social apps in Framer. By integrating SupaSync with Framergenie, you can build platforms where users can interact with each other, submit forms, upload images, and have that data synced to your Framer CMS.

Features You Can Build:
  • Tiered Sites: Create free or pro tiers, giving users access to different features based on their subscription plan. Use SupaSync to sync data like project limits, user access, and more.

  • User-Generated Content: Allow users to submit data through forms (including text, images, etc.), which will be synced to your Framer CMS and displayed dynamically.

  • Social App Features: Allow users to submit posts, upload profile pictures, and interact with your platform in real-time, syncing it all to Supabase and Framer.

Steps to Build a Form-Driven App:
  1. Create a Form:

    • Use Framergenie to create user forms. The form can include text fields, file upload fields (for images), and dropdowns for user data.

  2. Map Fields:

    • Map the form fields to the corresponding Supabase columns.

    • Ensure each form entry is synced to your Framer collection via SupaSync.

  3. Allow Users to Upload Images:

    • You can add an image upload field in your form. When users upload images, they will be stored in Supabase (or another file storage service) and the file URL will sync to your Framer CMS.

  4. Sync Form Data to Framer CMS:

    • Once the form is submitted, the data (including text, images, etc.) will be synced to your Framer collection automatically.

  5. Display the Data:

    • Set up your Framer CMS to display the submitted data in a dynamic layout, like a profile page, a social feed, or a testimonials page.


Common Issues

Issue

What to Check

Data not showing

Ensure the table has data and field mapping is accurate

Connection errors

Confirm your Supabase credentials are correct

Permissions denied

Your key must allow reading/writing the table

Slow performance

Try reducing the number of synced fields or rows

Incorrect data

Double-check your mappings and collection setup in Framer



Ready to Build Smarter with Framergenie?

Turn your Framer project into a real product with auth, data, and logic — all powered by Supabase. No code required.

Ready to Build Smarter with Framergenie?

Turn your Framer project into a real product with auth, data, and logic — all powered by Supabase. No code required.

@Framergenie

@Framergenie