ANTIGRAVITY LABEN
記事一覧/アプリ開発
アプリ開発/2026-03-29中級

Antigravity × Next.js で認証付き SaaS アプリを構築する — Auth.js v5 + Stripe + Cloudflare D1 実装パターン

Antigravity の AI エージェントを活用し、Auth.js v5 による認証・Stripe 課金・Cloudflare D1 データベースを組み合わせた SaaS アプリの構築方法を実践的に解説します。

antigravity435next-jssaas10auth-jsstripe9cloudflare-d1

AI エージェントで SaaS 開発を加速する

SaaS アプリケーションの構築には、認証・課金・データベースという3つの柱が欠かせません。従来はこれらの統合に数週間を要していましたが、Antigravity の AI エージェントを活用すれば、設計から実装までの時間を大幅に短縮できます。

これは Antigravity Lab のプレミアム記事で扱うような実践テクニックの一部を、無料で体験いただける見本記事です。より深い実装パターンにご興味をお持ちの方は、ぜひメンバーシップもご検討ください。

プロジェクトの初期セットアップ

Antigravity のターミナルで、プロジェクトの雛形を AI に生成させましょう。エージェントに具体的な技術スタックを伝えることで、最適な初期構成を提案してくれます。

# Antigravity のターミナルで以下のように指示
# 「Next.js 15 App Router + TypeScript + Tailwind CSS で
#   SaaS アプリの雛形を作成して。
#   認証は Auth.js v5、DB は Cloudflare D1、課金は Stripe を使う」
npx create-next-app@latest my-saas --typescript --tailwind --app --src-dir
cd my-saas
 
# 必要なパッケージをインストール
npm install next-auth@beta @auth/drizzle-adapter drizzle-orm stripe
npm install -D drizzle-kit wrangler

Antigravity のインラインチャット(Cmd+I)を使えば、各ファイルの生成を対話的に進められます。

Auth.js v5 による認証の実装

Auth.js v5(NextAuth.js v5)は、Next.js App Router にネイティブ対応した認証ライブラリです。Antigravity のエージェントに設計方針を伝え、認証設定を生成させます。

// 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 }) {
      // セッションにユーザーIDとサブスクリプション情報を追加
      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;

ポイントは DrizzleAdapter を使って Cloudflare D1 と直接接続している点です。これにより、ユーザー情報がエッジに近い D1 データベースに保存され、認証処理が高速になります。

Cloudflare D1 データベース設計

D1 は Cloudflare のエッジで動作する SQLite ベースのデータベースです。Drizzle ORM を使ってスキーマを定義します。

// src/db/schema.ts
import { sqliteTable, text, integer, real } from "drizzle-orm/sqlite-core";
 
// Auth.js 用テーブル
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 固有のテーブル
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" }),
});
# マイグレーション生成と適用
npx drizzle-kit generate
npx wrangler d1 migrations apply my-saas-db --local

Stripe 課金フローの実装

認証済みユーザーに対して、Stripe Checkout で課金を行う API ルートを実装します。

// 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];
 
  // 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));
  }
 
  // 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 });
}

認証ミドルウェアでルートを保護する

認証が必要なページを保護するミドルウェアを実装します。

// 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"],
};

Webhook で支払い状態を同期する

Stripe からのイベントを受け取り、データベースのサブスクリプション状態を更新する Webhook ハンドラを実装します。

// 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 });
}

全体を振り返って

Antigravity の AI エージェントを活用することで、Auth.js v5 + Stripe + Cloudflare D1 という本格的な技術スタックの SaaS アプリを効率よく構築できます。認証・課金・データベースの統合は複雑になりがちですが、エージェントにアーキテクチャの方針を伝えれば、一貫性のあるコードベースを維持しながら開発を進められます。

SaaS 開発の全体像

Antigravity の基本的な使い方は「Google Antigravity とは?」、バックエンド構築の詳細は「Supabase × Antigravity バックエンド構築ガイド」もあわせてご覧ください。AI への指示の出し方を磨きたい方には「プロンプトエンジニアリング入門」も参考になります。

シェア

お読みいただきありがとうございます

Antigravity Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

アプリ開発2026-04-28
Antigravity で SaaS を丸ごと作る — Stripe 課金・認証・デプロイを1つのIDEで完結させる
Antigravity IDE だけで SaaS プロダクトを構築する全工程を解説します。Supabase認証、Stripe課金、Cloudflare Workersデプロイまで、IDEを離れずに完結させる実践ガイドです。
アプリ開発2026-04-11
Antigravity × Convex でリアルタイム協調 SaaS を構築する:型安全なデータ同期から Stripe 課金まで
Antigravity IDE と Convex を組み合わせてリアルタイム協調 SaaS を構築する完全ガイド。スキーマ設計から Clerk 認証、ファイルストレージ、Stripe 課金まで本番対応の実装コードを体系的に解説します。
アプリ開発2026-04-06
Antigravity × マルチテナント SaaS 実装ガイド:Supabase RLS × Stripe メーター課金 × RBAC 権限制御
Antigravity を使ってマルチテナント SaaS を本番レベルで構築する完全ガイド。Supabase の Row Level Security でテナント分離、Stripe メーター課金で従量制プラン、RBAC で権限制御を実装する設計パターンと実装コードを詳解します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →