# Your first Edge Function

> Create, deploy, test and call an Edge Function on SupaHost from the dashboard in a few minutes, then update it with a new version.

Source: https://supahost.app/docs/functions/quickstart · Updated 2026-11-11

This walkthrough deploys a small function for a recipe app. It takes a list of ingredients and returns a shopping list sorted by aisle. You'll write it in the dashboard, deploy it, test it, and call it from your app. It takes about five minutes.

You need a SupaHost project and the owner role on the instance (only the owner can deploy).

## 1. Create the function

1. Open *Dashboard → your project → **Functions** tab* and click **New function**.
2. In **Name**, type `shopping-list`. The hint under the field shows the URL it will get: `https://app-one.acme.supahost.space/functions/v1/shopping-list`. Names use lowercase letters, numbers, `-` and `_`, and you can't change a name after the first deploy.
3. Leave **Require a JWT** on. Callers will need to send your project's anon key or a signed-in user's access token.

The editor opens with a starter `index.ts`. Replace its contents with:

```ts
const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};

const AISLES: Record<string, string> = {
  tomato: "Produce",
  basil: "Produce",
  garlic: "Produce",
  pasta: "Dry goods",
  rice: "Dry goods",
  parmesan: "Dairy",
  butter: "Dairy",
};

Deno.serve(async (req) => {
  // Browsers send a preflight request first. Answer it with the CORS headers.
  if (req.method === "OPTIONS") {
    return new Response("ok", { headers: corsHeaders });
  }

  const { ingredients } = await req.json().catch(() => ({ ingredients: null }));
  if (!Array.isArray(ingredients)) {
    return Response.json(
      { error: "Send a JSON body like { \"ingredients\": [\"tomato\", \"pasta\"] }" },
      { status: 400, headers: corsHeaders },
    );
  }

  const list: Record<string, string[]> = {};
  for (const item of ingredients.map((i: unknown) => String(i).toLowerCase().trim())) {
    const aisle = AISLES[item] ?? "Other";
    (list[aisle] ??= []).push(item);
  }

  console.log(`Built a list with ${ingredients.length} items`);
  return Response.json({ list }, { headers: corsHeaders });
});
```

## 2. Deploy it

Click **Deploy function**. The row shows **Deploying v1…** while SupaHost builds it. The build resolves every import and checks the code, which takes about a minute. When it's done the status changes to **Live · v1**.

If the build fails, the row shows **Deploy failed** with the error underneath. Fix the code with **Edit** and deploy again. See [Edge Function deploy failed](/kb/function-deploy-failed) for common causes.

## 3. Test it in the dashboard

Once a function is live, the **Test a function** card appears below the list:

1. Pick `shopping-list`, method **POST**, and **as anon key**.
2. Set the body to `{ "ingredients": ["Tomato", "pasta", "butter", "saffron"] }`.
3. Click **Send**.

You'll see the status code, the time it took and the JSON response. The card sends a real request, waits up to 60 seconds, and shows the first 64 KB of the body.

The **Logs** card further down now shows a request line (`POST /shopping-list → 200 in 12 ms`) and the `console.log` message.

## 4. Call it from your app

With supabase-js, `functions.invoke` sends the right headers for you. If a user is signed in it sends their access token, otherwise the anon key:

```ts
import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
  import.meta.env.VITE_SUPABASE_URL, // https://app-one.acme.supahost.space
  import.meta.env.VITE_SUPABASE_ANON_KEY,
);

const { data, error } = await supabase.functions.invoke("shopping-list", {
  body: { ingredients: ["tomato", "basil", "rice"] },
});

if (error) console.error(error);
else console.log(data.list); // { Produce: ["tomato", "basil"], "Dry goods": ["rice"] }
```

From a terminal:

```bash
curl -X POST "https://app-one.acme.supahost.space/functions/v1/shopping-list" \
  -H "Authorization: Bearer $SUPABASE_ANON_KEY" \
  -H "Content-Type: application/json" \
  -d '{"ingredients":["garlic","parmesan"]}'
```

Leave out the `Authorization` header and you'll get a `401` with `UNAUTHORIZED_NO_AUTH_HEADER`, which shows the JWT check is working.

## 5. Ship a change

Click **Edit** on the row, change the code (add `"onion": "Produce"` to the map, say) and click **Deploy new version**. The row shows **Deploying v2…**. Version 1 keeps answering requests until version 2 is built, so there's no gap. If version 2 fails to build, version 1 stays live.

## Deploying with an AI agent instead

If you've connected a coding agent through the [SupaHost MCP server](/docs/ai-tools/mcp), you can ask it to deploy the function. It uses the `deploy_edge_function` tool with the same files, then checks `list_edge_functions` until the status is `active`. See [Deploying functions](/docs/functions/deploying).

## Next steps

- [Writing functions: Deno, imports and structure](/docs/functions/writing-functions)
- [Authentication and JWT verification](/docs/functions/auth), to find out who is calling.
- [Environment variables and secrets](/docs/functions/secrets), to use API keys.
- [Calling functions from your app](/docs/functions/calling-functions)
