ScaleRocket/Web

API Calls

How to create and call backend functions — the serverless API layer for ScaleRocket.

API Calls

ScaleRocket uses Supabase Edge Functions as the API layer. Edge Functions are serverless TypeScript functions that run on Deno, close to your users. They handle things like processing payments, deducting credits, and any server-side logic.

ScaleRocket uses Convex Functions as the API layer. Convex functions are TypeScript functions that run on the Convex backend. They handle things like processing payments, deducting credits, and any server-side logic. Convex provides three types of functions: queries (read data), mutations (write data), and actions (call external APIs).

Directory Structure

Edge Functions live in supabase/functions/. Each folder is a separate function:

supabase/functions/
├── stripe-checkout/
│   └── index.ts       # Create Stripe checkout session
├── stripe-portal/
│   └── index.ts       # Create Stripe billing portal session
├── stripe-webhook/
│   └── index.ts       # Handle Stripe webhook events
├── use-credits/
│   └── index.ts       # Deduct credits from user balance
├── delete-account/
│   └── index.ts       # Delete user account and data
└── _shared/
    └── supabase.ts    # corsHeaders helper and admin client

Note: Folders starting with _ (like _shared) are not deployed as functions — they are shared modules you can import from.

Convex functions live in the convex/ directory. Each file can export multiple functions:

convex/
├── schema.ts          # Database schema
├── auth.config.ts     # Auth configuration
├── http.ts            # HTTP endpoints (webhooks)
├── stripe.ts          # Stripe checkout, portal, webhook logic
├── credits.ts         # Credit balance and deduction
├── users.ts           # User profile and account management
└── _generated/
    ├── api.d.ts       # Auto-generated API types
    └── server.d.ts    # Auto-generated server types

Note: The _generated/ directory is auto-generated by Convex and provides type-safe API references.

Creating a New Backend Function

1. Scaffold the Function

npx supabase functions new my-function

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

2. Write the Function

// supabase/functions/my-function/index.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: authError } = await supabaseAdmin.auth.getUser(token);
    if (authError || !user) {
      return new Response(
        JSON.stringify({ error: "Unauthorized" }),
        { status: 401, headers: { ...corsHeaders, "Content-Type": "application/json" } }
      );
    }

    // Parse request body
    const { someParam } = await req.json();

    // Your logic here
    const result = { message: `Hello ${user.email}`, someParam };

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

3. Test Locally

npx supabase functions serve my-function

The function is available at http://localhost:54321/functions/v1/my-function.

4. Deploy

npx supabase functions deploy my-function

1. Create a new file or add to an existing one

Create a file in the convex/ directory (e.g., convex/myFeature.ts).

2. Write the Function

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

// Query: read data (automatically reactive)
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();
  },
});

// Mutation: write data (transactional)
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,
    });
  },
});

// Action: call external APIs
export const callExternalApi = action({
  args: { someParam: v.string() },
  handler: async (ctx, args) => {
    const userId = await getAuthUserId(ctx);
    if (!userId) throw new Error("Unauthorized");

    const response = await fetch("https://api.example.com", {
      method: "POST",
      body: JSON.stringify({ param: args.someParam }),
    });

    return await response.json();
  },
});

3. Test Locally

Functions are available immediately when running:

npx convex dev

4. Deploy

npx convex deploy

Calling from the Client

ScaleRocket provides an API utility that wraps supabase.functions.invoke():

// apps/app/src/lib/api.ts
import { supabase } from "./supabase";

export async function callApi<T>(
  functionName: string,
  body?: Record<string, unknown>
): Promise<T> {
  const { data, error } = await supabase.functions.invoke(functionName, {
    body,
  });

  if (error) {
    throw new Error(error.message || "API call failed");
  }

  return data as T;
}

Use it in your components:

// apps/app/src/features/dashboard/SomeComponent.tsx
import { callApi } from "../../lib/api";

async function handleAction() {
  try {
    const result = await callApi("my-function", {
      someParam: "value",
    });
    console.log(result);
  } catch (error) {
    console.error("API error:", error);
  }
}

Convex provides type-safe hooks for calling functions directly:

// apps/app/src/features/dashboard/SomeComponent.tsx
import { useQuery, useMutation, useAction } from "convex/react";
import { api } from "../../convex/_generated/api";

function SomeComponent() {
  // Queries: reactive, auto-updating
  const data = useQuery(api.myFeature.getData);

  // Mutations: for writing data
  const createItem = useMutation(api.myFeature.createItem);

  // Actions: for external API calls
  const callExternal = useAction(api.myFeature.callExternalApi);

  async function handleAction() {
    try {
      await createItem({ content: "value" });
      // data updates automatically -- no refetch needed
    } catch (error) {
      console.error("API error:", error);
    }
  }
}

No API wrapper needed -- Convex hooks are fully type-safe and handle loading/error states.

Authentication in Backend Functions

The Supabase client automatically forwards the user's JWT to Edge Functions. On the server side, you verify it:

// 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")!
);

The supabaseAdmin client uses the service role key and bypasses all Row Level Security policies. Use it for server-side operations like updating subscription data from webhooks or verifying user tokens.

Note: The admin client is appropriate in Edge Functions because they run server-side. Never expose the service role key to client-side code.

Convex handles authentication automatically. Use getAuthUserId to get the current user in any function:

import { getAuthUserId } from "@convex-dev/auth/server";

// In any query, mutation, or action:
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error("Unauthorized");

No manual JWT verification is needed -- Convex manages session tokens automatically.

Error Handling

ScaleRocket uses consistent error responses across all backend functions:

// Success response
return new Response(
  JSON.stringify({ data: result }),
  { status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);

// Client error
return new Response(
  JSON.stringify({ error: "Invalid input", details: "Name is required" }),
  { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);

// Server error
return new Response(
  JSON.stringify({ error: "Internal server error" }),
  { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
);

On the client, the callApi utility throws on errors, so you can use try/catch:

try {
  const data = await callApi("my-function", { input: "value" });
  // Handle success
} catch (error) {
  // error.message contains the error from the Edge Function
  toast.error(error.message);
}

Convex functions throw errors directly. The client hooks handle them automatically:

// In Convex functions, just throw:
if (!userId) throw new Error("Unauthorized");
if (credits.balance < cost) throw new Error("Insufficient credits");

// On the client:
try {
  await createItem({ content: "value" });
} catch (error) {
  toast.error(error.message);
}

CORS Configuration

Edge Functions need CORS headers for browser requests:

// supabase/functions/_shared/supabase.ts (corsHeaders export)
export const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Headers":
    "authorization, x-client-info, apikey, content-type",
};

Note: In production, replace * with your actual domain for better security.

Convex handles CORS automatically for all function calls via the Convex client. No manual CORS configuration is needed.

For HTTP endpoints (like webhooks), you can configure CORS headers in convex/http.ts if needed.

Environment Variables

Edge Functions have access to Supabase variables automatically (SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY). For custom secrets:

# Set a secret for Edge Functions
npx supabase secrets set MY_SECRET=value

# List all secrets
npx supabase secrets list

Access them in your function:

const mySecret = Deno.env.get("MY_SECRET");

Set environment variables for Convex functions:

# Set an environment variable
npx convex env set MY_SECRET value

# List all environment variables
npx convex env list

Access them in your function:

const mySecret = process.env.MY_SECRET;

Next Steps

Done reading? Mark this page as complete.

On this page