Rork で作ったアプリが「動いてはいるけど、データが消えてしまう」「ログイン機能をつけたいけどどうすればいいか」「外部のAPIと連携したいけど手順がわからない」という段階にある方に向けて書いています。
Rork Max のバックエンドAPIは、適切に使えばモバイルアプリの本番品質に十分対応できます。ただし、その「適切な使い方」を知らないと、スケールしない設計を作ってしまいます。
Rork のバックエンドアーキテクチャを理解する
まず、Rork が自動生成するバックエンドの仕組みを理解する点が肝心です。
Rork のバックエンドは、Cloudflare Workers 上で動く軽量なHTTPサーバーです。各エンドポイントはサーバーレス関数として動作し、データストアは Cloudflare KV(キーバリューストア)または D1(SQLite互換)を使います。
この設計の特徴は、スケーラビリティが高く、デプロイが簡単なことです。一方で、複雑なリレーショナルデータモデルには向いていない制約もあります。
KV vs D1 の使い分け
Cloudflare KV を使うべきケース:
- セッションデータ・キャッシュ
- ユーザー設定・プリファレンス
- TTL(期限)付きデータ
- 単純なキーバリューの参照が多いデータ
Cloudflare D1 を使うべきケース:
- リレーショナルデータ(ユーザーと投稿の関係など)
- 複雑な検索クエリが必要なデータ
- 集計・分析が必要なデータ
- スキーマが安定しているデータ
データ永続化の実装
D1 を使ったユーザーデータの永続化
Rork のプロンプトで「SQLiteデータベースを使ってユーザーデータを永続化して」と指定することで、D1を使ったバックエンドが生成されます。
生成されたコードを理解・改善するために、基本的なパターンを押さえておきましょう。
// rork-generated backend: src/api/users.ts
import { D1Database } from "@cloudflare/workers-types";
export interface User {
id: string;
email: string;
display_name: string;
created_at: string;
updated_at: string;
}
export class UserRepository {
constructor(private db: D1Database) {}
async createUser(email: string, displayName: string): Promise<User> {
const id = crypto.randomUUID();
const now = new Date().toISOString();
await this.db.prepare(`
INSERT INTO users (id, email, display_name, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)
`).bind(id, email, displayName, now, now).run();
return { id, email, display_name: displayName, created_at: now, updated_at: now };
}
async getUserByEmail(email: string): Promise<User | null> {
const result = await this.db
.prepare("SELECT * FROM users WHERE email = ?")
.bind(email)
.first<User>();
return result ?? null;
}
async updateUser(id: string, updates: Partial<Pick<User, "display_name">>): Promise<User | null> {
const now = new Date().toISOString();
await this.db.prepare(`
UPDATE users SET display_name = ?, updated_at = ?
WHERE id = ?
`).bind(updates.display_name, now, id).run();
return this.db.prepare("SELECT * FROM users WHERE id = ?")
.bind(id).first<User>() ?? null;
}
}Rork で生成されたコードを改善する際の重要ポイント: Rork が生成するSQLは基本的に正しいですが、インデックスが設定されていないことがあります。検索頻度が高いカラム(emailなど)には必ずインデックスを追加してください。
-- マイグレーションファイルに追加
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_posts_user_id ON posts(user_id);
CREATE INDEX IF NOT EXISTS idx_posts_created_at ON posts(created_at DESC);認証の実装
JWT認証フロー
Rork に「JWTを使ったログイン機能をつけて」と指示すると、基本的なJWT認証が生成されます。ただし、本番環境ではいくつかの改善が必要です。
// src/api/auth.ts — 本番レベルの認証実装
import { SignJWT, jwtVerify } from "jose";
const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET);
const TOKEN_EXPIRY = "7d";
export async function createSession(userId: string, email: string): Promise<string> {
return new SignJWT({ userId, email })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime(TOKEN_EXPIRY)
.sign(JWT_SECRET);
}
export async function verifySession(token: string): Promise<{ userId: string; email: string } | null> {
try {
const { payload } = await jwtVerify(token, JWT_SECRET);
return { userId: payload.userId as string, email: payload.email as string };
} catch {
return null;
}
}
// ミドルウェア: 認証が必要なエンドポイントを保護
export function requireAuth(handler: Function) {
return async (request: Request, env: any) => {
const authHeader = request.headers.get("Authorization");
if (\!authHeader?.startsWith("Bearer ")) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" }
});
}
const token = authHeader.substring(7);
const session = await verifySession(token);
if (\!session) {
return new Response(JSON.stringify({ error: "Invalid or expired token" }), {
status: 401,
headers: { "Content-Type": "application/json" }
});
}
return handler(request, env, session);
};
}モバイルアプリ側の認証管理
RorkのSwiftコードでJWTを安全に保存・使用する方法です。
// RorkGeneratedApp/Services/AuthService.swift
import Security
import Foundation
class AuthService: ObservableObject {
@Published var isAuthenticated = false
@Published var currentUser: User?
private let keychainKey = "app.authToken"
// Keychainにトークンを保存(UserDefaultsより安全)
func saveToken(_ token: String) {
let data = token.data(using: .utf8)\!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: keychainKey,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
SecItemDelete(query as CFDictionary)
SecItemAdd(query as CFDictionary, nil)
}
func getToken() -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: keychainKey,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
SecItemCopyMatching(query as CFDictionary, &result)
guard let data = result as? Data else { return nil }
return String(data: data, encoding: .utf8)
}
func logout() {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: keychainKey
]
SecItemDelete(query as CFDictionary)
DispatchQueue.main.async {
self.isAuthenticated = false
self.currentUser = nil
}
}
}外部APIとの連携
Stripe決済の統合
アプリ内課金に Stripe を使う場合の実装パターンです。
// src/api/payments.ts
export async function createPaymentIntent(
amount: number,
currency: string,
userId: string,
env: { STRIPE_SECRET_KEY: string }
): Promise<{ clientSecret: string }> {
const response = await fetch("https://api.stripe.com/v1/payment_intents", {
method: "POST",
headers: {
"Authorization": `Bearer ${env.STRIPE_SECRET_KEY}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
amount: String(amount),
currency,
metadata: JSON.stringify({ userId }),
}),
});
if (\!response.ok) {
const error = await response.json();
throw new Error(`Stripe error: ${error.error.message}`);
}
const intent = await response.json();
return { clientSecret: intent.client_secret };
}重要: Stripe のシークレットキーは Cloudflare Workers の環境変数に設定し、コードに直接書かないでください。Rork のプロジェクト設定から環境変数を追加できます。
エラーハンドリングと信頼性
本番アプリで最も軽視されがちなのがエラーハンドリングです。
// 統一エラーハンドリング
export class AppError extends Error {
constructor(
message: string,
public statusCode: number = 500,
public code: string = "INTERNAL_ERROR"
) {
super(message);
this.name = "AppError";
}
}
export function createErrorResponse(error: unknown): Response {
if (error instanceof AppError) {
return new Response(
JSON.stringify({ error: error.message, code: error.code }),
{ status: error.statusCode, headers: { "Content-Type": "application/json" } }
);
}
console.error("Unexpected error:", error);
return new Response(
JSON.stringify({ error: "Internal server error", code: "INTERNAL_ERROR" }),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}Rork の限界を超えたときの移行戦略
Rork のバックエンドが向いていないケースもあります。月間アクティブユーザーが5万人を超えてきた、複雑なリレーショナルデータが必要になってきた、リアルタイム機能(WebSocket)が必要になった — こういった段階では、段階的な移行を検討します。
移行先としては Supabase(認証・リアルタイム機能が強い)や PlanetScale + Hono(スケーラブルなSQLが必要な場合)が現実的な選択肢です。Rorkで作ったAPIの設計をそのままに、バックエンドだけを差し替えることは技術的に可能です。
最初から「Rorkで作って、スケールしたら移行する」という戦略は現実的です。むしろ最初からオーバーエンジニアリングするより、Rorkで素早く検証してユーザーを獲得してから移行を検討する方が賢明な場合が多いです。
あなたのアプリが解決しようとしている問題とユーザー規模感を踏まえて、今必要な「ちょうどいいバックエンド」を選んでください。