Skip to content
OTFotf
All posts

Build an AI-powered SaaS with Next.js and OpenAI in 2026

D
DaveAuthor
6 min read
Build an AI-powered SaaS with Next.js and OpenAI in 2026

In 2026, AI is not a feature — it is the baseline. SaaS users expect generation, chat, and analytics as table stakes, not upsell. If you are building a software product today, "AI-powered" is the functional spec. Next.js plus OpenAI is the shortest credible path to launch: fast full-stack delivery plus capable models over a single HTTPS endpoint. This guide shows the production version of that path — App Router structure, secure server actions, API key hygiene, usage gates, and real payments.

Why Next.js plus OpenAI for an AI SaaS

AI-powered SaaS apps deliver their core value — generation, reasoning, insights — by connecting user flows to language or vision models. In production that usually means automating copywriting drafts, AI chat for support or onboarding, analytics summaries, code transformation for technical users, and upload-and-solve image flows.

Next.js shines here because the App Router project structure unifies web and API under one routing layer: the app directory for routes, public for static assets, and an optional src folder. One codebase owns the UI and the async pipelines, so inputs, state, and security are reasoned about in one place instead of across a patchwork of microfrontends and ad-hoc functions.

OpenAI brings the models, available over the OpenAI API platform as HTTPS endpoints. The critical constraint: API credentials must never leak to the browser. That problem is solved by server-side orchestration — server actions and route handlers hold the keys, the browser only ever sees results.

Result: prompts, users, and payments connect with real separation between browser and secret. Before you wire billing, work through a production launch checklist so auth, entitlements, and observability land before customers do.

How the architecture fits together

A durable AI SaaS flow is simple:

User → login → enter prompt → server action → OpenAI API → response → save to DB → display result

Production layers around that core:

  • Stripe for subscriptions and metered billing
  • Auth with row-level security so users only read their own generations
  • Rate limiting and usage tracking to stop API abuse
  • Caching of repeat generations to cut model cost
// app/actions/generate.ts — keys stay server-side, always.
'use server';

import OpenAI from 'openai';
import { requireUser } from '@/lib/auth';
import { checkUsageGate } from '@/lib/billing';

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

export async function generateCopy(prompt: string) {
  const user = await requireUser();
  await checkUsageGate(user.id); // plan limits + rate limit
  const result = await client.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [{ role: 'user', content: prompt }],
    max_tokens: 800,
  });
  // Persist to DB before returning — every generation is
  // billable, auditable, and re-renderable without re-calling.
  await saveGeneration(user.id, prompt, result.choices[0].message.content);
  return result.choices[0].message.content;
}

The model ID above is an example — pin whatever is current in your provider console. The architecture point holds regardless: the browser calls your server action, your server calls the model, and the secret never crosses the network boundary.

11 production screens. Login, database, payments — all wired.

The SaaS Dashboard Kit ships everything already connected. Nothing to set up. Live demo at saas.otf-kit.dev.

See the live demo

Auth and data isolation come first

Every AI SaaS is multi-tenant from day one: each user's prompts, generations, and files must be invisible to everyone else. Supabase Auth supports password, magic link, OTP, social login, and SSO with JWT-based auth integrated with row-level security — see the Supabase Auth docs. Pair that with a row-level security checklist so the policies are written and tested before launch, not after the first leak report.

Keep prompts and generations in tables keyed by user_id with RLS enforcing auth.uid() = user_id. Service-role keys live only in server actions and background jobs — never in client bundles, never in NEXT_PUBLIC_ variables.

Usage gates protect your margin

Model calls cost money on every keystroke of output. Ship three controls from day one:

// lib/billing.ts — the gate every generation passes through.
export async function checkUsageGate(userId: string) {
  const plan = await getPlan(userId); // free | pro | team
  const used = await getPeriodUsage(userId);
  if (used.generations >= plan.monthlyLimit) {
    throw new Error('USAGE_LIMIT — prompt an upgrade, not a retry.');
  }
  await checkRateLimit(`gen:${userId}`, plan.perMinute);
}

Free plans get a small monthly allowance, paid plans get higher limits plus overage handling, and every plan gets per-minute rate limits. Run model calls through background jobs when generations take longer than a request cycle — streaming for interactive chat, queues for batch work.

Billing that survives contact with customers

Subscriptions mean lifecycle states, not just a checkout button: trials, upgrades, downgrades, cancellations, seat changes, failed payments with dunning retries. Stripe's subscription overview documents that lifecycle, and Checkout Sessions give you hosted or embedded payment UIs plus webhook-driven status transitions.

The minimum viable billing wiring:

  1. Checkout Session creates the subscription and returns the customer to your app.
  2. A webhook handler syncs subscription status into your subscriptions table.
  3. Your usage gate reads that table — expired or past-due means read-only mode, not silent free service.
  4. Customer portal handles card updates and cancellations without your support queue.

Test the webhooks with the Stripe CLI before launch: subscribe, cancel, fail a payment, and confirm each transition flips the right flag in your database.

Streaming, caching, and cost control

Interactive chat should stream tokens so first paint arrives in milliseconds. Next.js route handlers support streaming responses; the OpenAI SDK supports streamed completions. Cache aggressively: identical prompts from onboarding templates or retry buttons should hit your database, not the model API. A Redis or Postgres cache keyed by a hash of model plus messages plus parameters turns repeat spend into a lookup.

Log every call with model, tokens in and out, latency, and user ID. That log is your cost dashboard, your abuse detector, and your debugging trail in one table.

Ship it as one system

The patterns above — App Router structure, server-side secrets, RLS-backed auth, usage gates, Stripe lifecycle webhooks, streaming plus caching — are the difference between a demo with an API key and a SaaS that survives its first hundred paying users. They hold for months, not just the next hackathon, because each one isolates a failure mode: leaked keys, cross-tenant reads, margin-eating abuse, or billing drift.

If you would rather start from all of this already wired — auth, Stripe, database, and AI routes your coding agent can extend — start from production-ready kits at OTF templates instead of hand-rolling the plumbing per project.

Sources

ai-toolssupabasebackend
OTF SaaS Dashboard Kit

Ship the product, not the setup.

  • 11 production screens — auth, billing, team, analytics, settings
  • Real database, payments, and login — all wired on day 1
  • AI configs pre-tuned so your agent extends instead of regenerates