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.tsfor webhooks and external calls
Available Functions
| Function | Purpose | Trigger |
|---|---|---|
stripe-checkout | Creates a Stripe Checkout session | Client call |
stripe-portal | Creates a Stripe Billing Portal session | Client call |
stripe-webhook | Processes Stripe webhook events | Stripe |
use-credits | Deducts credits atomically | Client call |
delete-account | Deletes user account and data | Client call |
| Function | Type | Purpose | Trigger |
|---|---|---|---|
stripe.createCheckoutSession | Action | Creates a Stripe Checkout session | Client call |
stripe.createPortalSession | Action | Creates a Stripe Billing Portal session | Client call |
stripe.handleWebhook | HTTP endpoint | Processes Stripe webhook events | Stripe |
credits.deductCredits | Mutation | Deducts credits atomically | Client call |
users.deleteAccount | Action | Deletes user account and data | Client 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.tsconvex/
├── _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 managementShared 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-functionThis 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-function4. Call from the client
const { data, error } = await supabase.functions.invoke("my-function", {
body: { someParam: "value" },
});5. Deploy
pnpm supabase functions deploy my-function1. 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 deployEnvironment 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_xxxSUPABASE_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 --prodList all environment variables:
npx convex env listDone reading? Mark this page as complete.