DEV Community

Cover image for How to Auto-Provision API Keys for Your Users on Sign Up with Supabase and Zuplo
Chidera Humphrey
Chidera Humphrey

Posted on

How to Auto-Provision API Keys for Your Users on Sign Up with Supabase and Zuplo

Introduction

You set up API key authentication for your Next.js routes. Zuplo is protecting your endpoints. Everything works—for you.

Now a real user signs up and lands in your developer portal but doesn't have any API keys to access your route. They can’t even create one themselves unless you do it for them. More than half of users won’t even bother to message you; they’ll drop off.

The fix is to auto-provision an API key the moment a user signs up, so when they land in the portal, the key is already there. No manual step. No support ticket.

This tutorial shows you how to wire Supabase Auth to Zuplo via a Supabase edge function. When a user signs up, the Supabase Auth hook fires an edge function that calls the Zuplo Developer API and creates a consumer and an API key. All before the user hits their first page.

This tutorial assumes you already have a Zuplo project with API key auth configured. If you don’t, this tutorial walks you through that first.

What this doesn’t cover: in-app key management, rotation, and revocation. Zuplo’s developer portal handles all of that out of the box; you won’t need to build it.

Looking to monetize your API? If you need Stripe subscriptions, plan upgrades, usage metering, and billing tied to API keys, Zuplo's Monetization feature handles all of that as an integrated system. This tutorial is for teams who want API key auto-creation on signup without full monetisation. If that's you, read on.

Section 1: How It Works

Before writing any code, it’s worth understanding what fires when. Because picking the wrong Supabase hook is the most common reason this setup silently fails.

Here’s the full sequence:

  1. A user signs up in the developer portal
  2. Supabase fires an Auth hook
  3. The hook calls your edge function
  4. The edge function calls the Zuplo Developer API, which creates an API consumer and key
  5. The user lands in the portal with their API key already configured

API key provisioning architecture

Section 2: Setting Up Supabase

Supabase handles your user auth: sign up, sign in, and email verification. If you already have a Supabase project, you only need to grab two values from it and check one setting.

If you’re starting fresh, go to Supabase and create a new project. Once your project is ready, copy the project URL and publishable (anon) key from the project settings.

The email/password auth method is enabled by default.

If you want users to verify their emails before signing in, enable email confirmation.

enabling email confirmation in Supabase

Note: With email confirmation enabled, you'll need to configure a redirect callback so Supabase knows where to send users after they verify their email. You do this in zudoku.config.tsx using the redirectAfterSignup field in the authentication config. Zuplo's Supabase auth docs cover this. For this tutorial, email confirmation is turned off to keep things simple.

Section 3: Configuring the Developer Portal

The developer portal’s behaviour is controlled by zudoku.config.tsx, which lives in the docs folder of your Zuplo project. This is where you tell the portal which auth provider to use, in this case, Supabase.

Go to the Code tab, open zudoku.config.tsx and replace the authentication field with the following:

authentication: {
    type: "supabase",
    providers: [],
    supabaseUrl: "your-supabase-url",
    supabaseKey: "your-publishable-key",
  }
Enter fullscreen mode Exit fullscreen mode

Let’s break down what’s happening in this Zuplo authentication configuration code:

  1. You configured authentication in the zudoku.config.tsx file by setting the type to “supabase”, which tells Zuplo to use Supabase as your authentication provider. This enables user management, login flows, and JWT validation through Supabase’s auth system.
  2. The supabaseUrl points to your specific Supabase project endpoint, and supabaseKey is the publishable/anonymous key that allows the gateway to verify JWTs and communicate with Supabase Auth without exposing admin credentials.
  3. The providers array is empty, meaning only Supabase’s default email/password authentication is enabled. But you could add social providers like Google or GitHub by populating this array (not included in this tutorial).

This authentication configuration integrates Supabase Auth directly into your API gateway, automatically protecting routes, validating tokens, and attaching user information to incoming requests without writing any custom authentication logic.

Replace the placeholder values with the project URL and publishable key you copied in Section 2.

Click Save. Zuplo automatically commits the change, rebuilds the gateway, and redeploys the portal. You don't need to push anything manually; the portal will reflect the new auth configuration within a few seconds.

Section 4: Creating the Edge Function

The edge function is the bridge between Supabase and Zuplo. When the Supabase Auth hook fires, it calls this function with the new user’s data. The function takes that data, calls the Zuplo Developer API, and creates a consumer with an API key attached—all in one round trip.

In your Supabase project dashboard, navigate to the Edge Functions tab.

edge function route in Supabase

Click 'Deploy a new function’ and select 'Via Editor’ (because you’ll write the edge function directly in the Supabase editor) to create a new edge function.

Replace the generated code with the following:

import { Webhook } from "https://esm.sh/standardwebhooks@1.0.0";
import { randomUUID } from "node:crypto";

Deno.serve(async (req: Request) => {
  if (req.method !== "POST") {
    return new Response("Method not allowed", { status: 405 });
  }

  const WEBHOOK_SECRET = Deno.env.get("BEFORE_USER_CREATED_SECRET");
  if (!WEBHOOK_SECRET) {
    console.error("Missing BEFORE_USER_CREATED_SECRET");
    return new Response(JSON.stringify({}), {
      status: 200,
      headers: { "content-type": "application/json" },
    });
  }

  const payload = await req.text();
  const headers = Object.fromEntries(req.headers);

  const wh = new Webhook(WEBHOOK_SECRET.replace("v1,whsec_", ""));
  let event: any;
  try {
    event = wh.verify(payload, headers);
  } catch (err) {
    console.error("Webhook verification failed:", err);
    return new Response("Unauthorized", { status: 401 });
  }

  const user = event.user;

  try {
    const ZUPLO_API_KEY = Deno.env.get("ZUPLO_API_KEY");
    if (!ZUPLO_API_KEY) throw new Error("Missing ZUPLO_API_KEY");

    const ZUPLO_ACCOUNT = Deno.env.get("ZUPLO_ACCOUNT_NAME")!;
        const API_KEY_BUCKET = Deno.env.get("ZUPLO_BUCKET_NAME")!;

    const consumerName = `c-${randomUUID()}`;

    const response = await fetch(
      `https://dev.zuplo.com/v1/accounts/${ZUPLO_ACCOUNT}/key-buckets/${API_KEY_BUCKET}/consumers?with-api-key=true`,
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${ZUPLO_API_KEY}`,
          "content-type": "application/json",
        },
        body: JSON.stringify({
          description: `Consumer for ${user.email ?? ""}`,
          managers: [user.email],
          metadata: { user_id: user.id },
          name: consumerName,
        }),
      }
    );

    const result = await response.json().catch(() => null);

    if (!response.ok) {
      console.error("Zuplo error:", result);
    } else {
      console.log(`Consumer created on signup: ${result?.id} for user ${user.id}`);
    }
  } catch (err) {
    // Don't block user creation
    console.error("Provisioning failed:", err);
  }

  // Before User Created expects an empty object back
  return new Response(JSON.stringify({}), {
    status: 200,
    headers: { "content-type": "application/json" },
  });
});
Enter fullscreen mode Exit fullscreen mode

Let’s break down what’s happening in this Supabase Edge Function code:

  1. You defined a Supabase webhook handler that triggers before a new user is created in your authentication system. The function first validates that the request uses the POST method and verifies the incoming webhook signature using a Standard Webhooks library with a secret from environment variables.
  2. Upon successful verification, the function extracts the new user’s email and ID from the verified event payload. It then calls the Zuplo API to automatically provision a new API consumer (and key) for this user, generating a unique consumer name with a random UUID and setting the user’s email as a manager on the consumer.
  3. The function includes careful error handling that logs failures but never blocks user creation—critical for maintaining a good signup experience. Regardless of whether Zuplo provisioning succeeds or fails, the webhook always returns an empty JSON object with a 200 status, confirming to Supabase that user creation should proceed.

This edge function creates seamless API key provisioning: when a user signs up via Supabase Auth, Zuplo automatically gets an API consumer ready for them behind the scenes, all without disrupting the user’s signup flow.

Adding the Secrets

The edge function needs three values now and one more after the next section:

  • Zuplo API key — authenticates your calls to the Zuplo Developer API
  • Zuplo organization name — scopes the API consumer to your org
  • Bucket Name — tells Zuplo which API key service to create the consumer in
  • BEFORE_USER_CREATED_SECRET: secret key generated when you create your Supabase Auth hook, which you’ll do in the next section.

Here’s where to find each one:

Zuplo API key

Click your avatar/logo → SettingsAPI Key. If you haven’t created one before, generate a new key and copy it.

Getting Zuplo API key

Zuplo organization name

This is the name you chose when creating your Zuplo organization.

getting your organization name in Zuplo

Bucket Name for the API key service

Go to the Services tab and select your API key service. Click Bucket Details to see the bucket name.

getting your API key service bucket name

Important: Make sure you select the right environment. A consumer created with a development bucket name will only appear in the development developer portal, not in production. (More on this in the Testing section.)

Adding the secrets to the Supabase edge function

In your edge function settings, go to Secrets and add variables with the values you copied.

Click “Deploy function”.

Your edge function is ready. Copy the function URL; you’ll need it in the next step.

Section 5: Wiring the Hook in Supabase

The edge function exists, but nothing is calling it yet. The Supabase Auth hook connects a new signup event to your function.

In your Supabase project dashboard, navigate to AuthenticationAuth Hooks.

Note: At the time of writing, the Auth Hooks feature is in beta, but it works reliably for this use case.

Click Add new hook and select Before User Created.

Before User Created is the right hook here because it fires exactly once—when a new user is created. Custom Access Token fires on every login, which means you'd be attempting to provision a consumer on every sign-in, not just the first time.

Select HTTPS as the hook type and paste in your edge function URL.

Click Generate new secret key. This secret prevents unauthorized callers from hitting your hook endpoint directly. Copy the secret key and save the hook.

Adding hook in Supabase Auth

Adding the Secret to the Edge Function

Go back to your edge function. Add a new secret named BEFORE_USER_CREATED_SECRET and set its value to the secret key you just generated.

The full integration is now wired together. Supabase will call your edge function on every new signup, and the function has everything it needs to create a Zuplo consumer.

Section 6: Testing It End to End

Open your developer portal. Which URL to use depends on which bucket name you configured in the edge function:

  • Production bucket: Click Deployment URL and select Production
  • Development bucket: Click 'Deployment URL’ and select 'Working copy’

getting your developer portal URL

Click Login and select Sign up. If you already have a user in your Supabase backend, use their credentials to sign in instead.

signing up/in in developer portal

After signing in/up, you’ll be redirected to the API Reference tab of the developer portal.

To verify the key was provisioned, click My AccountAPI Keys. You’ll see the key already attached to your account—no manual step required.

viewing your API in the developer portal

Better yet: when a user wants to test an API route in the portal, their key is pre-selected. They choose the consumer and click Send.

Troubleshooting

Below are the most common issues you’ll hit and how to fix them.

User signs up, but no API key is created

This usually means you’re hitting the wrong developer portal for the bucket environment you configured.

  • If you used a development bucket name in the edge function, select the “Working Copy" Developer Portal.
  • If you used a production bucket name, select the "Production” Developer Portal.

Edge Function Returns a 401 Error

Two things to check:

Verify JWT is disabled:
In your edge function settings, turn off “Verify JWT”. The hook request doesn’t carry a Supabase JWT, so leaving this on will reject every call.

Wrong hook type selected:
Make sure you selected the Before User Created hook and set the endpoint type to HTTPS (not Postgres function). Double-check that the edge function URL is correct.

Wrong or missing BEFORE_USER_CREATED_SECRET:

The edge function verifies the webhook signature on every request. If the BEFORE_USER_CREATED_SECRET value in your edge function secrets doesn't match the secret Supabase generated when you configured the hook, every request will fail verification and return a 401.

Go back to AuthenticationAuth Hooks, copy the secret again, and make sure the value in your edge function secrets matches it exactly, including the v1,whsec_ prefix.

Conclusion

Every user who signs up now gets a Zuplo consumer and API key automatically: no manual provisioning, no delay between signup and first API call.

From here, Zuplo’s developer portal handles the key management layer for free: users can roll their keys and edit their key label. For developer-facing APIs, that’s usually all you need.

If you want to go deeper—for example, tying API key creation to a billing plan or metering API usage per consumer—the next problem to solve is provisioning keys from inside your app rather than from a Supabase hook. That gives you the control to attach plan metadata at creation time and gate key generation behind a payment check. This article on the Zuplo blog covers in-app key management if you want to go that route.

Resources

Top comments (0)