1import{j as e}from"./vendor-motion-CfBPC-kS.js";import{L as t}from"./vendor-react-BR_v4Ont.js";import{C as n}from"./ContentPage-BxMYAaUt.js";import"./index-VVKdn61F.js";import"./vendor-supabase-BSd6ADfF.js";import"./HireCTA-DktS03tb.js";import"./EmailGateModal-BCI8ZqlV.js";import"./dialog-my3VBzDi.js";const a=e.jsxs(e.Fragment,{children:[e.jsx("h2",{children:"When to use an Edge Function instead of n8n"}),e.jsxs("p",{children:["n8n is the right tool when a non-developer will maintain the automation, or when you want a visual audit trail of every step. A Supabase ",e.jsx("strong",{children:"Edge Function"})," â Deno running at the edge â is the right tool when the glue lives inside a Supabase app you already own: you want it version-controlled with the rest of your code, deployed with ",e.jsx("code",{children:"supabase functions deploy"}),", and running with sub-second cold starts next to your database. GHL webhooks in, GHL API calls out, no separate automation platform to keep alive."]}),e.jsxs("p",{children:["This site's own booking runs on exactly this pattern: ",e.jsx("code",{children:"get-availability"})," and ",e.jsx("code",{children:"book-appointment"})," Edge Functions call the GoHighLevel calendar API and are what the booking widget (and the Vapi voice agent) hit. What follows is that pattern, generalized."]}),e.jsx("h2",{children:"Prerequisites"}),e.jsxs("ul",{children:[e.jsxs("li",{children:["A Supabase project and the Supabase CLI installed (",e.jsx("code",{children:"supabase login"})," done)"]}),e.jsx("li",{children:"A GHL Private Integration token scoped to whatever the function does"}),e.jsxs("li",{children:["Deno basics â the function is a single ",e.jsx("code",{children:"Deno.serve"})," handler"]})]}),e.jsx("h2",{children:"Step 1: Scaffold the function"}),e.jsx("pre",{style:{background:"var(--tm-code-bg)",color:"var(--tm-code-text)",padding:16,borderRadius:4,marginTop:8,fontSize:13,overflowX:"auto"},children:"supabase functions new ghl-availability"}),e.jsxs("p",{children:["That creates ",e.jsx("code",{children:"supabase/functions/ghl-availability/index.ts"})," with a ",e.jsx("code",{children:"Deno.serve"})," handler. Everything below goes in that file."]}),e.jsx("h2",{children:"Step 2: Authenticate to GHL from Deno â secrets, not code"}),e.jsx("p",{children:"Never paste the token into the source. Set it as a Supabase secret and read it from the environment at runtime:"}),e.jsx("pre",{style:{background:"var(--tm-code-bg)",color:"var(--tm-code-text)",padding:16,borderRadius:4,marginTop:8,fontSize:13,overflowX:"auto"},children:`supabase secrets set GHL_TOKEN=pit-xxxx... GHL_LOCATION_ID=abc123 2 3// index.ts 4const token = Deno.env.get("GHL_TOKEN")!; 5const headers = { 6 "Authorization": \`Bearer \${token}\`, 7 "Version": "2021-07-28", 8 "Content-Type": "application/json", 9};`}),e.jsxs("p",{children:["The ",e.jsx("code",{children:"Version"})," header is mandatory on the GHL v2 API â a valid token without it still returns 401. If you hit that, the ",e.jsx(t,{to:"/fix/n8n-http-401-unauthorized-ghl",style:{color:"var(--tm-accent)"},children:"401 fix"})," applies here too; the auth rules are identical whether the caller is n8n or Deno."]}),e.jsx("h2",{children:"Step 3: The availability / booking handler"}),e.jsx("p",{children:"Parse the request, call the GHL calendar API, return JSON. Keep it to one GHL call per invocation â an Edge Function that fans out to five endpoints is slow and hard to debug."}),e.jsx("pre",{style:{background:"var(--tm-code-bg)",color:"var(--tm-code-text)",padding:16,borderRadius:4,marginTop:8,fontSize:13,overflowX:"auto"},children:`Deno.serve(async (req) => { 10 const { startDate, endDate, timezone } = await req.json(); 11 const cal = Deno.env.get("GHL_CALENDAR_ID")!; 12 const url = new URL( 13 \`https://services.leadconnectorhq.com/calendars/\${cal}/free-slots\`); 14 url.searchParams.set("startDate", startDate); 15 url.searchParams.set("endDate", endDate); 16 url.searchParams.set("timezone", timezone); 17 18 const res = await fetch(url, { headers }); 19 const data = await res.json(); 20 return new Response(JSON.stringify(data), { 21 headers: { "Content-Type": "application/json", ...cors }, 22 }); 23});`}),e.jsx("h2",{children:"Step 4: Verify inbound webhook signatures"}),e.jsxs("p",{children:["If the function ",e.jsx("em",{children:"receives"})," a GHL webhook (rather than being called by your own app), verify it's really from GHL before acting â otherwise anyone with the URL can POST fake events. Check the shared secret or signature header GHL sends, and reject on mismatch. Same principle as any webhook re
23ceiver: trust nothing until it's verified."]}),e.jsx("h2",{children:"Step 5: Deploy and (optionally) schedule"}),e.jsx("pre",{style:{background:"var(--tm-code-bg)",color:"var(--tm-code-text)",padding:16,borderRadius:4,marginTop:8,fontSize:13,overflowX:"auto"},children:`supabase functions deploy ghl-availability 24# public URL: 25# https://<project-ref>.supabase.co/functions/v1/ghl-availability`}),e.jsxs("p",{children:["For recurring jobs (nightly sync, pipeline hygiene), trigger the function on a schedule with ",e.jsx("code",{children:"pg_cron"})," + ",e.jsx("code",{children:"pg_net"})," from inside Postgres, so the whole thing stays within Supabase."]}),e.jsx("h2",{children:"Common gotchas"}),e.jsxs("p",{children:[e.jsx("strong",{children:"1. CORS."})," If a browser calls the function (a booking widget), you must return CORS headers and handle the ",e.jsx("code",{children:"OPTIONS"}),` preflight explicitly â Deno doesn't add them for you. Missing CORS is the #1 "works in curl, fails in the browser" cause.`]}),e.jsxs("p",{children:[e.jsx("strong",{children:"2. Cold starts."})," Edge Functions cold-start fast but not instantly. On a latency-sensitive path (a live voice call), keep the function warm or accept the first-call delay â see the latency notes in the ",e.jsx(t,{to:"/guide/vapi-ghl-voice-agent",style:{color:"var(--tm-accent)"},children:"Vapi voice agent guide"}),"."]}),e.jsxs("p",{children:[e.jsx("strong",{children:"3. Secret rotation."})," When you rotate the GHL token, ",e.jsx("code",{children:"supabase secrets set"})," the new value and redeploy â running functions read secrets at boot, so a rotation without redeploy leaves the old token in memory until the next cold start."]}),e.jsxs("p",{children:[e.jsx("strong",{children:"4. The service-role key."})," If the function writes to your database, it uses the service-role key, which ",e.jsx("em",{children:"bypasses RLS"}),". Scope what the function does carefully â see ",e.jsx(t,{to:"/lab/supabase-rls-multi-tenant-agencies",style:{color:"var(--tm-accent)"},children:"Supabase RLS patterns for multi-tenant agencies"}),"."]}),e.jsx("p",{style:{marginTop:32},children:e.jsx(t,{to:"/workflows",style:{color:"var(--tm-accent)"},children:"Browse 4,900+ ready-to-import workflow templates â"})})]}),u=()=>e.jsx(n,{tier:2,slug:"supabase-edge-function-ghl",title:"How to call the GoHighLevel API from a Supabase Edge Function",subtitle:"A Deno Edge Function that authenticates to GHL, checks calendar availability, and books â with secrets, signature verification, CORS, and the deploy pattern this site's own booking runs on.",lastUpdated:"2026-09-03",readTime:"6",techStack:["Supabase","Deno","GoHighLevel","Edge Functions","Calendar API"],leadMagnet:{label:"Skip the scaffold. Download the starter Edge Function â a Deno handler with GHL auth from secrets, the free-slots and book-appointment calls, CORS + OPTIONS handling, and a signature-verification stub. Drop it into supabase/functions and deploy.",downloadUrl:"/workflows/ghl-availability-edge-function.ts"},relatedPages:[{title:"Fix: GoHighLevel calendar booking stopped working",slug:"ghl-calendar-booking-failures",tier:1},{title:"How to build a Vapi voice agent that books into GHL",slug:"vapi-ghl-voice-agent",tier:2},{title:"Supabase RLS patterns for multi-tenant agencies",slug:"supabase-rls-multi-tenant-agencies",tier:3}],relatedServices:["Supabase","GoHighLevel"],howTo:[{name:"Scaffold the function",text:"Run supabase functions new ghl-availability to create supabase/functions/ghl-availability/index.ts with a Deno.serve handler."},{name:"Authenticate to GHL from secrets",text:"Set the token with supabase secrets set GHL_TOKEN=... and read it from Deno.env at runtime. Include the mandatory Version: 2021-07-28 header â a valid token without it still returns 401."},{name:"Write the availability/booking handler",text:"Parse the request, call the GHL calendar free-slots or appointment endpoint once, and return JSON. Keep it to one GHL call per invocation for speed and debuggability."},{name:"Verify inbound webhook signatures",text:"If the function receives a GHL webhook, verify the shared secret or signature header before acting, and reject on mismatch, so no one with the URL can POST fake events."},{name:"Deploy and optionally schedule",text:"Run supabase functions deploy ghl-
25availability for a public URL. For recurring jobs, trigger it on a schedule with pg_cron plus pg_net from inside Postgres."}],faq:[{q:"Should I use a Supabase Edge Function or n8n for GHL glue?",a:"Use n8n when a non-developer maintains it or you want a visual audit trail. Use an Edge Function when the glue belongs inside a Supabase app you own â version-controlled, deployed with the CLI, and running next to your database."},{q:"How do I keep the GHL token out of my Edge Function code?",a:"Set it as a Supabase secret with supabase secrets set GHL_TOKEN=... and read it via Deno.env.get at runtime. Never paste the token into source. After rotating the token, redeploy â functions read secrets at boot."},{q:"Why does my Edge Function work in curl but fail from the browser?",a:"Missing CORS. Deno doesn't add CORS headers for you â return them explicitly and handle the OPTIONS preflight request, or the browser blocks the call."},{q:"Does an Edge Function writing to my database respect RLS?",a:"No â if it uses the service-role key it bypasses RLS entirely. Scope what the function does carefully and treat the service role as trusted infrastructure, not a user context."}],body:a});export{u as default};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.