ScaleRocket/Web

Adding AI Features

Integrate Claude, OpenAI, or any AI provider into your SaaS using Edge Functions and the credit system.

Adding AI Features

ScaleRocket doesn't include a specific AI SDK — that's intentional. Your users might need text generation, image analysis, code assistance, or nothing AI-related at all. Instead, this guide shows you how to add any AI provider and connect it to the existing credit system.

Architecture

User clicks "Generate" in dashboard
  → apps/app calls Edge Function
    → Edge Function deducts credits (use-credits)
    → Edge Function calls AI API (Claude, OpenAI, etc.)
    → Returns result to user

The key pattern: AI calls happen in Edge Functions, never in the client. This keeps your API keys secure and lets you control costs via credits.

Example: Claude SDK

1. Install the SDK

Add the Anthropic SDK to your Edge Function dependencies. Since Edge Functions run on Deno, you import directly:

// No npm install needed — Deno imports from npm
import Anthropic from "npm:@anthropic-ai/sdk";

2. Set your API key

npx supabase secrets set ANTHROPIC_API_KEY=sk-ant-...

3. Create an Edge Function

// supabase/functions/generate-text/index.ts
import { corsHeaders, supabaseAdmin } from "../_shared/supabase.ts";
import Anthropic from "npm:@anthropic-ai/sdk";

const anthropic = new Anthropic({
  apiKey: Deno.env.get("ANTHROPIC_API_KEY"),
});

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

  try {
    // 1. Authenticate the user
    const authHeader = req.headers.get("Authorization");
    if (!authHeader) {
      return new Response(
        JSON.stringify({ error: "Not authenticated" }),
        { status: 401, headers: { ...corsHeaders, "Content-Type": "application/json" } }
      );
    }

    const token = authHeader.replace("Bearer ", "");
    const { data: { user }, error: authError } = await supabaseAdmin.auth.getUser(token);
    if (authError || !user) {
      return new Response(
        JSON.stringify({ error: "Invalid token" }),
        { status: 401, headers: { ...corsHeaders, "Content-Type": "application/json" } }
      );
    }

    // 2. Parse the request
    const { prompt } = await req.json();
    if (!prompt || typeof prompt !== "string") {
      return new Response(
        JSON.stringify({ error: "Missing prompt" }),
        { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
      );
    }

    // 3. Deduct credits (1 credit per generation)
    const { data: credits, error: creditError } = await supabaseAdmin
      .from("credits")
      .update({ balance: supabaseAdmin.rpc("decrement_balance", { amount: 1 }) })
      .eq("user_id", user.id)
      .gte("balance", 1)
      .select()
      .single();

    if (creditError || !credits) {
      return new Response(
        JSON.stringify({ error: "Insufficient credits" }),
        { status: 402, headers: { ...corsHeaders, "Content-Type": "application/json" } }
      );
    }

    // 4. Call Claude
    const message = await anthropic.messages.create({
      model: "claude-sonnet-4-20250514",
      max_tokens: 1024,
      messages: [{ role: "user", content: prompt }],
    });

    const text = message.content[0].type === "text" ? message.content[0].text : "";

    // 5. Return the result
    return new Response(
      JSON.stringify({
        result: text,
        creditsRemaining: credits.balance,
      }),
      { headers: { ...corsHeaders, "Content-Type": "application/json" } }
    );

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

4. Call from the dashboard

// apps/app/src/lib/api.ts — add this function
export async function generateText(prompt: string) {
  return callFunction<{ result: string; creditsRemaining: number }>(
    "generate-text",
    { method: "POST", body: { prompt } }
  );
}

5. Use in a component

// apps/app/src/routes/_authenticated/generate.tsx
import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react";
import { Button, Card, CardHeader, CardTitle, CardContent } from "@saas/ui";
import { generateText } from "@/lib/api";

export const Route = createFileRoute("/_authenticated/generate")({
  component: GeneratePage,
});

function GeneratePage() {
  const [prompt, setPrompt] = useState("");
  const [result, setResult] = useState("");
  const [loading, setLoading] = useState(false);

  const handleGenerate = async () => {
    setLoading(true);
    const { data, error } = await generateText(prompt);
    setLoading(false);

    if (error) {
      alert(error);
      return;
    }

    setResult(data.result);
  };

  return (
    <div className="p-6 max-w-2xl">
      <Card>
        <CardHeader>
          <CardTitle>AI Generator</CardTitle>
        </CardHeader>
        <CardContent className="space-y-4">
          <textarea
            value={prompt}
            onChange={(e) => setPrompt(e.target.value)}
            placeholder="Enter your prompt..."
            className="w-full rounded-md border p-3 min-h-[100px]"
          />
          <Button onClick={handleGenerate} isLoading={loading}>
            Generate (1 credit)
          </Button>
          {result && (
            <div className="rounded-md bg-muted p-4 whitespace-pre-wrap">
              {result}
            </div>
          )}
        </CardContent>
      </Card>
    </div>
  );
}

6. Deploy

npx supabase functions deploy generate-text

Using OpenAI Instead

The pattern is identical — just swap the SDK:

// supabase/functions/generate-text/index.ts
import OpenAI from "npm:openai";

const openai = new OpenAI({
  apiKey: Deno.env.get("OPENAI_API_KEY"),
});

const response = await openai.chat.completions.create({
  model: "gpt-4o",
  messages: [{ role: "user", content: prompt }],
});

const text = response.choices[0].message.content;

Set the key:

npx supabase secrets set OPENAI_API_KEY=sk-...

Credit Costs

You can vary credit costs based on the operation:

// Light operation: 1 credit
const CREDIT_COST = {
  "text-short": 1,    // Quick text generation
  "text-long": 3,     // Long-form content
  "image": 5,         // Image generation
  "analysis": 2,      // Document analysis
};

// Deduct the appropriate amount
const cost = CREDIT_COST[operationType];

This integrates naturally with ScaleRocket's existing credit system — users buy a plan with a monthly allowance, and each AI operation deducts from their balance.

Security Tips

  • Never expose AI API keys in client code — always call through Edge Functions
  • Always deduct credits before calling the AI — if the AI call fails, refund the credits
  • Set usage limits — cap the max_tokens to prevent expensive requests
  • Validate inputs — sanitize prompts to prevent prompt injection
  • Log usage — track which users make how many requests for cost monitoring

Next Steps

Done reading? Mark this page as complete.

On this page