SaaS を作ろうとすると、ツールの数に圧倒されます。IDE、ターミナル、ブラウザ、Stripe ダッシュボード、デプロイツール……。往復するたびに集中力が削がれ、「どのツールで何をやっていたのか」を思い出すのに数分かかることもあります。
Antigravity を使い始めてから、この往復がほぼなくなりました。IDE の中だけで、認証からデータベース、課金、デプロイまで完結します。AI エージェントがコードを書き、ターミナルでテストし、そのまま本番にデプロイできます。開発体験が大きく変わります。
このガイドでは、Antigravity だけを使って、会員機能付きの SaaS を 1 つ作ります。読み終えたときに、あなたも「IDE から出る必要はないな」と感じるはずです。
SaaS のアーキテクチャ全体像
完成する SaaS は、こんな構成です。
- フロントエンド: Next.js App Router(Antigravity で作成)
- 認証: Supabase Auth(メール / Google OAuth)
- データベース: PostgreSQL in Supabase(RLS で行レベルセキュリティ)
- 決済: Stripe Checkout + Webhook
- API: Next.js Route Handlers + Supabase
- デプロイ先: Cloudflare Workers(Vercel 併用も可)
この構成の利点は、すべてが Antigravity の AI エージェントで操作できる点です。ダッシュボードを開かずに済みます。
Antigravity でプロジェクトを初期化する
Antigravity を開き、新しいプロジェクトを作成します。テンプレートは「Next.js with TypeScript」を選びます。
プロジェクトが開いたら、Antigravity のエージェント(AIアシスタント)に以下のプロンプトを投げます。
次のパッケージを package.json に追加してください:
- @supabase/supabase-js
- @supabase/auth-helpers-nextjs
- stripe
- clsx
その後 npm install を実行してください。
エージェントが自動で依存を追加し、インストールを完了します。ターミナルを開く必要はありません。
次に、環境変数を設定します。Antigravity の「Environment」タブで、以下を追加します。
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOi...(Supabase から取得)
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOi...(同じく Supabase)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...(Stripe)
STRIPE_SECRET_KEY=sk_live_...(同じく Stripe)
NEXT_PUBLIC_APP_URL=http://localhost:3000(本番は実際の URL)
認証を Supabase で実装する
Supabase の認証システムを使うことで、パスワード管理・OTP・OAuth をすべて Supabase に任せられます。
Antigravity のエージェントに、こう指示します。
lib/supabase.ts を作成してください。内容は以下です:
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
export async function getUser() {
const { data: { user } } = await supabase.auth.getUser();
return user;
}
次に、ログインページを作成します。
app/(auth)/login/page.tsx を作成してください。以下の機能を含めます:
1. メールアドレス入力フォーム
2. "Sign in with Email" ボタン(Supabase Auth を使用)
3. "Sign in with Google" ボタン(OAuth)
4. サインアップリンク
TypeScript を使い、エラーハンドリングも含めてください。
エージェントが React コンポーネントを生成します。コンポーネントの例:
'use client';
import { useState } from 'react';
import { supabase } from '@/lib/supabase';
export default function LoginPage() {
const [email, setEmail] = useState('');
const [loading, setLoading] = useState(false);
async function handleEmailSignIn(e: React.FormEvent) {
e.preventDefault();
setLoading(true);
const { error } = await supabase.auth.signInWithPassword({
email,
password: '', // Magic link flow の場合は不要
});
if (error) {
console.error('Sign-in error:', error.message);
} else {
// ダッシュボードにリダイレクト
window.location.href = '/dashboard';
}
setLoading(false);
}
return (
<div className="max-w-md mx-auto mt-10">
<h1 className="text-2xl font-bold mb-6">Sign In</h1>
<form onSubmit={handleEmailSignIn}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="your@email.com"
required
className="w-full px-4 py-2 border rounded-lg mb-4"
/>
<button
type="submit"
disabled={loading}
className="w-full bg-blue-600 text-white py-2 rounded-lg"
>
{loading ? 'Signing in...' : 'Sign in with Email'}
</button>
</form>
</div>
);
}データベース設計:RLS で安全に
Supabase のダッシュボードで、テーブルを 2 つ作成します。
users テーブル (Supabase Auth と連動)
- id (UUID, PK)
- email (text)
- created_at (timestamp)
- subscription_status (text: 'free', 'pro', 'premium')
- stripe_customer_id (text, unique)
articles テーブル (プレミアム記事)
- id (UUID, PK)
- user_id (UUID, FK → users.id)
- title (text)
- content (text)
- created_at (timestamp)
- is_premium (boolean)
最も重要な部分は Row Level Security (RLS) です。有効にします。
articles テーブルの RLS ポリシー:
-- ユーザーは自分の記事だけを見られる
CREATE POLICY "Users can view their own articles"
ON articles
FOR SELECT
USING (auth.uid() = user_id);
-- ユーザーは自分の記事だけを作成・更新できる
CREATE POLICY "Users can insert their own articles"
ON articles
FOR INSERT
WITH CHECK (auth.uid() = user_id);Antigravity のエージェントに以下を指示:
lib/database.ts を作成してください。
以下の関数を含めます:
- fetchUserArticles(userId): 指定ユーザーの記事をすべて取得
- createArticle(userId, title, content, isPremium): 記事を作成
- deleteArticle(articleId, userId): 記事を削除(自分のものだけ)
Supabase クライアントを使い、エラーハンドリングを含めてください。
生成されるコード例:
import { supabase } from './supabase';
export async function fetchUserArticles(userId: string) {
const { data, error } = await supabase
.from('articles')
.select('*')
.eq('user_id', userId)
.order('created_at', { ascending: false });
if (error) throw new Error(error.message);
return data;
}
export async function createArticle(
userId: string,
title: string,
content: string,
isPremium: boolean
) {
const { data, error } = await supabase
.from('articles')
.insert([{
user_id: userId,
title,
content,
is_premium: isPremium,
}])
.select();
if (error) throw new Error(error.message);
return data[0];
}Stripe Checkout と Webhook の実装
Stripe の課金フローは 3 ステップです。
- Checkout セッション作成 → ユーザーをチェックアウト画面に送る
- 決済完了 → Stripe が webhook を送る
- subscription_status を更新 → ユーザーのプランを 'pro' に変更
まず、API ルートを作成します。Antigravity のエージェントに:
app/api/create-checkout-session/route.ts を作成してください。
以下の処理を含めます:
1. リクエストボディから userId を取得
2. Stripe の Checkout Session を作成
3.成功時は sessionId を返す
4. エラー時は 500 を返す
Stripe の priceId は環境変数から読み込みます。
生成されるコード例:
import Stripe from 'stripe';
import { NextRequest, NextResponse } from 'next/server';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(req: NextRequest) {
const { userId, email } = await req.json();
try {
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
mode: 'subscription',
line_items: [
{
price: process.env.NEXT_PUBLIC_STRIPE_PRICE_PRO!, // 'price_xxx'
quantity: 1,
},
],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
customer_email: email,
metadata: {
userId,
},
});
return NextResponse.json({ sessionId: session.id });
} catch (error: any) {
return NextResponse.json(
{ error: error.message },
{ status: 500 }
);
}
}次に、Webhook ハンドラーを作成します。
app/api/webhooks/stripe/route.ts を作成してください。
以下の処理を含めます:
1. Stripe からのリクエストを署名検証で確認
2. イベントタイプ 'customer.subscription.updated' と 'customer.subscription.created' を処理
3. metadata.userId を取得し、Supabase の users テーブルの subscription_status を 'pro' に更新
4. エラーハンドリング
webhook secret は環境変数から読み込みます。
生成されるコード例:
import Stripe from 'stripe';
import { NextRequest, NextResponse } from 'next/server';
import { supabase } from '@/lib/supabase';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
export async function POST(req: NextRequest) {
const body = await req.text();
const sig = req.headers.get('stripe-signature')!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
} catch (err: any) {
return NextResponse.json(
{ error: `Webhook signature verification failed: ${err.message}` },
{ status: 400 }
);
}
if (
event.type === 'customer.subscription.created' ||
event.type === 'customer.subscription.updated'
) {
const subscription = event.data.object as Stripe.Subscription;
const userId = subscription.metadata.userId;
const { error } = await supabase
.from('users')
.update({ subscription_status: 'pro' })
.eq('id', userId);
if (error) {
console.error('Failed to update user subscription:', error.message);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
return NextResponse.json({ received: true });
}Antigravity のターミナルで、webhook をローカルテストできます。Stripe CLI を使用:
stripe listen --forward-to localhost:3000/api/webhooks/stripe
stripe trigger customer.subscription.createdエージェントに指示して、テスト用のスクリプトを生成させることもできます。
フロントエンド:ダッシュボードの構築
ログイン後のダッシュボードを作成します。
app/(authenticated)/dashboard/page.tsx を作成してください。
以下の機能を含めます:
1. 現在のユーザーを取得(Supabase Auth)
2. ユーザーの subscription_status を表示
3. 自分の記事一覧を表示(articles テーブルから)
4. "記事を書く" ボタン
5. "Pro にアップグレード" ボタン(subscription_status が 'free' の場合のみ)
エラーハンドリング・ローディング状態も含めてください。
生成されるコンポーネント例:
'use client';
import { useEffect, useState } from 'react';
import { supabase } from '@/lib/supabase';
import Link from 'next/link';
interface User {
id: string;
email: string;
subscription_status: string;
}
interface Article {
id: string;
title: string;
created_at: string;
}
export default function DashboardPage() {
const [user, setUser] = useState<User | null>(null);
const [articles, setArticles] = useState<Article[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function loadData() {
const { data: { user: authUser } } = await supabase.auth.getUser();
if (!authUser) {
window.location.href = '/login';
return;
}
const { data: userData } = await supabase
.from('users')
.select('*')
.eq('id', authUser.id)
.single();
const { data: articlesData } = await supabase
.from('articles')
.select('id, title, created_at')
.eq('user_id', authUser.id)
.order('created_at', { ascending: false });
setUser(userData);
setArticles(articlesData || []);
setLoading(false);
}
loadData();
}, []);
if (loading) return <div>Loading...</div>;
return (
<div className="max-w-4xl mx-auto p-6">
<h1 className="text-3xl font-bold mb-4">Dashboard</h1>
<div className="bg-gray-100 p-4 rounded-lg mb-6">
<p className="text-lg">
Status: <strong>{user?.subscription_status}</strong>
</p>
{user?.subscription_status === 'free' && (
<button
onClick={() => {
// Stripe Checkout に遷移
fetch('/api/create-checkout-session', {
method: 'POST',
body: JSON.stringify({
userId: user.id,
email: user.email,
}),
})
.then((res) => res.json())
.then((data) => {
window.location.href = `https://checkout.stripe.com/pay/${data.sessionId}`;
});
}}
className="mt-4 bg-blue-600 text-white px-4 py-2 rounded"
>
Upgrade to Pro
</button>
)}
</div>
<h2 className="text-2xl font-bold mb-4">Your Articles</h2>
{articles.length === 0 ? (
<p>No articles yet.</p>
) : (
<ul className="space-y-2">
{articles.map((article) => (
<li key={article.id} className="border-b pb-2">
<Link href={`/articles/${article.id}`} className="text-blue-600">
{article.title}
</Link>
<p className="text-sm text-gray-500">
{new Date(article.created_at).toLocaleDateString()}
</p>
</li>
))}
</ul>
)}
<Link href="/articles/new" className="mt-6 inline-block bg-green-600 text-white px-4 py-2 rounded">
Write New Article
</Link>
</div>
);
}Cloudflare Workers へのデプロイ
Antigravity のターミナルで、以下のコマンドを実行します。
npm install -g wrangler
wrangler loginプロジェクトルートに wrangler.toml を作成します。エージェントに指示:
wrangler.toml を作成してください。内容は以下です:
name = "my-saas-app"
type = "javascript"
compatibility_date = "2024-01-01"
[env.production]
routes = [
{ pattern = "example.com/*", zone_name = "example.com" }
]
[[triggers.crons]]
cron = "0 * * * *"
[build]
command = "npm install && npm run build"
cwd = "./"
その後、デプロイします。
wrangler deployAntigravity のターミナルからコマンドを実行するだけで、Cloudflare に本番アプリがデプロイされます。
カスタムドメインを設定する場合も、ターミナルから:
wrangler domains create my-saas-app.com --environment productionIDE を出ることなく、すべてが完了します。
本番環境でのテスト
デプロイ後、本番環境で一連の操作をテストします。
- 本番 URL でサインアップ
- 記事を 1 つ作成
- Pro にアップグレード(テスト用 Stripe カードを使用)
- webhook が正しく処理されたか確認
Antigravity のエージェントに、テスト用の E2E テストスクリプトを生成させることもできます。
tests/e2e.test.ts を作成してください。
以下のシナリオをテストします:
1. ユーザーがサインアップできる
2. 記事を作成できる
3. Stripe Checkout セッションが作成される
4. Webhook で subscription_status が更新される
Playwright を使用してください。
IDE を出ない開発の価値
SaaS 開発は複雑です。通常なら、IDE・ブラウザ・ターミナル・Stripe ダッシュボード・Supabase コンソール……と、複数のツールを行き来することになります。
Antigravity を使えば、IDE だけで完結します。コードを書き、AI エージェントが実装し、ターミナルでテストし、そのままデプロイできます。認知的負荷が大きく減ります。
さらに、AI エージェントの提案を受けながら開発できるので、ベストプラクティスに自然と従うようになります。「このフローでいいのか?」と迷うとき、エージェントに聞けば、プロの実装が返ってくる。
このガイドで作ったアーキテクチャは、小規模な SaaS だけでなく、スケールする本番環境にも対応しています。RLS でセキュリティを、Stripe Webhook でビジネスロジックを、Cloudflare Workers で高速なグローバルデプロイを実現できます。
あなたの次のプロダクトは、Antigravity だけで作ってみてください。その開発体験の違いに驚くと思います。