S
Supabase6d ago
tim

Insert into users.profiles right after signUp() (email confirmation flow) — request goes as anon, 40

Context * Nuxt 3 + Nuxt Supabase client * Email confirmation enabled * Table: users.profiles with FK to auth.users(id,email) and RLS * Policies exist for SELECT/UPDATE/INSERT; INSERT has WITH CHECK (id = auth.uid()) * Grants: schema USAGE + INSERT/SELECT/UPDATE to authenticated Schema (short)
create table users.profiles (
id uuid primary key default gen_random_uuid(),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
email varchar not null unique,
first_name varchar not null,
last_name varchar not null,
avatar_path varchar null,
constraint profiles_email_fkey foreign key (email) references auth.users (email) on update cascade on delete cascade,
constraint profiles_id_fkey foreign key (id) references auth.users (id) on update cascade on delete cascade
);
-- RLS enabled, INSERT policy: WITH CHECK (id = auth.uid())
create table users.profiles (
id uuid primary key default gen_random_uuid(),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
email varchar not null unique,
first_name varchar not null,
last_name varchar not null,
avatar_path varchar null,
constraint profiles_email_fkey foreign key (email) references auth.users (email) on update cascade on delete cascade,
constraint profiles_id_fkey foreign key (id) references auth.users (id) on update cascade on delete cascade
);
-- RLS enabled, INSERT policy: WITH CHECK (id = auth.uid())
Client code (where I try to insert names right after signUp):
// inside SignUpForm.vue
const { data, error } = await supabase.auth.signUp({
email: email.value,
password: password.value,
options: {
emailRedirectTo: runtimeConfig.public.emailVerificationPage,
captchaToken: token.value,
},
});

if (data?.user) {
// This fails with 401 (request seen at the gateway as role: "anon")
const { error: profileError } = await supabase
.schema('users')
.from('profiles')
.insert({
id: data.user.id,
email: email.value,
first_name: firstName.value,
last_name: lastName.value,
});
}
// inside SignUpForm.vue
const { data, error } = await supabase.auth.signUp({
email: email.value,
password: password.value,
options: {
emailRedirectTo: runtimeConfig.public.emailVerificationPage,
captchaToken: token.value,
},
});

if (data?.user) {
// This fails with 401 (request seen at the gateway as role: "anon")
const { error: profileError } = await supabase
.schema('users')
.from('profiles')
.insert({
id: data.user.id,
email: email.value,
first_name: firstName.value,
last_name: lastName.value,
});
}
3 Replies
tim
timOP6d ago
Observed behavior * The POST to /rest/v1/profiles is 401. * My gateway logs decode the JWT as role: "anon", session_id: null, subject: null. * Makes sense because with email confirmation enabled, signUp() returns no session I guess, so the client has no access token to attach. * RLS & grants are fine (SELECT/UPDATE work once the user is logged in); this particular INSERT goes out as anon → rejected. What I’d like to do * Insert the initial profile row immediately after signUp, before the user verifies their email (so we can store first_name/last_name captured during registration). Questions 1. Is there a supported way to “insert as the user” right after signUp() when email confirmation is enabled)? 2. If not, is the recommended pattern one of the following? * DB trigger on auth.users (server-side, SECURITY DEFINER) that inserts into users.profiles and pulls names from raw_user_meta_data (i.e., pass data: { first_name, last_name } in signUp.options). * Server-side upsert using a Supabase service client (Edge Function / server route) protected by Turnstile/CSRF. (But then we must carefully guard it, since it bypasses RLS. I try to only use the Service Role when it is really really necessary). * Defer the insert until after email verification (first authenticated page load), then upsert as the user. * Use auth.admin.createUser (server-side) with email_confirm to create and seed the profile in one go (but we do want standard email confirmation UX). 3. Any caveats with the trigger approach (e.g., search_path, SECURITY DEFINER, RLS interaction, defaulting id to auth.uid() vs explicit NEW.id) that I should be aware of? Notes * Sequences: none in users schema. * Schema USAGE: authenticated = true, anon = false. * Policies confirm INSERT WITH CHECK (id = auth.uid()). Goal * Keep strict RLS (no anon inserts), but still capture and persist first/last names at registration time without waiting for the user to verify/sign in. Thanks for any guidance or best-practice pointers!
garyaustin
garyaustin6d ago
SignUp if you are doing email confirm does not return a user session. It will redirect to your code after they access the email. There is user data but no session. You could use the data option on signup to send in data to user_metadata and then in a trigger function on auth.users insert to your profile table. Note though if you ever plan to use OAuth this is not possible so you would have to solve getting the data after they are confirmed anyway.
tim
timOP6d ago
Thanks a lot, that clears it up! Appreciate the guidance!

Did you find this page helpful?