How To Setup Polar With Supabase (using Edge Functions)

How To Setup Polar With Supabase (using Edge Functions)

July 14, 2025

In this guide, you'll learn how to set up Polar with Supabase.

Supabase and Polar are great tools that allow you to easily create a SAAS.

Unfortunately, if you want to use them together, you have to set up a third party server to synchronize the two.

But not anymore...!

What you'll build

In this blog post, you'll learn how to set up polar.sh with Supabase using Supabase's built-in Edge Functions.

diagram

For that, we'll set up a Supabase Edge Function that will listen forPolar's subscription events.

The subscriptions of the users will be saved in a supabase database.

Each user can then read their own subscription state from a dedicated subscriptions database table.

Setup

Step 1: Prefill the Checkout Screen

For that, you can prefill the email field of the checkout page by appending a url parameter to the checkout url.

For example:

https://buy.polar.sh/XXXX?customer_name=Youssef%DBenlemlih&customer_email=email@example.com

You can format a URL by using this example function in TypeScript:

function formatUrl(displayName, email) {
  return `https://buy.polar.sh/XXXX?customer_name=${encodeURIComponent(displayName)}&customer_email=${encodeURIComponent(
    email,
  )}`;
}

Step 2: Create a subscriptions Table

This is where the user subscriptions will be saved and read from your app.

Navigate to the Supabase SQL Editor and run the following SQL command to create a new table:

create table public.subscriptions
(
    user_id                    uuid                     not null,
    user_email                 character varying        not null,
    created_at                 timestamp with time zone not null default now(),
    modified_at                timestamp with time zone null     default now(),
    subscription_id            text                     not null,
    subscription_status        text                     not null,
    subscription_starts_at     timestamp with time zone not null,
    subscription_ends_at       timestamp with time zone null,
    subscription_price         numeric                  not null,
    subscription_currency      text                     not null,
    product_id                 text                     not null,
    product_name               text                     not null,
    product_recurring_interval text                     not null,
    raw_last_event             json                     not null,
    constraint subscriptions_pkey primary key (user_id, subscription_id),
    constraint subscriptions_user_id_fkey1 foreign KEY (user_id) references auth.users (id) on update CASCADE on delete CASCADE
) TABLESPACE pg_default;

Notice we're using (user_id, subscription_id) as a primary key.

This is crucial because it assures we don't have double entries for a user subscription.

Step 3: Add read policy to Subscriptions

Now let's add a Policy to the Subscriptions table so that each user can read their own subscription.

Navigate to the Policies page and add a new Policy with these values:

FieldValue
Name:Enable select for users based on user_id
Action:PERMISSIVE
Command:SELECT
Target roles:public (leave unchanged)
USING expression:(( SELECT auth.uid() AS uid) = user_id)
CHECK expression:None

It should look like this:

create-supabase-policy

Step 4: Add an SQL Helper Function

For the supabase edge function, we'll also need an SQL function to get the id associated with a user with a given email

Navigate to the Database Functions Page and create new function called get_user_id_by_email with the following SQL:

select id
from auth.users
where email = input_email;

It should look like this:

create-sql-function

Step 5: Create a Supabase Edge Function

Navigate to the Edge Functions Page and click Deploy New Function > Via Editor.

Enter the following code, give it the name polar-webhook and click on Deploy Function.

import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { createClient } from "jsr:@supabase/supabase-js@2";
import {
  validateEvent,
  WebhookVerificationError,
} from "npm:@polar-sh/sdk@0.34.2/webhooks";

const secretKey = "POLAR_WEBHOOK_SECRET";
Deno.serve(async (req) => {
  const body = await req.text().catch(
    () =>
      new Response("Invalid body", {
        status: 400,
      }),
  );
  const supabaseClient = createClient(
    Deno.env.get("SUPABASE_URL"),
    Deno.env.get("SUPABASE_SERVICE_ROLE_KEY"),
  );
  const secret = Deno.env.get(secretKey) ?? "";
  try {
    const headers = Object.fromEntries(req.headers.entries());
    const event = validateEvent(body, headers, secret);
    console.info({
      event,
    });
    switch (event.type) {
      case "subscription.created":
      case "subscription.updated": {
        event.data.customer.email;
        const { data: userId, error: userError } = await supabaseClient.rpc(
          "get_user_id_by_email",
          {
            input_email: event.data.customer.email,
          },
        );
        if (userError) {
          console.error("Error fetching user by email:", userError);
          return new Response("User fetch error", {
            status: 500,
          });
        }
        const row = {
          user_id: userId,
          user_email: event.data.customer.email,
          modified_at: new Date(),
          subscription_id: event.data.id,
          subscription_status: event.data.status,
          subscription_starts_at: event.data.startedAt,
          subscription_ends_at: event.data.endsAt,
          subscription_price: event.data.amount,
          subscription_currency: event.data.currency,
          product_id: event.data.productId,
          product_name: event.data.product.name,
          product_recurring_interval: event.data.product.recurringInterval,
          raw_last_event: event,
        };
        const { error } = await supabaseClient
          .from("subscriptions")
          .upsert(row, {
            onConflict: "user_id, subscription_id",
          });
        if (error) {
          console.error("Error updating subscription", error);
          return new Response(
            "Error updating subscription: " + stringifyError(error),
            {
              status: 500,
            },
          );
        }
        return new Response("Handled event successfully.", {
          status: 200,
        });
      }
    }
    return new Response("Ignored event successfully.", {
      status: 200,
    });
  } catch (err) {
    console.error(err);
    if (err instanceof WebhookVerificationError) {
      return new Response("Forbidden", {
        status: 403,
      });
    }
    return new Response("Error: " + stringifyError(err), {
      status: 400,
    });
  }
});

function stringifyError(error) {
  if (error instanceof Error) {
    return JSON.stringify(
      {
        name: error.name,
        message: error.message,
        stack: error.stack,
      },
      null,
      2,
    );
  }
  return JSON.stringify(error);
}

Copy the url of the edge function, you'll need it right away:

copy-edge-function-url

Step 6: Add A Webhook Endpoint to Polar

Now let's hop to the webhook settings page under Settings > Webhooks and click Add Endpoint.

Enter the url you copied from earlier, select the format Raw and select the events subscription.created and subsciption.updated.

Click on Create.

This will redirect you to the webhook details page where the secret is shown.

Copy this secret to a safe place (like a password manager) and let's hop back to Supabase.

Step 7: Set Up The Secret to Edge Function

The edge function needs the secret to make sure that no one can call your endpoint other than Polar.

Navigate to Edge Function Secrets and add a new Secret:

KeyValue
POLAR_WEBHOOK_SECRET<the copied secret>

Step 8: Read the subscription state in the client

Last but not least, you can now read the subscription state on the client!

Here's an example in TypeScript:

async function isPro() {
  const { data: subscriptions, error } = await supabase
    .from("subscriptions")
    .select("*")
    .eq("user_id", data.id);
  if (error) throw new Error(error.message);
  return subscriptions.some((sub) =>
    dayjs().isBetween(sub.subscription_starts_at, sub.subscription_ends_at),
  );
}

Wrapping Up

Woohoo! 🎉

You've successfully integrated Polar with Supabase using Edge Functions!