Webサービス個人開発日記

開発を始めてからだいぶ経過してるけど…進捗や記録を残しておきたいので今更Zennをはじめてみる。
記事書く勇気はないのでスクラップで。書いてく中でなんか知見が出たら記事書いてみようかな。
2025年なのでAIもガンガン使ってく。GitHub CopilotとかRoo Codeとか。高いけど。
Webサービスのコンセプトとか概要は割としっかり決めてあるし、(AIに読ませる用の)サービス概要とかドメイン概念とか用語定義とかのドキュメントも(AIが)書いてリポジトリに置いてあるけど、こういうのはちゃんとサービスをローンチしてから話したほうが良いと思っているので、ここでは書かない。秘匿したままどこまで書けるのかは謎だけど。

AIを使ってて思うのは、流行したフレームワークやライブラリに強いし、独自のやり方に弱いこと。
動くものを、サービスを作りたいならNext.jsとPostgreSQLに全振りしてVercelでドーン! したほうが良いんだろうと思う。
が、技術オタクの延長線上のついででサービスを作る場合はそれだと面白くないので、AIのパワーが弱まるとしてもここはこだわっていきたい。エンジニアとしての最後の抵抗。
というわけで、私は技術オタクなのでアーキテクチャの紹介からします。
ポイントは
- Cloudflare エコシステムを使う
- SvelteKit でフロントエンドと BFF をつくる
- ドメインロジックを専用のバックエンドに分離する
- Workers RPC で通信する
あたり。
こう、複数の Workers に分けたり、tRPC じゃなくて Workers RPC 上で自前の RPC 処理を組んだりみたいな独創性を出すことが AI コーディング上もローカルでの開発上も不利なことはだいぶ実感した。実感したけどこれでいく。
もともとは SvelteKit の代わりに Remix (正しくは React Router 7 だけど、React Router って書くと BFF 感が薄いので)にしてたり、書いてないけどその過程で UI ライブラリ変えたり、ORM も Prisma から Drizzle ORM に変えたりとか紆余曲折を経てるけどその辺は略。既存コードを Roo Code に翻訳させたら捗ったとだけ。

↑で Drizzle ORM に移行しました、って書いてるけど実は移行の途中だったりする。
DrizzleKit がマイグレーションのために読むソースファイルの様式がよくわかってないけど、おそらくテーブルがすべてexportされていればそれ以外はなんでも良いのであろう。きっと。

複数の Workers に分けてる理由について
- 技術的側面で言えば関心の分離のため
- セキュリティ観点で言えばセキュリティの向上のため
- 個人的な気持ちとしてはチャレンジのため
あとは Workers ではコードの容量が制限される…というのもあるけど、有料プランだし今回のサービスでそんな容量は食わないんじゃないかと予想はしてるので書いてない。
多層防御の観点から、こういう形で認証を構築する予定。
AI に訊いたら KV に JWT 丸々保管するのは良くないかもね、と言われたので、KV には JWT のヘッダー部とペイロード部だけ保管して、署名部はユーザーのセッション Cookie に置くことにした。セッション Cookie は当然セッションごとにユニークかつユーザーと紐づくようにしてあるので Session fixation の対策もできてるハズ。

AI コーディング、何語(日本語 or 英語)でやるのがいいんだろうなあというのをずっと考えている。
AI の性能で言えば現状英語で統一するのが最も性能が発揮されるけど、使う側の認知負荷を考えると母語のほうが明らかにやりやすい。幸い自分は英語ができなくはないので、今のところ主に英語でやってるけど…
今回のサービスでも LLM を活用した機能を一部搭載する予定だけど、それの実現可能性のテストをしたときに英語のほうが AI の性能があがることを実感した。タスクで取り扱う内容が日本語であっても英語でプロンプト書いたほうが良い。ついでにトークン数も少ないし。

Wrangler (Miniflare) のローカル DB ファイルを特定する
だいぶ複雑だった。
Gist に Bun スクリプト載せました。

Cloudflare D1 + Drizzle ORM どうやってく問題
一旦この方針にした。
- drizzle.config.ts は常にローカルの DB (SQLite ファイル)を指すようにする
- マイグレーションは
drizzle-kit generate
で SQL ファイルを生成し、wrangler
で適用する
Drizzle Kit 自体にもマイグレーションを適用させる機能はあるっぽいが、(試してないけど)どうも今 D1 と直接やるのはできなそうな印象がある(なにかしらの Issue があった)。
やりたいのは DB が実行されているコードと同期するようにマイグレーションされている状態を作ることなので、マイグレーションの実行を Drizzle Kit でやるか Wrangler でやるかはどっちでもいい…と思う。
困ることがあるとすると、Drizzle Studio でデプロイされてる DB を見たくなった場合とかだろうか。まあなんか起きたらまた書く。
ちなみに drizzle.config.ts は Drizzle Kit と Drizzle Studio のみが使用するという認識。アプリ側は適切に D1 アダプター使って参照します。

Drizzle でサブクエリしようとしてハマった
TL;DR: Drizzle では sql`SELECT ... FROM ...`
みたいなのを書くと駄目っぽい。db.select(...).from(...)
としたら動いた。
URL の /:username/:resourceName
から必要なリソースを取り出すため、
SELECT * FROM resources
WHERE "resources"."name" = :resourceName
AND "resources"."userId" = (SELECT "users"."id" FROM "users" WHERE "users"."username" = :username);
のような SQL を実行したかった。
Drizzle の Query API でこれをやろうとすると、だいたい次のようなコードになると思う。
/** ユーザー名からユーザーIDを返すサブクエリを返す */
function subQueryUserIdByUsername(username: string) {
// ↓✅️これだと大丈夫
return db.select({ id: users.id }).from(users).where(sql`${users.username} = ${username}`);
// ↓❌️これだとだと駄目
return sql`(SELECT ${users.id} FROM ${users} WHERE ${users.username} = ${username})`;
}
// slug からリソースを取得
declare const resourceName: string;
declare const username: string;
const resource = await db.query.resources.findFirst({
where: sql`${resources.name} = ${resourceName} AND ${resources.userId} = ${subQueryUserIdByUsername(username)}`,
});
sql`SELECT`
版だと、何故か当該箇所がこのように生成され、正しいテーブルを参照してくれない。
... AND "resources"."userId" = (SELECT "resources"."id" FROM "users" WHERE "resources"."username" = :username)
--- ^ "users" じゃない ^ "users" じゃない
まあまあバグっぽい挙動なのでバグな気はする…が、これに関する Issues を探してみたけど見つからなかった。
このへんの句は再利用可能なように関数に小分けにしたいので、クエリビルドのために db
への参照が必要になるのは若干つらい。