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 wranglerAntigravity のインラインチャット(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 --localStripe 課金フローの実装
認証済みユーザーに対して、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 への指示の出し方を磨きたい方には「プロンプトエンジニアリング入門」も参考になります。