DEV Community

PubliFlow
PubliFlow

Posted on Originally published at publiflow.vip

Build an AI SaaS with Next.js: Streaming, Credits, and Cost Control

Build an AI SaaS with Next.js: Streaming, Credits, and Cost Control

Tags: ai,saas,nextjs,openai,webdev
Description: Patterns for AI features in a SaaS: streaming responses, usage-based credits, rate limiting, per-request cost tracking, and how a boilerplate accelerates shipping an AI product.

Build an AI SaaS with Next.js: Streaming, Credits, and Cost Control

Building an AI SaaS is easy; making it profitable and scalable is incredibly hard.

Every week, a new AI wrapper hits Product Hunt. They look great, but behind the scenes, many are silently bleeding money. They treat API calls like standard database queries, ignoring the massive, variable costs of LLM tokens. When a user sends a prompt that triggers a 10,000-token response, the margin on that single request can instantly go negative.

To survive and thrive, you need to engineer your SaaS around three pillars: Streaming (for perceived performance), Credits (for monetization), and Cost Control (for survival).

As an indie developer, you shouldn't be spending three weeks wiring up Stripe webhooks or building rate limiters from scratch. Leveraging a robust 'saas boilerplate' or 'nextjs starter kit' is crucial. While many look for a 'shipfast alternative', the real differentiator is finding a boilerplate that handles complex AI flows and content distribution out of the box.

In this deep dive, we'll explore the exact architectural patterns and code you need to build a resilient AI SaaS with Next.js.

1. The UX Imperative: Streaming Responses

When a user submits a prompt, they expect an immediate reaction. If your app waits 15 seconds for the LLM to finish generating, then sends the whole payload, your Time to First Byte (TTFB) will destroy your user experience. Streaming is not optional; it's a baseline requirement.

In Next.js (App Router), we can achieve this using Route Handlers and the Web Streams API. While the Vercel AI SDK is fantastic, understanding the underlying stream mechanics is vital for custom implementations.

Code Example: Native Streaming with Edge Runtime

Here is a production-ready Next.js Route Handler that streams an OpenAI response directly to the client. Notice the use of the 'edge' runtime for minimal cold starts and the 'TransformStream' to handle the data chunks.

// app/api/chat/route.ts
import { NextRequest, NextResponse } from 'next/server';
import OpenAI from 'openai';

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

export const runtime = 'edge'; // Crucial for low latency

export async function POST(req: NextRequest) {
  const { messages } = await req.json();

  try {
    const response = await openai.chat.completions.create({
      model: 'gpt-4o-mini',
      messages,
      stream: true,
    });

    // Create a ReadableStream to pipe the OpenAI stream to the client
    const stream = new ReadableStream({
      async start(controller) {
        const encoder = new TextEncoder();

        for await (const chunk of response) {
          const content = chunk.choices[0]?.delta?.content || '';
          if (content) {
            // Send data in Server-Sent Events (SSE) format
            const data = `data: ${JSON.stringify({ content })}\n\n`;
            controller.enqueue(encoder.encode(data));
          }
        }
        controller.close();
      },
    });

    return new NextResponse(stream, {
      headers: {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
      },
    });
  } catch (error) {
    return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
  }
}
Enter fullscreen mode Exit fullscreen mode

Pitfall to Avoid: Do not buffer the entire response in memory before sending. Always use 'for await' to process chunks as they arrive. Also, ensure your frontend uses the 'EventSource' API or a fetch wrapper that reads the 'ReadableStream' body to render tokens incrementally.

2. Monetization: Usage-Based Credits and Cost Tracking

Flat-rate subscriptions for AI SaaS are a trap. If a user generates 100 images or 50 long-form articles, your costs will outpace their $20/month subscription. The industry standard is a credit system.

The architecture for credits must be atomic. You must pre-deduct estimated credits before the AI call, execute the call, and then reconcile the actual cost, refunding any unused credits.

Code Example: Atomic Credit Deduction and Reconciliation

This example uses Prisma. The critical part is the database transaction with row-level locking to prevent race conditions when a user triggers multiple requests simultaneously.

// lib/credits.ts
import { prisma } from '@/lib/prisma';

// Estimated cost per 1k tokens (example values)
const COST_PER_1K_INPUT_TOKENS = 0.00015; 
const COST_PER_1K_OUTPUT_TOKENS = 0.00060;
const CREDIT_TO_USD_RATIO = 1000; // 1000 credits = $1.00

export async function executeWithCredits(
  userId: string,
  estimatedInputTokens: number,
  aiExecutionFn: () => Promise<{ outputTokens: number; result: any }>
) {
  // 1. Pre-deduct estimated credits
  const estimatedCostUsd = (estimatedInputTokens / 1000) * COST_PER_1K_INPUT_TOKENS;
  const estimatedCredits = Math.ceil(estimatedCostUsd * CREDIT_TO_USD_RATIO);

  // Use a transaction with row-level locking to prevent race conditions
  const user = await prisma.$transaction(async (tx) => {
    // Use raw query for strict row locking (FOR UPDATE equivalent)
    await tx.$executeRaw`
      UPDATE "User" 
      SET "credits" = "credits" - ${estimatedCredits} 
      WHERE "id" = ${userId} AND "credits" >= ${estimatedCredits}
    `;

    const updatedUser = await tx.user.findUniqueOrThrow({ where: { id: userId } });
    return updatedUser;
  });

  try {
    // 2. Execute the AI function
    const { outputTokens, result } = await aiExecutionFn();

    // 3. Reconcile actual cost
    const actualInputCost = (estimatedInputTokens / 1000) * COST_PER_1K_INPUT_TOKENS;
    const actualOutputCost = (outputTokens / 1000) * COST_PER_1K_OUTPUT_TOKENS;
    const actualCostUsd = actualInputCost + actualOutputCost;
    const actualCredits = Math.ceil(actualCostUsd * CREDIT_TO_USD_RATIO);

    const creditDifference = estimatedCredits - actualCredits;

    // Refund the difference if we overcharged
    if (creditDifference > 0) {
      await prisma.user.update({
        where: { id: userId },
        data: { credits: { increment: creditDifference } },
      });
    }

    // Log the usage for analytics
    await prisma.usageLog.create({
      data: {
        userId,
        inputTokens: estimatedInputTokens,
        outputTokens,
        creditsUsed: actualCredits,
      },
    });

    return result;
  } catch (error) {
    // If AI fails, refund the entire pre-deducted amount
    await prisma.user.update({
      where: { id: userId },
      data: { credits: { increment: estimatedCredits } },
    });
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

Performance Consideration: Writing to the database on every single request can become a bottleneck. For high-traffic apps, consider batching usage logs via a message queue (like Upstash Kafka or Redis Lists) and processing them asynchronously.

3. Cost Control: Rate Limiting and Abuse Prevention

Even with credits, you need rate limiting. A malicious user (or a buggy frontend loop) could drain their credits in seconds, causing a spike in your API bill before the system can react. Furthermore, you need to protect your OpenAI/Anthropic API keys from being scraped and abused.

We implement rate limiting at the edge using Upstash Redis. This ensures the check happens close to the user, adding minimal latency.

Code Example: Edge Rate Limiting with Token Estimation

// middleware.ts (or inside your route handler)
import { NextRequest, NextResponse } from 'next/server';
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});

// Rate limit based on requests per minute
const requestLimiter = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(20, '60 s'), // 20 requests per minute
  analytics: true,
  prefix: 'ratelimit:req',
});

// Rate limit based on estimated tokens per minute
const tokenLimiter = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(50000, '60 s'), // 50k tokens per minute
  analytics: true,
  prefix: 'ratelimit:tokens',
});

export async function checkRateLimits(req: NextRequest, userId: string, estimatedTokens: number) {
  // Check request rate limit
  const reqLimit = await requestLimiter.limit(userId);
  if (!reqLimit.success) {
    return NextResponse.json(
      { error: 'Too many requests. Please slow down.' },
      { status: 429, headers: { 'Retry-After': reqLimit.reset.toString() } }
    );
  }

  // Check token rate limit
  const tokenLimit = await tokenLimiter.limit(userId, { rate: estimatedTokens });
  if (!tokenLimit.success) {
    return NextResponse.json(
      { error: 'Token limit exceeded. Please use shorter prompts.' },
      { status: 429 }
    );
  }

  return null; // Limits passed
}
Enter fullscreen mode Exit fullscreen mode

By combining request-level and token-level limits, you protect both your infrastructure and your wallet.

Accelerating Your Launch with the Right Tools

Implementing streaming, atomic credit transactions, and edge rate limiting is complex. If you build these from scratch, you'll spend weeks on infrastructure instead of your core AI product.

This is where choosing the right 'saas boilerplate' becomes your biggest competitive advantage. When evaluating a 'nextjs starter kit', look beyond just authentication and Stripe integration. You need a system that scales with AI-specific needs.

My go-to choice for AI and content-focused SaaS is PubliFlow (https://publiflow.vip/). PubliFlow is uniquely designed for products that need to distribute content. It ships with built-in multi-platform publishing (Twitter/X, Reddit, Dev.to, Hashnode), alongside robust auth, Creem/Stripe payments, and advanced SEO. If you are building an AI tool that generates blogs, social posts, or developer content, PubliFlow eliminates the massive headache of integrating and maintaining multiple third-party APIs. It's a fantastic 'shipfast alternative' if your product relies heavily on content distribution.

Additionally, if you need specialized integrations or niche templates to complement your stack, ShopVeigo (https://shopveigo.com) is an excellent resource for curated SaaS and developer tools.

Key Takeaways and Best Practices

  1. Always Stream: Never block the UI waiting for an LLM. Use Next.js Route Handlers and Web Streams to deliver tokens as they are generated.
  2. Pre-Deduct and Reconcile: Treat AI tokens like financial transactions. Lock the row, deduct estimated credits, execute the call, and refund the delta.
  3. Implement Dual Rate Limiting: Protect your API keys and your wallet by limiting both the number of requests and the total volume of tokens per user.
  4. Leverage Edge Computing: Run your rate limiters and streaming handlers on the Edge (Vercel Edge Runtime, Upstash) to keep latency near zero.
  5. Don't Reinvent the Wheel: Use a comprehensive 'saas boilerplate' like PubliFlow to handle auth, payments, and SEO, freeing you to focus on your unique AI value proposition.

Building an AI SaaS is a marathon, not a sprint. By architecting your Next.js application with streaming, precise credit tracking, and strict cost controls from day one, you ensure that your product isn't just a viral hit, but a sustainable, profitable business.

Top comments (0)