Supabase Integration

useHUMA
+ Supabase.

Protect your Supabase app from bots and fake signups — without CAPTCHAs. A single API call returns a real-time trust score (0–100). No PII stored. Under 200ms.

<200ms
Latency
0 PII
Data stored
5 min
To integrate
HOWHow it works
1.Your frontend collects a behavioral token as the user fills your form.
2.Your backend (Edge Function or Server Action) sends the token to useHUMA.
3.useHUMA returns a trust score in real time — no PII, no biometrics stored.
4.You allow or block the Supabase action based on the score threshold.
01
Get your API key

Sign up at humaverify.com and grab your API key from the dashboard. 14-day free trial — no credit card required.

HUMA_API_KEY=huma_live_xxxxxxxxxxxxxxxxxxxxxxxx

Add this to your Supabase Edge Function secrets or .env.local.

02
Add the frontend script

Add this to your layout.tsx or _app.tsx:

<script src="https://humaverify.com/huma.js"></script>

Then collect the behavioral signals on form submit:

const handleSubmit = async (e) => {
  e.preventDefault();

  // Anonymous behavioral signals — no PII.
  // If huma.js was blocked by an extension this is undefined. Send it as
  // undefined rather than {}: an empty object means "we watched and saw
  // nothing", which scores as a bot, while a missing field means "we have
  // no data", which your server should let through. A privacy extension is
  // far more common on a developer's machine than a bot on your form.
  const signals = window.Huma ? window.Huma.debug().features : undefined;

  // Call your own endpoint. Do NOT pass this to supabase.auth.signUp():
  // it accepts a fixed shape and silently drops anything it does not know,
  // so the signals would never leave the browser.
  await fetch('/functions/v1/verify-human', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password, signals }),
  });
};
03
Verify in a Supabase Edge Function

Create supabase/functions/verify-human/index.ts:

import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'

serve(async (req) => {
  const { signals, email, password } = await req.json()

  // Verify with useHUMA
  const humaRes = await fetch('https://humaverify.com/api/v1/verify', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${Deno.env.get('HUMA_API_KEY')}`,
    },
    // Pseudonymous id, never the raw email: it is stored with the record.
    // Hash it, or use whatever internal id you already have.
    body: JSON.stringify({ userId: await sha256(email), sessionData: signals }),
  })

  // Fail open. If useHUMA is rate limited, down, or slow, this must not become
  // your outage: let the request through. Reading .human off a non-200 body
  // gives undefined, and !undefined is true, which would 403 every real user
  // the moment anything went wrong on our side.
  let human = true
  if (humaRes.ok) {
    const result = await humaRes.json()
    human = result.human
    // Tells you when a field name did not land. Log it while integrating.
    if (result.warnings) console.warn('[huma]', result.warnings.join(' '))
  }

  if (!human) {
    return new Response(
      JSON.stringify({ error: 'Verification failed. Please try again.' }),
      { status: 403, headers: { 'Content-Type': 'application/json' } }
    )
  }

  // Proceed with Supabase signup
  const { createClient } = await import('https://esm.sh/@supabase/supabase-js@2')
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  )

  const { data, error } = await supabase.auth.admin.createUser({
    email, password, email_confirm: true,
  })

  if (error) return new Response(
    JSON.stringify({ error: error.message }),
    { status: 400, headers: { 'Content-Type': 'application/json' } }
  )

  return new Response(
    JSON.stringify({ user: data.user }),
    { status: 200, headers: { 'Content-Type': 'application/json' } }
  )
})
04
Alternative — Next.js Server Actions

Add useHUMA to your app/signup/actions.ts:

'use server'

export async function signUp(formData: FormData) {
  const email = formData.get('email') as string
  const password = formData.get('password') as string
  // This field does not exist unless you create it. Add a hidden input to the
  // form and fill it from the collector before submit:
  //   <input type="hidden" name="huma_signals" />
  //   el.value = JSON.stringify(window.Huma.debug().features)
  const raw = formData.get('huma_signals')
  const signals = typeof raw === 'string' && raw ? JSON.parse(raw) : undefined

  const humaRes = await fetch('https://humaverify.com/api/v1/verify', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${process.env.HUMA_API_KEY}`,
    },
    body: JSON.stringify({ userId: email, sessionData: signals }),
  })

  // Fail open, same reason as above: a 429 or a 503 must not block your users.
  let human = true
  if (humaRes.ok) human = (await humaRes.json()).human

  if (!human) {
    return { error: 'Verification failed. Please try again.' }
  }

  const supabase = createClient()
  const { error } = await supabase.auth.signUp({ email, password })

  if (error) return { error: error.message }
  return { success: true }
}
REF
Trust Score Reference
0.75–1.0High confidence humanAllow
0.50–0.74Likely humanAllow + log
0.25–0.49Little to go onAdd friction
0.0–0.24Automation detectedBlock

confidence is a decimal from 0 to 1. The score field on the session heartbeat is the same number as 0 to 100, which is the only place that scale appears. You rarely need either: every verify response also returns human, already compared against your account threshold, and the threshold itself so you never have to guess which number decided it.

Ready to protect your app?
Start your free trial
14-day free trial · No credit card required · Under 5 min to integrate
Get your free API key →
Help