Skip to content

Supabase Local

Prerequisites

  • Docker - Docker is used to run Supabase locally.
  • Docker Compose - Docker Compose is used to run Supabase locally.
  • Doppler CLI - Doppler is used to manage secrets.
  • Supabase CLI - Supabase CLI is used to manage Supabase projects.
  • Bun - For the web apps, we use Bun to run the project.

Setup

Setup Supabase

Go to the root/source folder of the project and run the following command to login to Supabase:

supabase login
supabase init
# supabase link --project-ref <project-ref>

Configure config.toml accordingly:

project_id = "something"

# ...

[db.seed]
enabled = true
sql_paths = ["./seeds/*.sql"]

# ...

[studio]
enabled = true
port = 54323
api_url = "env(SUPABASE_URL)"
openai_api_key = "env(OPENAI_API_KEY)"

# ...

[storage.buckets.avatars]
public = true
file_size_limit = "50MiB"
allowed_mime_types = ["image/png", "image/jpeg"]
objects_path = "./storage/avatars"

# ...

[auth]
enabled = true
site_url = "env(BASE_URL)"

# ...

[auth.external.google]
enabled = true
client_id = "env(SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_ID)"
secret = "env(SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET)"
redirect_uri = "env(SUPABASE_AUTH_EXTERNAL_GOOGLE_REDIRECT_URI)"
url = ""
skip_nonce_check = false

Add the custom auth hook to the config.toml file:

[auth.hook.custom_access_token_hook]
enabled = true
uri = "pg-functions://postgres/public/custom_access_token_hook"

During the migrations, it is necessary to assign the additional permissions from the documentation

Run Supabase with Doppler

First make sure you have the secrets in Doppler:

SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_ID
SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET
SUPABASE_AUTH_EXTERNAL_GOOGLE_REDIRECT_URI

Login to Doppler and setup using the corresponding project, using the dev_personal branch:

doppler login
doppler setup

Confirm the secrets are in the dev_personal branch:

doppler secrets

Migrations

If there are new users/roles in the database other than the default ones, it is necessary to create a new migration file.

supabase migration new roles

This will create a new migration file in the migrations folder. And add the corresponding query to the migration file.

CREATE USER "mage" REPLICATION BYPASSRLS;

For the remote schema pull, it is necessary to move temporarily the file from the migrations folder.

Remote Schema Pull

To pull the remote schema, run the following command:

doppler run --forward-signals -- supabase db pull

This will create a file like 20250115064442_remote_schema.sql. After the schema is pulled, move the migration file about the roles back to the migrations folder.

Missing triggers

For some reason, Supabase migrations do not create the triggers. So it is necessary to create them manually. In this case, we need to create a new migration file for the auth related triggers.

supabase migrations new on_auth_triggers

Then add the following query to the migration file:

CREATE TRIGGER on_auth_user_created
    AFTER INSERT
    ON public.users
    FOR EACH ROW EXECUTE FUNCTION handle_new_user();

CREATE TRIGGER on_auth_user_updated
    AFTER UPDATE OF email, deleted_at, banned_until, email_confirmed_at
    ON public.users
    FOR EACH ROW EXECUTE FUNCTION handle_auth_user_update();

Custom Access Token Hook

The custom access token hook is used to create the access token for the user. It is necessary to create a new migration file for the custom access token hook.

supabase migrations new auth_hook
GRANT EXECUTE
    ON FUNCTION public.custom_access_token_hook
    TO supabase_auth_admin;

GRANT USAGE ON SCHEMA public TO supabase_auth_admin;

REVOKE EXECUTE
    ON FUNCTION public.custom_access_token_hook
    FROM authenticated, anon, public;

-- Grant permissions for the user_roles and user_permissions tables

GRANT ALL ON TABLE public.user_roles TO supabase_auth_admin;

REVOKE ALL ON TABLE public.user_roles FROM anon, public;

GRANT ALL ON TABLE public.user_permissions TO supabase_auth_admin;

REVOKE ALL ON TABLE public.user_permissions FROM anon, public;

For reference, the hook function we use is this (add to the migration file if it is not already in the remote schema migration file):

CREATE OR REPLACE FUNCTION public.custom_access_token_hook(event jsonb) RETURNS jsonb
    LANGUAGE plpgsql STABLE
    AS $$
DECLARE
    claims               jsonb;
    user_roles           jsonb;
    role_permissions     jsonb;
    custom_permissions   jsonb;
    combined_permissions jsonb;
BEGIN
    -- Fetch all roles for the user
    SELECT jsonb_agg(role)
    INTO user_roles
    FROM public.user_roles
    WHERE user_id = (event ->> 'user_id')::uuid;

    -- If no roles are found, assign the default role 'user'
    IF user_roles IS NULL THEN
        user_roles := jsonb_build_array('user');
    END IF;

    -- Fetch all permissions associated with the user's roles
    SELECT jsonb_agg(concat(rp.resource, ':', rp.action))
    INTO role_permissions
    FROM public.role_permissions rp
    WHERE rp.role = ANY (array(SELECT jsonb_array_elements_text(user_roles)));

    -- Fetch all custom permissions for the user
    SELECT jsonb_agg(concat(up.resource, ':', up.action))
    INTO custom_permissions
    FROM public.user_permissions up
    WHERE up.user_id = (event ->> 'user_id')::uuid;

    -- Combine role permissions and custom permissions and remove duplicates
    combined_permissions := (SELECT jsonb_agg(DISTINCT permission)
                             FROM jsonb_array_elements_text(coalesce(role_permissions, '[]'::jsonb) ||
                                                            coalesce(custom_permissions, '[]'::jsonb)) AS permission);

    claims := event -> 'claims';

    IF user_roles IS NOT NULL THEN
        claims := jsonb_set(claims, '{user_roles}', user_roles);
    ELSE
        claims := jsonb_set(claims, '{user_roles}', '[]'::jsonb); -- Default to empty array
    END IF;

    claims := jsonb_set(claims, '{user_permissions}', combined_permissions);

    event := jsonb_set(event, '{claims}', claims);

    RETURN event;
END;
$$;

Run Supabase

Run the following command to start Supabase:

doppler run --forward-signals -- supabase start
bun run supabase:start

If another instance of Supabase is running, you may get the following error:

Bind for 0.0.0.0:54322 failed: port is already allocated

To fix this, stop the containers, and run the command again.

Stop Supabase

To stop Supabase, run the following command:

doppler run --forward-signals -- supabase stop

Or if it is configured in the web app, run the following command:

bun run supabase:stop

[!NOTE] Every time the config.toml file is changed, it is necessary to restart Supabase.

doppler run --forward-signals -- supabase stop --no-backup
doppler run --forward-signals -- supabase start

Stop Supabase without backup

To stop Supabase without backing up the database, run the following command:

doppler run --forward-signals -- supabase stop --no-backup

Same as above, if it is configured in the web app, run the following command:

bun run supabase:stop:no-backup