ScaleRocket/Web

Edge Functions

Server-side functions for payments, emails, and API endpoints.

Overview

ScaleRocket uses Supabase Edge Functions for all server-side logic. They run on Deno Deploy, close to users globally, and handle payments, emails, auth validation, and custom API endpoints.

ScaleRocket uses Convex Functions for all server-side logic. They run on the Convex cloud platform and handle payments, emails, auth validation, and custom API endpoints. Convex provides three function types: queries (read), mutations (write), and actions (external API calls).

What Are They

Edge Functions are TypeScript functions that run on Supabase's edge network (Deno Deploy). They replace traditional API routes and serverless functions:

  • Runtime: Deno (not Node.js)
  • Location: supabase/functions/
  • URL pattern: https://<project>.supabase.co/functions/v1/<function-name>
  • Auth: JWT verification built-in

Convex Functions are TypeScript functions that run on the Convex platform. They replace traditional API routes and serverless functions:

  • Runtime: Node.js (V8 isolate)
  • Location: convex/
  • Access: Type-safe hooks (useQuery, useMutation, useAction)
  • Auth: Built-in via @convex-dev/auth
  • HTTP endpoints: convex/http.ts for webhooks and external calls

Available Functions

FunctionPurposeTrigger
stripe-checkoutCreates a Stripe Checkout sessionClient call
stripe-portalCreates a Stripe Billing Portal sessionClient call
stripe-webhookProcesses Stripe webhook eventsStripe
use-creditsDeducts credits atomicallyClient call
delete-accountDeletes user account and dataClient call
FunctionTypePurposeTrigger
stripe.createCheckoutSessionActionCreates a Stripe Checkout sessionClient call
stripe.createPortalSessionActionCreates a Stripe Billing Portal sessionClient call
stripe.handleWebhookHTTP endpointProcesses Stripe webhook eventsStripe
credits.deductCreditsMutationDeducts credits atomicallyClient call
users.deleteAccountActionDeletes user account and dataClient call

Directory Structure

supabase/functions/
├── _shared/                    # Shared utilities
│   └── supabase.ts             # corsHeaders helper and admin client
├── stripe-checkout/
│   └── index.ts
├── stripe-portal/
│   └── index.ts
├── stripe-webhook/
│   └── index.ts
├── use-credits/
│   └── index.ts
└── delete-account/
    └── index.ts
convex/
├── _generated/                 # Auto-generated types (do not edit)
│   ├── api.d.ts
│   └── server.d.ts
├── schema.ts                   # Database schema
├── auth.config.ts              # Auth configuration
├── http.ts                     # HTTP endpoints (webhooks)
├── stripe.ts                   # Stripe checkout, portal, webhook handlers
├── credits.ts                  # Credit balance and deduction
└── users.ts                    # User profile and account management

Shared Utilities

The _shared/ directory contains reusable code imported by all functions. It is not deployed as a function itself. Everything lives in a single supabase.ts file.

_shared/supabase.ts

// supabase/functions/_shared/supabase.ts
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";

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

export const supabaseAdmin = createClient(
  Deno.env.get("SUPABASE_URL")!,
  Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);

Every function starts with a CORS check and imports from _shared/supabase.ts:

import { corsHeaders, supabaseAdmin } from "../_shared/supabase.ts";

Deno.serve(async (req) => {
  // Handle CORS preflight
  if (req.method === "OPTIONS") {
    return new Response("ok", { headers: corsHeaders });
  }

  try {
    // Verify the user's JWT
    const authHeader = req.headers.get("Authorization");
    if (!authHeader) throw new Error("No authorization header");

    const token = authHeader.replace("Bearer ", "");
    const { data: { user }, error } = await supabaseAdmin.auth.getUser(token);
    if (error || !user) throw new Error("Invalid token");

    // ... function logic

    return new Response(JSON.stringify({ success: true }), {
      headers: { ...corsHeaders, "Content-Type": "application/json" },
    });
  } catch (error) {
    return new Response(JSON.stringify({ error: error.message }), {
      status: 401,
      headers: { ...corsHeaders, "Content-Type": "application/json" },
    });
  }
});

Convex functions share utilities through regular TypeScript imports. Auth is handled via the @convex-dev/auth library:

// convex/stripe.ts
import { action, internalMutation } from "./_generated/server";
import { v } from "convex/values";
import { getAuthUserId } from "@convex-dev/auth/server";
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export const createCheckoutSession = action({
  args: { priceId: v.string() },
  handler: async (ctx, args) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) throw new Error("Unauthorized");

    // ... function logic
  },
});

No CORS handling needed -- Convex manages this automatically for client function calls.

Creating a New Function

1. Generate the function

pnpm supabase functions new my-function

This creates supabase/functions/my-function/index.ts.

2. Write the handler

// supabase/functions/my-function/index.ts
import { corsHeaders, supabaseAdmin } from "../_shared/supabase.ts";

Deno.serve(async (req) => {
  if (req.method === "OPTIONS") {
    return new Response("ok", { headers: corsHeaders });
  }

  try {
    const authHeader = req.headers.get("Authorization");
    if (!authHeader) throw new Error("No authorization header");

    const token = authHeader.replace("Bearer ", "");
    const { data: { user }, error: authError } = await supabaseAdmin.auth.getUser(token);
    if (authError || !user) throw new Error("Invalid token");

    const { someParam } = await req.json();

    // Your logic here
    const { data, error } = await supabaseAdmin
      .from("my_table")
      .select("*")
      .eq("user_id", user.id);

    return new Response(JSON.stringify({ data }), {
      headers: { ...corsHeaders, "Content-Type": "application/json" },
    });
  } catch (error) {
    return new Response(JSON.stringify({ error: error.message }), {
      status: 400,
      headers: { ...corsHeaders, "Content-Type": "application/json" },
    });
  }
});

3. Test locally

pnpm supabase functions serve my-function

4. Call from the client

const { data, error } = await supabase.functions.invoke("my-function", {
  body: { someParam: "value" },
});

5. Deploy

pnpm supabase functions deploy my-function

1. Create a file in the convex/ directory

Create convex/myFeature.ts (or add to an existing file).

2. Write the function

// convex/myFeature.ts
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
import { getAuthUserId } from "@convex-dev/auth/server";

export const getData = query({
  args: {},
  handler: async (ctx) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) throw new Error("Unauthorized");

    return await ctx.db
      .query("myTable")
      .withIndex("by_userId", (q) => q.eq("userId", userId))
      .collect();
  },
});

export const createItem = mutation({
  args: { content: v.string() },
  handler: async (ctx, args) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) throw new Error("Unauthorized");

    return await ctx.db.insert("myTable", {
      userId,
      content: args.content,
    });
  },
});

3. Test locally

Functions are available automatically when running npx convex dev.

4. Call from the client

import { useQuery, useMutation } from "convex/react";
import { api } from "../convex/_generated/api";

const data = useQuery(api.myFeature.getData);
const createItem = useMutation(api.myFeature.createItem);

await createItem({ content: "value" });

5. Deploy

npx convex deploy

Environment Variables

Edge Functions access secrets via Deno.env.get(). Set them with:

# Local development -- add to supabase/.env
STRIPE_SECRET_KEY=sk_test_xxx

# Production
pnpm supabase secrets set STRIPE_SECRET_KEY=sk_live_xxx

SUPABASE_URL, SUPABASE_ANON_KEY, and SUPABASE_SERVICE_ROLE_KEY are available automatically.

Convex functions access environment variables via process.env. Set them with:

# Local development (uses dev deployment)
npx convex env set STRIPE_SECRET_KEY sk_test_xxx

# Production
npx convex env set STRIPE_SECRET_KEY sk_live_xxx --prod

List all environment variables:

npx convex env list

Done reading? Mark this page as complete.

On this page