ANTIGRAVITY LABJP
Articles/App Development
App Development/2026-03-29Intermediate

Building an Authenticated SaaS App with Antigravity × Next.js — Auth.js v5 + Stripe + Cloudflare D1 Patterns

Learn how to leverage Antigravity's AI agent to build a SaaS application with Auth.js v5 authentication, Stripe billing, and Cloudflare D1 database — a practical implementation guide.

antigravity436next-jssaas10auth-jsstripe9cloudflare-d1

Accelerating SaaS Development with AI Agents

Building a SaaS application requires three foundational pillars: authentication, billing, and a database. Traditionally, integrating these systems could take weeks. With Antigravity's AI agent, you can dramatically reduce the time from architecture to working code.

This is a sample of the kind of in-depth, practical content available to Antigravity Lab members. If you find this valuable, consider joining our membership for more advanced implementation guides.

Project Setup

Start by having Antigravity's agent scaffold the project. When you specify the exact tech stack, the agent can suggest an optimal initial configuration.

# In Antigravity's terminal, instruct the agent:
# "Create a Next.js 15 App Router + TypeScript + Tailwind CSS SaaS scaffold.
#  Use Auth.js v5 for auth, Cloudflare D1 for database, Stripe for billing."
npx create-next-app@latest my-saas --typescript --tailwind --app --src-dir
cd my-saas
 
# Install required packages
npm install next-auth@beta @auth/drizzle-adapter drizzle-orm stripe
npm install -D drizzle-kit wrangler

Use Antigravity's inline chat (Cmd+I) to iteratively generate each file through natural conversation.

Implementing Authentication with Auth.js v5

Auth.js v5 (NextAuth.js v5) natively supports the Next.js App Router. Let the agent know your design requirements, and it will generate the authentication configuration.

// src/lib/auth.ts
import NextAuth from "next-auth";
import Google from "next-auth/providers/google";
import GitHub from "next-auth/providers/github";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import { db } from "./db";
 
export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: DrizzleAdapter(db),
  providers: [
    Google({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    GitHub({
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    }),
  ],
  callbacks: {
    async session({ session, user }) {
      // Add user ID and subscription info to session
      session.user.id = user.id;
      return session;
    },
  },
  pages: {
    signIn: "/login",
    error: "/auth/error",
  },
});
// src/app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/lib/auth";
 
export const { GET, POST } = handlers;

The key here is using DrizzleAdapter to connect directly with Cloudflare D1. This stores user data in a D1 database close to the edge, making authentication requests faster.

Cloudflare D1 Database Design

D1 is a SQLite-based database that runs on Cloudflare's edge network. We'll define the schema using Drizzle ORM.

// src/db/schema.ts
import { sqliteTable, text, integer, real } from "drizzle-orm/sqlite-core";
 
// Auth.js tables
export const users = sqliteTable("users", {
  id: text("id").primaryKey(),
  name: text("name"),
  email: text("email").notNull().unique(),
  emailVerified: integer("emailVerified", { mode: "timestamp" }),
  image: text("image"),
  stripeCustomerId: text("stripe_customer_id"),
  plan: text("plan").default("free"), // free, pro, premium
  planExpiresAt: integer("plan_expires_at", { mode: "timestamp" }),
});
 
export const accounts = sqliteTable("accounts", {
  id: text("id").primaryKey(),
  userId: text("userId").notNull().references(() => users.id),
  type: text("type").notNull(),
  provider: text("provider").notNull(),
  providerAccountId: text("providerAccountId").notNull(),
  refresh_token: text("refresh_token"),
  access_token: text("access_token"),
  expires_at: integer("expires_at"),
  token_type: text("token_type"),
  scope: text("scope"),
  id_token: text("id_token"),
});
 
export const sessions = sqliteTable("sessions", {
  id: text("id").primaryKey(),
  sessionToken: text("sessionToken").notNull().unique(),
  userId: text("userId").notNull().references(() => users.id),
  expires: integer("expires", { mode: "timestamp" }).notNull(),
});
 
// SaaS-specific tables
export const subscriptions = sqliteTable("subscriptions", {
  id: text("id").primaryKey(),
  userId: text("user_id").notNull().references(() => users.id),
  stripeSubscriptionId: text("stripe_subscription_id").unique(),
  plan: text("plan").notNull(), // pro, premium
  status: text("status").notNull(), // active, canceled, past_due
  currentPeriodEnd: integer("current_period_end", { mode: "timestamp" }),
  createdAt: integer("created_at", { mode: "timestamp" }),
});
# Generate and apply migrations
npx drizzle-kit generate
npx wrangler d1 migrations apply my-saas-db --local

Implementing the Stripe Billing Flow

Create an API route that handles Stripe Checkout for authenticated users.

// src/app/api/checkout/route.ts
import { NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
import { users } from "@/db/schema";
import { eq } from "drizzle-orm";
 
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
 
const PLANS = {
  pro: {
    priceId: process.env.STRIPE_PRO_PRICE_ID!,
    name: "Pro",
  },
  premium: {
    priceId: process.env.STRIPE_PREMIUM_PRICE_ID!,
    name: "Premium",
  },
} as const;
 
export async function POST(req: NextRequest) {
  const session = await auth();
  if (!session?.user?.id) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
 
  const { plan } = await req.json();
  if (!PLANS[plan as keyof typeof PLANS]) {
    return NextResponse.json({ error: "Invalid plan" }, { status: 400 });
  }
 
  const selectedPlan = PLANS[plan as keyof typeof PLANS];
 
  // Get or create Stripe Customer
  const user = await db.select().from(users)
    .where(eq(users.id, session.user.id))
    .get();
 
  let customerId = user?.stripeCustomerId;
 
  if (!customerId) {
    const customer = await stripe.customers.create({
      email: session.user.email!,
      metadata: { userId: session.user.id },
    });
    customerId = customer.id;
 
    await db.update(users)
      .set({ stripeCustomerId: customerId })
      .where(eq(users.id, session.user.id));
  }
 
  // Create Checkout Session
  const checkoutSession = await stripe.checkout.sessions.create({
    customer: customerId,
    mode: plan === "premium" ? "payment" : "subscription",
    line_items: [{ price: selectedPlan.priceId, quantity: 1 }],
    success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/dashboard?upgraded=true`,
    cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/pricing`,
    metadata: {
      userId: session.user.id,
      plan,
    },
  });
 
  return NextResponse.json({ url: checkoutSession.url });
}

Protecting Routes with Auth Middleware

Implement middleware to protect pages that require authentication.

// src/middleware.ts
import { auth } from "@/lib/auth";
import { NextResponse } from "next/server";
 
export default auth((req) => {
  const isLoggedIn = !!req.auth;
  const isProtectedRoute = req.nextUrl.pathname.startsWith("/dashboard");
  const isApiRoute = req.nextUrl.pathname.startsWith("/api/checkout");
 
  if (isProtectedRoute && !isLoggedIn) {
    return NextResponse.redirect(new URL("/login", req.url));
  }
 
  if (isApiRoute && !isLoggedIn) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
 
  return NextResponse.next();
});
 
export const config = {
  matcher: ["/dashboard/:path*", "/api/checkout"],
};

Syncing Payment State with Webhooks

Implement a Webhook handler to receive Stripe events and update subscription status in your database.

// src/app/api/webhook/route.ts
import { NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";
import { db } from "@/lib/db";
import { users, subscriptions } from "@/db/schema";
import { eq } from "drizzle-orm";
 
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
 
export async function POST(req: NextRequest) {
  const body = await req.text();
  const signature = req.headers.get("stripe-signature")!;
 
  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch {
    return NextResponse.json(
      { error: "Invalid signature" },
      { status: 400 }
    );
  }
 
  switch (event.type) {
    case "checkout.session.completed": {
      const session = event.data.object as Stripe.Checkout.Session;
      const userId = session.metadata?.userId;
      const plan = session.metadata?.plan;
 
      if (userId && plan) {
        await db.update(users)
          .set({ plan })
          .where(eq(users.id, userId));
      }
      break;
    }
 
    case "customer.subscription.deleted": {
      const sub = event.data.object as Stripe.Subscription;
      const userId = sub.metadata?.userId;
 
      if (userId) {
        await db.update(users)
          .set({ plan: "free" })
          .where(eq(users.id, userId));
      }
      break;
    }
  }
 
  return NextResponse.json({ received: true });
}

Looking back

By leveraging Antigravity's AI agent, you can efficiently build a fully-featured SaaS application with Auth.js v5 + Stripe + Cloudflare D1. While integrating authentication, billing, and database layers is inherently complex, communicating your architectural vision to the agent helps maintain a consistent codebase throughout development.

To get started with Antigravity basics, check out "What is Google Antigravity?". For more on backend development, see "Supabase × Antigravity Backend Guide". To improve how you communicate with AI, "Prompt Engineering Fundamentals" is also worth reading.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

App Dev2026-04-28
Build a Complete SaaS in Antigravity — Stripe Billing, Auth, and Deployment Without Leaving Your IDE
Build an entire SaaS product within Antigravity IDE — from Supabase auth to Stripe billing to Cloudflare Workers deployment, without switching tools.
App Dev2026-04-11
Building Real-Time Collaborative SaaS with Antigravity and Convex: From Type-Safe Data Sync to Stripe Billing
A complete guide to building a real-time collaborative SaaS using Antigravity IDE and Convex. Covers schema design, Clerk authentication, file storage, and Stripe billing with production-ready code.
App Dev2026-04-06
Antigravity × Multi-Tenant SaaS Complete Implementation Guide: Supabase RLS × Stripe Metered Billing × RBAC
A production-grade guide to building multi-tenant SaaS with Antigravity IDE. Learn tenant isolation with Supabase Row Level Security, usage-based billing with Stripe Metered Billing, and fine-grained access control with RBAC — with full implementation code.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →