[{"data":1,"prerenderedAt":618},["ShallowReactive",2],{"\u002Fblog\u002F2024\u002Fsupabase-functions":3,"\u002Fblog\u002F2024\u002Fsupabase-functions-surround":607},{"id":4,"title":5,"author":6,"badge":11,"body":13,"date":595,"description":596,"draft":597,"extension":598,"image":599,"meta":601,"navigation":602,"path":603,"seo":604,"stem":605,"__hash__":606},"posts\u002Fblog\u002F2024\u002Fsupabase-functions.md","Supabase functions",{"name":7,"to":8,"avatar":9},"Nir Galon","https:\u002F\u002Fx.com\u002Fnirgn975",{"src":10},"\u002Favatar.webp",{"label":12},"development",{"type":14,"value":15,"toc":589},"minimark",[16,34,43,46,49,246,268,273,279,332,337,370,382,392,396,413,492,507,533,537,557,578,582,585],[17,18,19,20,27,28,33],"p",{},"Recently I started to work with ",[21,22,26],"a",{"href":23,"rel":24},"https:\u002F\u002Fsupabase.com",[25],"nofollow","Supabase",", which I must admit is a joy to use!, but I was stuck on some logic I wanted to do which is to create an organization and a user profile when a new user signs up. So naturally I started to read about supabase functions which are ",[21,29,32],{"href":30,"rel":31},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Ffunctions.html",[25],"PostgreSQL functions",".",[17,35,36,37,42],{},"It wasn't a very easy thing to do, since I hadn't used ",[21,38,41],{"href":39,"rel":40},"https:\u002F\u002Fwww.postgresql.org",[25],"PostgreSQL"," since version 9 and I didn't know about all the latest and greatest, but that didn't stop me from Google searching it, asking ChatGPT about it, watching some YouTube tutorials, and reading some blog posts on it - I couldn't find anything close to what I needed to achieve. So I put on some old school music and got to work - played with it for hours while reading the PostgreSQL documentation, and finally I succeeded!",[17,44,45],{},"As I said before, I couldn't find anyone that published something close to it, so I figured I have to.",[17,47,48],{},"The signup method looks like this:",[50,51,57],"pre",{"className":52,"code":53,"filename":54,"language":55,"meta":56,"style":56},"language-javascript shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","const { data, error } = await supabase.auth.signUp({\n  email: localState.email,\n  password: localState.password,\n  options: {\n    data: {\n      first_name: localState.firstName,\n      last_name: localState.lastName,\n      organization_name: localState.organizationName,\n    },\n  },\n});\n","AppRegister.vue","javascript","",[58,59,60,113,134,151,162,172,189,206,223,229,235],"code",{"__ignoreMap":56},[61,62,65,69,73,77,80,83,86,89,93,96,98,101,103,107,110],"span",{"class":63,"line":64},"line",1,[61,66,68],{"class":67},"spNyl","const",[61,70,72],{"class":71},"sMK4o"," {",[61,74,76],{"class":75},"sTEyZ"," data",[61,78,79],{"class":71},",",[61,81,82],{"class":75}," error ",[61,84,85],{"class":71},"}",[61,87,88],{"class":71}," =",[61,90,92],{"class":91},"s7zQu"," await",[61,94,95],{"class":75}," supabase",[61,97,33],{"class":71},[61,99,100],{"class":75},"auth",[61,102,33],{"class":71},[61,104,106],{"class":105},"s2Zo4","signUp",[61,108,109],{"class":75},"(",[61,111,112],{"class":71},"{\n",[61,114,116,120,123,126,128,131],{"class":63,"line":115},2,[61,117,119],{"class":118},"swJcz","  email",[61,121,122],{"class":71},":",[61,124,125],{"class":75}," localState",[61,127,33],{"class":71},[61,129,130],{"class":75},"email",[61,132,133],{"class":71},",\n",[61,135,137,140,142,144,146,149],{"class":63,"line":136},3,[61,138,139],{"class":118},"  password",[61,141,122],{"class":71},[61,143,125],{"class":75},[61,145,33],{"class":71},[61,147,148],{"class":75},"password",[61,150,133],{"class":71},[61,152,154,157,159],{"class":63,"line":153},4,[61,155,156],{"class":118},"  options",[61,158,122],{"class":71},[61,160,161],{"class":71}," {\n",[61,163,165,168,170],{"class":63,"line":164},5,[61,166,167],{"class":118},"    data",[61,169,122],{"class":71},[61,171,161],{"class":71},[61,173,175,178,180,182,184,187],{"class":63,"line":174},6,[61,176,177],{"class":118},"      first_name",[61,179,122],{"class":71},[61,181,125],{"class":75},[61,183,33],{"class":71},[61,185,186],{"class":75},"firstName",[61,188,133],{"class":71},[61,190,192,195,197,199,201,204],{"class":63,"line":191},7,[61,193,194],{"class":118},"      last_name",[61,196,122],{"class":71},[61,198,125],{"class":75},[61,200,33],{"class":71},[61,202,203],{"class":75},"lastName",[61,205,133],{"class":71},[61,207,209,212,214,216,218,221],{"class":63,"line":208},8,[61,210,211],{"class":118},"      organization_name",[61,213,122],{"class":71},[61,215,125],{"class":75},[61,217,33],{"class":71},[61,219,220],{"class":75},"organizationName",[61,222,133],{"class":71},[61,224,226],{"class":63,"line":225},9,[61,227,228],{"class":71},"    },\n",[61,230,232],{"class":63,"line":231},10,[61,233,234],{"class":71},"  },\n",[61,236,238,240,243],{"class":63,"line":237},11,[61,239,85],{"class":71},[61,241,242],{"class":75},")",[61,244,245],{"class":71},";\n",[17,247,248,249,252,253,256,257,260,261,256,264,267],{},"As you can see, there is the email and password, which is passed to the signUp method, but the extra information is ",[58,250,251],{},"first_name"," and ",[58,254,255],{},"last_name"," which I wanted to put in the ",[58,258,259],{},"profiles"," table, and the ",[58,262,263],{},"organization_name",[58,265,266],{},"organizations"," table. And, of course, linked them all together with some old school foreign keys.",[269,270,272],"h2",{"id":271},"_1-tables","1. Tables",[17,274,275,276,278],{},"So, first let's create the ",[58,277,259],{}," table:",[50,280,285],{"className":281,"code":282,"filename":283,"language":284,"meta":56,"style":56},"language-sql shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","create table profiles (\n  id uuid references auth.users on delete cascade not null primary key,\n  created_at timestamp with time zone,\n  updated_at timestamp with time zone,\n  first_name text,\n  last_name text,\n  phone text,\n  organization_id uuid references public.organizations on delete cascade not null,\n);\n","createProfiles.sql","sql",[58,286,287,292,297,302,307,312,317,322,327],{"__ignoreMap":56},[61,288,289],{"class":63,"line":64},[61,290,291],{},"create table profiles (\n",[61,293,294],{"class":63,"line":115},[61,295,296],{},"  id uuid references auth.users on delete cascade not null primary key,\n",[61,298,299],{"class":63,"line":136},[61,300,301],{},"  created_at timestamp with time zone,\n",[61,303,304],{"class":63,"line":153},[61,305,306],{},"  updated_at timestamp with time zone,\n",[61,308,309],{"class":63,"line":164},[61,310,311],{},"  first_name text,\n",[61,313,314],{"class":63,"line":174},[61,315,316],{},"  last_name text,\n",[61,318,319],{"class":63,"line":191},[61,320,321],{},"  phone text,\n",[61,323,324],{"class":63,"line":208},[61,325,326],{},"  organization_id uuid references public.organizations on delete cascade not null,\n",[61,328,329],{"class":63,"line":225},[61,330,331],{},");\n",[17,333,334,335,278],{},"And now let's create the ",[58,336,266],{},[50,338,341],{"className":281,"code":339,"filename":340,"language":284,"meta":56,"style":56},"create table organizations (\n  id uuid not null primary key,\n  created_at timestamp with time zone,\n  updated_at timestamp with time zone,\n  name text,\n);\n","createOrganizations.sql",[58,342,343,348,353,357,361,366],{"__ignoreMap":56},[61,344,345],{"class":63,"line":64},[61,346,347],{},"create table organizations (\n",[61,349,350],{"class":63,"line":115},[61,351,352],{},"  id uuid not null primary key,\n",[61,354,355],{"class":63,"line":136},[61,356,301],{},[61,358,359],{"class":63,"line":153},[61,360,306],{},[61,362,363],{"class":63,"line":164},[61,364,365],{},"  name text,\n",[61,367,368],{"class":63,"line":174},[61,369,331],{},[17,371,372,373,375,376,378,379,381],{},"As you can see the ",[58,374,259],{}," table is the one linking between the user from ",[58,377,100],{}," table and the organization from ",[58,380,266],{}," table. And you can head over to the Database tab to see the schema in a graphical representation with Supabase Schema Visualizer:",[17,383,384],{},[385,386],"img",{"alt":387,"className":388,"src":391},"Schema Visualizer",[389,390],"rounded-lg","mx-auto","\u002Fposts\u002F2024\u002Fsupabase-functions\u002Fschema-visualizer.webp",[269,393,395],{"id":394},"_2-function","2. Function",[17,397,398,399,401,402,404,405,407,408,252,410,412],{},"Now it's time to create the function itself. The function should get the ",[58,400,263],{}," and create a new organization in the ",[58,403,266],{}," table. We should save the organization's newly created id to then use it when we create a new profile in the ",[58,406,259],{}," table, along with the ",[58,409,251],{},[58,411,255],{}," from the signUp extra options.",[50,414,417],{"className":281,"code":415,"filename":416,"language":284,"meta":56,"style":56},"create or replace function handle_new_org_and_profile() returns trigger as $$\ndeclare\n  org_id uuid;\nbegin\n  -- insert new organization\n  insert into public.organizations(name)\n  values (new.raw_user_meta_data->>'organization_name')\n  returning id into org_id;\n  --  insert new profile\n  insert into public.profiles (id, first_name, last_name, organization_id)\n  values (new.id, new.raw_user_meta_data->>'first_name', new.raw_user_meta_data->>'last_name', org_id);\n  return new;\nend;\n$$ language plpgsql security definer;\n","supabaseTrigger.sql",[58,418,419,424,429,434,439,444,449,454,459,464,469,474,480,486],{"__ignoreMap":56},[61,420,421],{"class":63,"line":64},[61,422,423],{},"create or replace function handle_new_org_and_profile() returns trigger as $$\n",[61,425,426],{"class":63,"line":115},[61,427,428],{},"declare\n",[61,430,431],{"class":63,"line":136},[61,432,433],{},"  org_id uuid;\n",[61,435,436],{"class":63,"line":153},[61,437,438],{},"begin\n",[61,440,441],{"class":63,"line":164},[61,442,443],{},"  -- insert new organization\n",[61,445,446],{"class":63,"line":174},[61,447,448],{},"  insert into public.organizations(name)\n",[61,450,451],{"class":63,"line":191},[61,452,453],{},"  values (new.raw_user_meta_data->>'organization_name')\n",[61,455,456],{"class":63,"line":208},[61,457,458],{},"  returning id into org_id;\n",[61,460,461],{"class":63,"line":225},[61,462,463],{},"  --  insert new profile\n",[61,465,466],{"class":63,"line":231},[61,467,468],{},"  insert into public.profiles (id, first_name, last_name, organization_id)\n",[61,470,471],{"class":63,"line":237},[61,472,473],{},"  values (new.id, new.raw_user_meta_data->>'first_name', new.raw_user_meta_data->>'last_name', org_id);\n",[61,475,477],{"class":63,"line":476},12,[61,478,479],{},"  return new;\n",[61,481,483],{"class":63,"line":482},13,[61,484,485],{},"end;\n",[61,487,489],{"class":63,"line":488},14,[61,490,491],{},"$$ language plpgsql security definer;\n",[17,493,494,495,498,499,502,503,506],{},"The object from the signUp call is hosted in this ",[58,496,497],{},"new"," variable, and the extra options are in the ",[58,500,501],{},"raw_user_meta_data"," field. The two separate insert statements don't share a state, but we need to take the newly created organization id somehow. To do that we ",[58,504,505],{},"declare"," an id at the top of the function which every statement in the function can access too.",[17,508,509,510,513,514,517,518,521,522,525,526,528,529,532],{},"We ",[58,511,512],{},"returning"," the id from the ",[58,515,516],{},"insert"," command ",[58,519,520],{},"into"," the ",[58,523,524],{},"org_id"," variable we declared. Then we can use it in the next ",[58,527,516],{}," statement for our ",[58,530,531],{},"organization_id"," column.",[269,534,536],{"id":535},"_3-trigger","3. Trigger",[17,538,539,540,543,544,549,550,553,554,556],{},"Finally, we somehow need to call this function automatically every time a new row is created in the ",[58,541,542],{},"auth.users"," table, and we can do that with ",[21,545,548],{"href":546,"rel":547},"https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fdatabase\u002Fpostgres\u002Ftriggers",[25],"triggers",". So let's create a trigger to execute the ",[58,551,552],{},"handle_new_org_and_profile"," function we created above for each row in ",[58,555,542],{}," after the database inserts the row.",[50,558,561],{"className":281,"code":559,"filename":560,"language":284,"meta":56,"style":56},"create trigger on_auth_user_create_org_and_profile\n  after insert on auth.users\n  for each row execute procedure handle_new_org_and_profile();\n","supabseTrigger.sql",[58,562,563,568,573],{"__ignoreMap":56},[61,564,565],{"class":63,"line":64},[61,566,567],{},"create trigger on_auth_user_create_org_and_profile\n",[61,569,570],{"class":63,"line":115},[61,571,572],{},"  after insert on auth.users\n",[61,574,575],{"class":63,"line":136},[61,576,577],{},"  for each row execute procedure handle_new_org_and_profile();\n",[269,579,581],{"id":580},"_4-summary","4. Summary",[17,583,584],{},"You're now all set.",[586,587,588],"style",{},"html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":56,"searchDepth":115,"depth":115,"links":590},[591,592,593,594],{"id":271,"depth":115,"text":272},{"id":394,"depth":115,"text":395},{"id":535,"depth":115,"text":536},{"id":580,"depth":115,"text":581},"2024-02-11","Using a Supabase Postgres trigger to auto-create a profile on signup.",false,"md",{"src":600},"\u002Fposts\u002F2024\u002Fsupabase-functions\u002Fcover.webp",{},true,"\u002Fblog\u002F2024\u002Fsupabase-functions",{"title":5,"description":596},"blog\u002F2024\u002Fsupabase-functions","_nrw-NQpAklXH0zZNqwK7SjUfvEyCpTrUxRFiFqgL98",[608,613],{"title":609,"path":610,"stem":611,"description":612,"children":-1},"Change your email in GitHub","\u002Fblog\u002F2022\u002Fgithub-email","blog\u002F2022\u002Fgithub-email","Migrating email addresses without losing your GitHub contribution history.",{"title":614,"path":615,"stem":616,"description":617,"children":-1},"I built a thing","\u002Fblog\u002F2025\u002Fsubwatch","blog\u002F2025\u002Fsubwatch","Building and launching Subwatch, a pay-once Chrome extension for subscriptions.",1786001638688]