目次
概要
一般的なコラボレーションツールは、ドキュメント、チャット、タスク管理などを中心に設計されています。
LOFTYは、それとは少し違うアプローチを取っています。
LOFTYは、チームで共有できる仮想デスクトップ型のワークスペースです。ユーザーは共有デスクトップを作成し、ファイルやフォルダを整理しながら、チームメンバーと一緒に作業できます。
一般的なSaaSのダッシュボードを操作するというよりも、普段使っているPCのデスクトップに近い感覚で利用できる環境を目指しています。
ユーザーはデスクトップを作成し、他のユーザーを招待し、ファイルやフォルダを整理し、それぞれのメンバーに適切な権限を設定できます。
また、所有権の移行やアカウント削除など、実際のSaaSサービスで必要になるアカウントライフサイクルについても考慮しています。
このプロジェクトでは、コラボレーション機能だけでなく、認証、権限管理、クラウドストレージ、メール送信、データベース設計、本番環境へのデプロイなど、フルスタックアプリケーションに必要なさまざまな要素を実際に実装しました。
LOFTYのアイデア
共有できるデジタルワークスペース
LOFTYの中心となるアイデアはシンプルです。
「チームで一つのデスクトップを共有できたらどうなるか?」
LOFTYでは、ワークスペースを単なるページやデータベース上のレコードとしてではなく、ひとつのデスクトップ環境として表現しています。
各デスクトップには、以下のような情報が含まれます。
- ファイル
- フォルダ
- チームメンバー
- 権限
- 所有者
ユーザーは複数のデスクトップを作成でき、それぞれ異なるプロジェクトやチームに利用できます。
例えば、開発者であれば個人プロジェクト用のデスクトップ、クライアント用のデスクトップ、チーム開発用のデスクトップをそれぞれ作成できます。
それぞれのデスクトップは独立したワークスペースとして機能し、独自のメンバー構成と権限を持ちます。
なぜデスクトップなのか
デスクトップという仕組みは、多くのユーザーにとってすでに馴染みのある情報整理方法です。
一般的なファイルシステムでは、
- デスクトップにフォルダがある
- フォルダの中にファイルがある
- ファイルはプロジェクトごとに整理される
- 必要に応じて他の人と共有する
という構造になっています。
LOFTYでは、この既存のメンタルモデルをベースにしながら、現代的なSaaSに必要なコラボレーション機能を追加しています。
新しい操作方法を一から覚える必要がなく、すでに知っている「デスクトップ」という概念をそのままチームワークに利用できることを目指しています。
主な機能
共有デスクトップ
LOFTYにおける中心的なリソースが「デスクトップ」です。
ユーザーはプロジェクトやチームを表す名前を付けてデスクトップを作成できます。
各デスクトップには以下の情報があります。
- 一意のID
- 名前
- 所有者
- メンバー
- ファイル
- フォルダ
- 権限
デスクトップのメンバーシップはユーザーごとに独立して管理されるため、あるデスクトップを共有しているからといって、ユーザーが所有する他のデスクトップまで共有されることはありません。
これにより、プロジェクトやチームごとに独立したワークスペースを作ることができます。
ファイル・フォルダ管理
LOFTYでは、一般的なファイルシステムと同じような構造でコンテンツを整理できます。
ユーザーはフォルダを作成し、その中にファイルをアップロードできます。
ファイルについては、例えば以下のような情報を管理しています。
- ファイル名
- ファイルサイズ
- MIMEタイプ
- 所属フォルダ
- 作成日時
- アップロードしたユーザー
- ストレージ上のキー
フォルダの中にさらにフォルダを作成することもできるため、従来のOSに近い階層構造を構築できます。
この構造は、将来的にファイル検索、プレビュー、共有リンクなどの機能を追加する際にも利用できます。
チームコラボレーション
デスクトップは他のLOFTYユーザーと共有できます。
デスクトップの所有者はメールアドレスを使ってメンバーを追加し、そのユーザーに与える権限を選択できます。
メンバーが追加されると、LOFTYからデスクトップの情報と付与されたロールを含む招待メールが送信されます。
基本的な流れは以下の通りです。
- ユーザーがデスクトップを作成する
- 所有者が他のユーザーのメールアドレスを入力する
- ロールを選択する
- ユーザーがデスクトップに追加される
- LOFTYから招待メールが送信される
- 招待されたユーザーが共有デスクトップを開く
メールの送信に一時的な問題が発生した場合でも、データベース上のメンバーシップ自体は保持されます。
つまり、メールサービスの一時的な障害によって、すでに付与されたアクセス権まで失われることはありません。
ロールと権限
LOFTYでは、ロールベースの権限管理を採用しています。
現在のロールは以下の3種類です。
- Owner
- Editor
- Viewer
Ownerはデスクトップに対する完全な管理権限を持ちます。
Editorはデスクトップ上で共同作業できますが、所有者のみが行える管理操作にはアクセスできません。
Viewerはより限定されたアクセス権を持ち、主にワークスペースの内容を確認するユーザーを想定しています。
権限をデスクトップのメンバーシップ単位で管理することで、ファイル、フォルダ、メンバー管理などの操作に一貫したアクセスルールを適用できます。
デスクトップの所有権
LOFTYでは、メンバーであることとデスクトップの所有者であることを明確に分けています。
この設計は、アカウント削除機能を実装する際に特に重要になりました。
ユーザーがデスクトップの所有者である状態でアカウントを削除すると、そのデスクトップの所有者が存在しなくなってしまいます。
そのため、LOFTYではアカウントを削除する前に、所有しているデスクトップを処理する必要があります。
ユーザーは以下のどちらかを選択できます。
- 他のメンバーに所有権を移行する
- デスクトップ自体を削除する
すべてのデスクトップの所有権を手放した後に、アカウントを安全に削除できます。
所有権を移行すると、以前のOwnerはEditorになり、指定されたメンバーが新しいOwnerになります。
これにより、所有者が存在しない孤立したデスクトップが発生することを防いでいます。
アカウント管理
LOFTYには、コラボレーション機能だけでなく、一般的なアカウント管理機能も実装しています。
ユーザーは以下の操作を行えます。
- アカウント登録
- ログイン
- ログアウト
- メールアドレスの認証
- パスワードリセット
- プロフィール更新
- デスクトップメンバー管理
- アカウント削除
アカウント削除では、ユーザーが所有しているデータと他のメンバーが利用しているデータとの関係も考慮しています。
例えば、あるユーザーが削除されたからといって、そのユーザーが作成したファイルまで削除してしまうと、他のメンバーが利用しているコンテンツまで失われる可能性があります。
そのため、データベースのリレーションと削除時の動作を慎重に設計しています。
技術アーキテクチャ
使用技術
LOFTYはTypeScriptを中心としたフルスタック構成で開発しています。
主な技術は以下の通りです。
- TypeScript — アプリケーション全体で型安全性を確保
- React — インタラクティブなフロントエンドを構築
- Vite — フロントエンドの開発・ビルド環境
- Fastify — バックエンドAPI
- PostgreSQL — ユーザー、デスクトップ、メンバー、ファイルなどのデータを保存
- Drizzle ORM — 型安全なデータベースクエリとスキーマ管理
- Zod — APIリクエストとユーザー入力のバリデーション
- Cloudflare R2 — アップロードされたファイルのオブジェクトストレージ
- Resend — トランザクションメールの送信
- Cloudflare — フロントエンドとDNS
- Railway — バックエンドAPIのホスティング
プロジェクト全体はpnpmとTurborepoを使用したモノレポとして構成しています。
モノレポ構成
フロントエンド、バックエンド、共有パッケージを一つのリポジトリで管理しています。
簡略化すると、以下のような構成です。
apps/
web/
api/
packages/
db/
types/
validation/
typescript-config/
Webアプリケーションはユーザーインターフェースを担当し、APIは認証、デスクトップ管理、メンバー管理、ファイル、フォルダなどのサーバーサイド処理を担当します。
共有パッケージには、データベース定義、TypeScriptの型、バリデーションスキーマ、共通設定などをまとめています。
モノレポを採用することで、フロントエンドとバックエンドの間で型やデータ構造を一貫して管理できます。
また、将来的にモバイルアプリなどの別クライアントを追加する場合にも、既存のビジネスロジックを再利用しやすくなります。
バックエンドとAPI
バックエンドにはFastifyを使用し、バージョン管理されたREST APIを提供しています。
APIは機能ごとにルートを分割しています。
主な領域は以下の通りです。
- 認証
- OAuth
- プロフィール
- デスクトップ
- フォルダ
- ファイル
- メンバー
- リアルタイム通信
例えば、デスクトップ関連のAPIは以下のパスから提供しています。
/v1/desktops
認証関連のAPIは、
/v1/auth
から利用できます。
リクエストのバリデーションにはZodを使用しています。
これにより、不正な入力がデータベース処理まで到達することを防ぎ、APIのデータ構造を明確にしています。
また、Fastifyのプラグイン機能を利用して、認証情報などの共通処理を複数のルートで再利用しています。
データベース
リレーショナルデータモデル
アプリケーションの主要なデータベースにはPostgreSQLを使用しています。
ユーザー、デスクトップ、メンバー、ファイル、フォルダなどの関係をリレーショナルデータとして管理しています。
大まかな構造は以下のようになります。
User
├── Desktops
├── Desktop Memberships
└── Sessions
Desktop
├── Members
├── Folders
└── Files
Folder
└── Files
複数のユーザーが複数のデスクトップに参加できるため、メンバーシップは独立したテーブルとして管理しています。
ユーザーIDのリストをデスクトップのレコードに直接保存するのではなく、メンバーシップ自体を独立したデータとして扱うことで、ユーザーのロールや参加日時などの情報も管理できます。
データベース制約
共同利用されるアプリケーションでは、データの整合性が特に重要です。
例えば、同じユーザーが同じデスクトップに複数回登録されることは避ける必要があります。
そのため、データベースの制約を利用して重複を防いでいます。
また、ユーザーやデスクトップが削除された場合に、関連データをどう扱うかについても慎重に設計しています。
デスクトップが削除された場合には、それに関連するリソースも必要に応じて削除します。
一方で、ユーザーが削除された場合には、そのユーザーが作成したファイルやフォルダを無条件に削除するのではなく、他のメンバーが引き続き利用できるようにする必要があります。
こうしたルールは、可能な限りアプリケーションコードだけに頼らず、データベース側でも整合性を保つようにしています。
ファイルストレージ
Cloudflare R2
LOFTYでは、ファイルのメタデータと実際のファイルデータを分離しています。
PostgreSQLにはファイル名や保存場所などのメタデータを保存し、実際のバイナリデータはCloudflare R2に保存します。
構造としては以下のようになります。
PostgreSQL
↓
File metadata
↓
R2 object key
↓
Cloudflare R2
↓
Actual file
大容量のファイルデータをPostgreSQLに直接保存するのではなく、オブジェクトストレージを利用することで、データベースとファイルストレージをそれぞれ独立してスケールさせやすくしています。
ストレージキー
アップロードされた各ファイルには、R2上の対応するオブジェクトを識別するためのストレージキーがあります。
そのため、データベースがアプリケーション上のファイルシステムの情報を管理し、R2が実際のファイルデータを保存するという役割分担になっています。
この構造を利用することで、将来的に以下のような機能も追加できます。
- ファイルプレビュー
- ダウンロードリンク
- ファイルバージョン管理
- ストレージ容量制限
- 大容量ファイルへの対応
- 画像・ドキュメント処理
認証
セッションベース認証
LOFTYでは、セッションベースの認証を採用しています。
ユーザーがログインすると、そのアカウントに紐づいたセッションが作成されます。
セッションには有効期限が設定され、ログアウト時にはセッションを無効化できます。
また、セッション情報をそのままデータベースに保存するのではなく、機密性の高い認証情報についてはハッシュ化して保存しています。
この考え方は、セッション情報だけでなく、パスワードリセットなどのセキュリティ関連トークンにも適用しています。
パスワードのセキュリティ
ユーザーのパスワードをそのままデータベースに保存することはありません。
パスワードはArgon2を使用してハッシュ化してから保存しています。
パスワードリセットではランダムなトークンを生成し、そのトークン自体ではなく安全な形で保存します。
これにより、万が一データベースの内容が漏洩した場合でも、保存されているデータだけから直接パスワードやリセット用の認証情報を利用することが難しくなります。
リアルタイムコラボレーション
LOFTYには、リアルタイムでデスクトップの状態を同期するための基盤も用意しています。
バックエンドにはリアルタイム接続用のエンドポイントがあり、接続するユーザーを認証した上で通信を確立します。
これにより、あるユーザーが行った変更を、同じデスクトップを利用している他のメンバーへページの再読み込みなしで反映できるようになります。
将来的にリアルタイムイベントとして扱えるものには、例えば以下があります。
- ファイルの作成
- ファイルの削除
- フォルダの作成
- フォルダの削除
- メンバー変更
- デスクトップ情報の更新
通常のCRUD操作を行うREST APIと、長時間接続を必要とするリアルタイム通信を分離することで、それぞれを独立して発展させられる構成にしています。
開発で直面した課題
共有デスクトップの設計
開発において最も大きな課題の一つは、「デスクトップとは何なのか」をデータモデルとして明確にすることでした。
単純にデスクトップを一つのフォルダとして扱うこともできます。
しかしLOFTYのデスクトップには、単なるファイルの入れ物以上の役割があります。
例えば、
- 所有権
- メンバー
- ロール
- コラボレーション
- ファイル
- フォルダ
- 権限
などを管理する必要があります。
そのため、デスクトップをフォルダとは別の上位レベルのリソースとして設計しました。
この区別を明確にすることで、メンバーシップや権限管理をよりシンプルに実装できました。
権限と所有権
コラボレーションアプリケーションでは、多くの権限に関するエッジケースが発生します。
例えば、
- Viewerはファイルを変更できるのか?
- Editorは他のユーザーを招待できるのか?
- Ownerは自分自身を削除できるのか?
- Ownerがアカウントを削除したらどうなるのか?
- メンバーではないユーザーに所有権を移行できるのか?
- 元のOwnerのロールはどうなるのか?
といった問題があります。
これらをAPI全体で一貫して処理する必要があります。
LOFTYでは、認証と認可を分けて考えています。
認証では、
「このユーザーは誰なのか?」
を確認します。
認可では、
「このユーザーは、このデスクトップに対してこの操作を行う権限があるのか?」
を確認します。
その上で、権限チェックを共通のヘルパーとして切り出し、各ルートで同じルールを再利用しています。
ファイルストレージの管理
ファイルストレージは、一般的なCRUDアプリケーションよりも複雑な部分があります。
データベースとオブジェクトストレージの状態を一致させる必要があるためです。
例えばファイルのアップロードでは、
- 実際のファイルをR2に保存する
- 対応するデータベースレコードを作成する
という複数の処理が必要になります。
ファイル削除の場合には逆の処理が必要です。
このとき、一方の処理だけが成功するケースも考慮しなければなりません。
そのため、ストレージ層ではエラー処理やクリーンアップ、データベースレコードとR2オブジェクトの整合性について慎重に設計しています。
本番環境の構築
ローカル環境から本番環境へ移行すると、アプリケーションコード以外にも多くの問題を考える必要がありました。
LOFTYは複数のサービスに分かれています。
lofty.social
│
▼
Cloudflare
│
├── Frontend
│
└── API → Railway
│
├── PostgreSQL / Neon
│
└── Cloudflare R2
それぞれのサービスに対して、環境変数、ドメイン、DNS、デプロイ設定などを管理する必要があります。
また、フロントエンドとAPIを別のドメインで運用するため、本番環境ではCORSも正しく設定する必要があります。
開発環境で使用していた、
http://localhost:5173
http://localhost:3001
から、
https://lofty.social
https://api.lofty.social
へ移行することで、本番環境用の設定を明確に分離しました。
デプロイ
フロントエンド
LOFTYのフロントエンドはCloudflare上でデプロイしています。
本番環境のWebサイトは以下からアクセスできます。
https://lofty.social
APIのURLは環境変数で管理しているため、ローカル環境と本番環境で異なるAPIエンドポイントを利用できます。
これにより、本番用のURLをアプリケーションコードに直接ハードコードする必要がありません。
バックエンド
Fastify APIはRailwayにデプロイしています。
本番APIは、
https://api.lofty.social
からアクセスできます。
Railway上でAPIを実行し、PostgreSQLのデータベースとCloudflare R2へ接続しています。
バックエンドはモノレポ内の共有パッケージと一緒にTurborepoを利用してビルドしています。
メール
トランザクションメールにはResendを使用しています。
LOFTYでは以下のようなメールを送信します。
- アカウント認証
- パスワードリセット
- デスクトップへの招待
本番環境ではlofty.socialの認証済みドメインを送信元として使用しています。
開発中はResendのテスト用送信元を使用していましたが、テスト環境では送信先が制限されています。
そのため、本番環境へ移行する際に独自ドメインをResendで認証し、LOFTYから実際のユーザーへ招待メールを送信できるようにしました。
今後のロードマップ
コラボレーション機能
現在の基盤を利用して、さらに多くのコラボレーション機能を追加できます。
今後検討している機能には以下があります。
- リアルタイムでのファイル更新
- ドラッグ&ドロップによるファイル管理
- ファイルプレビュー
- 共有クリップボード
- デスクトップのアクティビティフィード
- ユーザーのオンライン状態
- 通知
- ファイルコメント
- ファイルのバージョン履歴
デスクトップ体験
デスクトップ自体も、今後さらに発展させていく予定です。
例えば、
- カスタム壁紙
- リサイズ可能なウィンドウ
- 複数アプリケーションの同時利用
- コンテキストメニュー
- キーボードショートカット
- デスクトップウィジェット
- カスタムアイコン
- モバイル対応の改善
などを追加できます。
長期的には、単なるファイルブラウザではなく、チームのための完全なデジタルデスクトップ環境へ発展させることを目指しています。
技術的な改善
サービスの規模が大きくなるにつれて、インフラ面も改善していく必要があります。
今後の技術的な改善候補には以下があります。
- 自動化された統合テスト
- より高度なリアルタイム通信基盤
- バックグラウンドジョブ
- ファイル処理用ワーカー
- キャッシュ
- 検索インデックス
- ストレージ容量制限
- 利用状況の分析
- モニタリングとObservability
- データベースクエリの最適化
- より細かい権限管理
まとめ
LOFTYは、非常にシンプルなアイデアから始まりました。
「チームで一つのデスクトップを共有したらどうなるだろう?」
しかし、このアイデアを実際に動くプロダクトへ発展させるためには、単純なファイルブラウザを作るだけでは十分ではありませんでした。
共同利用できるデータモデル、認証・認可、デスクトップの所有権、メンバー管理、クラウドストレージ、メール送信、データベース設計、そして複数サービスの本番デプロイまで、多くの要素を組み合わせる必要がありました。
技術面では、React、Fastify、PostgreSQL、Drizzle、Cloudflare R2、Railway、Cloudflareなど、現代的なTypeScriptベースの技術スタックを実際のサービスに組み込む経験ができました。
また、実際にSaaSを構築する上では、単に機能を実装するだけではなく、権限、エラー処理、データの整合性、アカウントのライフサイクル、デプロイ設定、そしてサービス間の責任範囲まで考える必要があることも学びました。
LOFTYの長期的な目標は、デスクトップというコンセプトをさらに発展させ、チームがプロジェクト、ファイル、コミュニケーションを一つの環境にまとめられる、柔軟で使いやすいコラボレーションワークスペースを作ることです。