AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Edge Functions

skill-tomaspozo-agentlink-edge-functions · by tomaspozo

Supabase Edge Functions. Use when the task involves creating, modifying, or debugging edge functions, webhooks, external API integrations, service-to-service calls, or anything that runs in the Deno edge runtime. Also use for the native MCP server — exposing app functionality over MCP, adding an MCP tool, or connecting Claude/Cursor/an MCP client to the app (supabase/functions/mcp/, withMCP, work…

No reviews yet
0 installs
10 views
0.0% view→install

Install

$ agentstack add skill-tomaspozo-agentlink-edge-functions

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-tomaspozo-agentlink-edge-functions)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
1mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Edge Functions? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Edge Functions

Edge Functions handle everything that needs to talk to the outside world — webhooks, third-party APIs, scheduled triggers, service-to-service calls. They are not for CRUD or business logic (that belongs in database functions via RPCs).

Outbound HTTP always runs in an edge function — this is not a choice

Any request to a URL outside Postgres — pinging a monitored endpoint, calling a third-party API, hitting an LLM, sending a webhook — runs in an edge function, driven by cron + a queue. Never make the outbound request from in-database pg_net. This is settled architecture: don't surface it as a decision to the user, don't ask "edge function worker vs. in-database pg_net" — the answer is always the edge function.

  • pg_net (net.http_post) is only the trigger transport: pg_cronpublic._internal_admin_call_edge_function(...) makes one HTTP call to wake an edge function. That is its entire sanctioned role. It is never the HTTP client for business logic — no per-row fetches, no fan-out to third parties, no response handling in SQL.
  • Why the edge function wins every time: it gets timeouts, retries, redirect handling, secrets, real error handling, observability, and the Deno ecosystem. pg_net is fire-and-forget from inside a transaction with none of that — N outbound calls from SQL tie up the database and silently drop failures.

Canonical flow for "do work against the outside world on a schedule":

pg_cron (e.g. every 1m)
  └─ public._internal_admin_call_edge_function('internal-')   ← pg_net's ONLY job: one call to wake the worker
       └─ edge fn: RPC to fetch the due set (api._admin_… reads the queue / due rows)
       └─ edge fn: fetch each external URL (timeout, redirects, retries)
       └─ edge fn: RPC to write results back

For bursty or per-item work, enqueue into PGMQ (api.agentlink_tasks) and let internal-queue-worker drain it — cron just wakes the worker. See the [database](../database/SKILL.md) cron + queue conventions, and [recipes.md](../../agents/references/recipes.md) for full worked examples (scheduled URL pinger, queued email, periodic sync). For sending app-driven (non-auth) email specifically, use the [notifications](../notifications/SKILL.md) skill — it rides this same queue via api._admin_send_emailinternal-send-email.

IMPORTANT!

  • All database access uses .rpc() — never .from(). The public schema is not exposed via the Data API, so .from() cannot reach tables. Use ctx.supabase.rpc() or ctx.supabaseAdmin.rpc() to call functions in the api schema.
  • Every edge function uses the withSupabase wrapper. No exceptions.
  • Every edge function needs its own deno.json with pinned dependency versions — the global one is excluded during deployment.
  • Every edge function must have verify_jwt = false in supabase/config.toml:
[functions.my-function]
verify_jwt = false

This is required because the withSupabase wrapper handles auth itself. If verify_jwt is left as true (the default), Supabase's gateway rejects requests before they reach the wrapper.

Quick Start

First edge function in a project?

The _shared/ utilities (responses.ts) should already exist in supabase/functions/_shared/ — the CLI sets these up. If missing, run npx agentlink-sh@latest.

The withSupabase wrapper comes from the @supabase/server npm package, resolved via per-function deno.json import maps.

Creating a new function

  1. Create the function directorysupabase/functions/my-function/
  2. Add deno.json with pinned dependency versions:

``jsonc // supabase/functions/my-function/deno.json { "imports": { "@supabase/server": "npm:@supabase/server@0.1.0-alpha.1", "@supabase/supabase-js": "npm:@supabase/supabase-js@2" } } ` Always pin versions — exact for pre-release (@0.1.0-alpha.1), major for stable (@2`). Never use unversioned specifiers.

  1. Choose the auth type — who can call this function? (see Selection Guide below)
  2. Write index.ts using withSupabase:
import { withSupabase } from "@supabase/server";
import { jsonResponse, errorResponse } from "../_shared/responses.ts";

export default {
  fetch: withSupabase({ auth: "user", supabaseOptions: { db: { schema: "api" } } }, async (_req, ctx) => {
    try {
      const { data, error } = await ctx.supabase.rpc("my_rpc_function");

      if (error) return errorResponse(error.message);
      return jsonResponse(data);
    } catch (err) {
      console.error("Unhandled error:", err);
      return errorResponse("Internal server error", 500);
    }
  }),
};
  1. Add to config.toml:

``toml [functions.my-function] enabled = true verify_jwt = false ``

  1. Test — Local: npx supabase functions serve / Deploy: pnpm exec agentlink env deploy (pushes schemas + functions to the chosen env). Direct npx supabase functions deploy --use-api also works for functions-only pushes.

Selection Guide

| Scenario | Allow | Why | | ------------------------------------------------- | --------------------- | ----------------------------------------------------- | | User clicks a button in the app | "user" | Need user identity + RLS-scoped queries | | External webhook (Stripe, GitHub) | "public" | No Supabase JWT — validate webhook signature yourself | | Supabase Auth Hook | "public" | Called by Supabase Auth, not a user session | | Public API / health check | "public" | Open access, no auth needed | | Cron job / scheduled function | "secret" | No user context — needs secret key validation | | Called from DB via _internal_admin_call_edge_function | "secret" | DB calls use the secret key | | Called by users AND by other services | ["user", "secret"] | Dual-auth — accepts either credential |

When in doubt: logged-in user → "user". External service → "public". Internal infrastructure → "secret".

Naming convention: internal- prefix for system-only functions

Functions that are never called from client code — they're only enqueued via PGMQ, fired via pg_net, or invoked by an auth hook — get an internal- prefix in their directory name. Functions called by clients (frontend code, external webhooks, public APIs) get a bare name.

| Pattern | Examples | When to use | |---|---|---| | Bare name | stripe-webhook, share-image, chart-render | Anything a client (or external service) might hit. | | internal- prefix | internal-send-auth-email, internal-send-email, internal-queue-worker | Queue workers, auth-hook handlers, cron-only functions, anything wrapped with auth: "secret" and never exposed to users. |

Three reasons:

  1. Discoverability. A reader scanning supabase/functions/ sees the privilege boundary at a glance — no need to open index.ts to learn whether a function is user-facing.
  2. Symmetry with the SQL convention. _internal_admin_* (DEFINER helpers in public) and internal-* (system-only edge functions) share the same word for the same concept: "system, not client."
  3. Safety net for routing accidents. If the agent later writes enqueue('send-email') somewhere new, the missing internal- prefix won't match anything in the queue dispatcher — fails loudly instead of silently calling the wrong function.

Don't use a leading underscore (_internal-foo) — Supabase treats top-level _* directories as non-deployable (which is why _shared/ works as a non-deployed module). The hyphenated internal- prefix is the safe form.

Edge function names beginning with internal- should always be paired with auth: "secret". If you find yourself writing auth: "user" on an internal-* function, the prefix is wrong.

> For the full wrapper API, dual-auth patterns, anti-patterns, and context reference, load [withSupabase Reference](./references/with_supabase.md).


Secrets

Edge functions need SUPABASE_PUBLISHABLE_KEY and SUPABASE_SECRET_KEY configured as secrets — they are not available by default.

# Local development — add to supabase/.env or .env.local
SUPABASE_PUBLISHABLE_KEY=sb_publishable_...
SUPABASE_SECRET_KEY=sb_secret_...

# Cloud / Production — set via CLI (already configured by scaffold)
npx supabase secrets set SUPABASE_PUBLISHABLE_KEY=sb_publishable_...
npx supabase secrets set SUPABASE_SECRET_KEY=sb_secret_...

SUPABASE_URL is available by default and does not need to be set.


Project Structure

supabase/functions/
├── _shared/                       # Shared utilities (NOT deployed — leading `_`)
│   └── responses.ts               # Response helpers
├── _feature-name/                 # Feature-specific shared modules (NOT deployed)
│   └── helpers.ts
├── stripe-webhook/                # Client/external-facing — bare name
│   ├── index.ts
│   └── deno.json
├── internal-queue-worker/         # System-only (cron + pg_net) — `internal-` prefix
│   ├── index.ts
│   └── deno.json
├── internal-send-auth-email/      # System-only (auth hook → queue) — `internal-` prefix
│   ├── index.ts
│   └── deno.json
└── deno.json                      # Global — local dev fallback only

Folders prefixed with _ are shared modules — they are not deployed as edge functions. Folders prefixed with internal- ARE deployed but signal "system-only, never call from a client" by convention (paired with auth: "secret"). Every deployed function needs its own deno.json with its dependencies mapped — the global deno.json is excluded during deployment.


Native MCP Server

Your app ships a native MCP server at supabase/functions/mcp/ — a per-user, OAuth 2.1 protected resource server that lets Claude, Cursor, or any MCP client call your app's api.* RPCs on the user's behalf. It mirrors withSupabase: the function file is export default { fetch: withMCP(info, registerTools) }, and _shared/mcp.ts owns OAuth discovery, the caller's-JWT binding, and the per-request x-workspace-id workspace model.

To expose functionality over MCP, add a domain tool — one ctx.workspaceTool(...) per RPC:

import { withMCP, registerAdminTools, z } from "../_shared/mcp.ts";

export default {
  fetch: withMCP({ title: Deno.env.get("APP_NAME"), icon: ICON }, (ctx) => {
    registerAdminTools(ctx); // optional: 10 free workspace/member/invitation tools

    ctx.workspaceTool(
      "list_notes",
      { title: "List notes", description: "List notes in a workspace.", inputSchema: { status: z.string().optional() } },
      ({ supabase, status }) => supabase.rpc("note_list", { p_status: status ?? null }),
    );
  }),
};

workspaceTool adds the workspace arg, resolves it, binds a workspace-scoped client, calls your handler, and wraps the RPC { data, error }. The handler just calls the api.* RPC — the DB (auth_verify_access + RLS) enforces per-workspace access. Never use a service-role client in a tool (ctx has no supabaseAdmin — confused-deputy). Requires [functions.mcp] verify_jwt = false + [auth.oauth_server] enabled = true in config.toml.

> For the tool config, the ctx toolkit, registerAdminTools, and the cloud OAuth caveat, load [MCP Server Reference](./references/mcp.md).


Reference Files

Load these as needed:

  • [🔧 withSupabase Wrapper](./references/with_supabase.md) — Full wrapper API: auth types, dual-auth, clients, anti-patterns, context reference
  • [🔌 MCP Server](./references/mcp.md) — The native per-user OAuth MCP server: withMCP, adding a domain tool with workspaceTool, the ctx toolkit, registerAdminTools, config.toml + cloud OAuth caveat
  • [📦 Dependencies & Deployment](./references/dependencies.md) — Per-function deno.json, import maps, bare specifiers, sub-path mapping, version pinning, --use-api deployment
  • [📁 Edge Function Patterns](./references/edge_functions.md) — Folder structure details, response helpers, feature-specific modules
  • [🔑 API Key Migration](./references/apikeymigration.md) — Migrate from legacy anon/service_role keys to new publishable/secret keys
  • [📧 Resend setup](../cli/references/resend.md) — For functions that send email (internal-send-email, internal-send-auth-email, …): how RESEND_API_KEY / RESEND_FROM_EMAIL are configured per-env, local resend-box vs cloud, and the "email not sending" debug flow

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.