◉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.

antigravity464next-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-05-03
Building Idempotency Keys and Dedupe Stores in TypeScript with Antigravity
A production guide to designing idempotency keys and dedupe stores in TypeScript with Antigravity — covering Stripe webhook retries, Temporal replays, and the Cloudflare KV / Redis / Postgres trade-offs you actually need to choose between.
▣ App Dev2026-04-26
Antigravity × Cloudflare Edge SaaS Architecture Guide: Production Patterns with D1, Workers, R2, and Workers AI
A complete production guide for building edge SaaS with Antigravity + Cloudflare D1, Workers, R2, and Workers AI. Covers real pitfalls including N+1 queries, CPU limits, and cost management.
⬡ Integrations2026-04-29
Building a Stripe Dunning Recovery Pipeline with Antigravity AI Agents — Stop Failed Payments from Becoming Churn
Wire Antigravity AI agents into Stripe Smart Retries to recover failed payments before they turn into churn. Webhooks, idempotency, AI-tailored email copy, Slack escalation, and winback offers — all in one production-ready pattern.