ファイルストレージ

RailwayのS3互換ストレージバケットを使ったファイルのアップロードと配信。

アプリがユーザーアップロードファイル(画像、PDF、エクスポート)を保存する必要があるときは、RailwayのS3互換ストレージバケットを使います。ファイルはデフォルトでプライベートで、短時間有効な署名付きURLでユーザーに配信します。

リファレンス実装

クライアントを自作しないでください。このファイルをそのままアプリにコピーします:

apps/staff-portal/app/_lib/storage.ts  →  apps/<your-app>/app/_lib/storage.ts

2つの関数をエクスポートします: createStorageClientFromEnv()(環境変数を読み、設定済みのS3クライアントを返す)とgetPresignedUrl(storage, key, expiresIn)(時間制限付きのダウンロードURLを生成)。

依存関係を追加

アプリのpackage.jsonに:

package.jsonjson
{
"dependencies": {
  "@aws-sdk/client-s3": "^3.709.0",
  "@aws-sdk/s3-request-presigner": "^3.709.0"
}
}

モノレポルートから pnpm install

環境変数

以下4つがすべて必要です。環境ごとにRailwayでGitHub secret経由で設定します — 環境変数を参照。

変数目的
BUCKET_ENDPOINTS3互換APIのエンドポイント
BUCKET_ACCESS_KEY_ID認証情報
BUCKET_SECRET_ACCESS_KEY認証情報
BUCKET_NAME読み書き対象のバケット

オプション:

  • BUCKET_REGION — デフォルトはus-east-1

必須変数が欠けていると、createStorageClientFromEnv()Missing storage environment variablesをスローします。

クライアントの使い方

サーバー側での利用typescript
import {
createStorageClientFromEnv,
getPresignedUrl,
} from '@/app/_lib/storage';

const storage = createStorageClientFromEnv();

// 7日間有効な署名付きURLを生成
const url = await getPresignedUrl(
storage,
'my-app/photos/avatar.png',
3600 * 24 * 7
);

// URLをブラウザに返す — ブラウザは直接ファイルを取得できる。

バケット構成

すべてのキーにアプリ名のプレフィックスを付けてファイルをグループ化します:

my-app/photos/...
checkin-board/attendances/...
staff-portal/documents/...

こうすることで、1つのバケットが複数アプリを綺麗に扱えます。

つまづきポイント(一度は読んでおく価値あり)

これらはリファレンスのstorage.tsに組み込み済みですが、「修正」してしまわないよう覚えておきましょう:

  • BUCKET_ENDPOINTの末尾スラッシュを削除 — 末尾スラッシュがあるとSignatureDoesNotMatchになります。
  • forcePathStyle: true がRailwayのS3互換APIには必須です。
  • チェックサム計算'WHEN_REQUIRED'に設定する必要があります — AWS SDK v3 ≥ 3.600はデフォルトでチェックサムヘッダを追加し、RailwayのTigrisは403を返します。

Claudeに聞く

新規アプリでファイルアップロードを設定
Claudeプロンプト
meal-plannerアプリにファイルアップロード機能を追加してください。staff-portalからstorage.tsをコピー、@aws-sdk依存関係を追加、「meal-planner/photos/」プレフィックスで画像を保存するPOST /api/uploadルートを作成してください。24時間有効な署名付きダウンロードURLを返してください。

クイズ

Quiz

バケットに保存されたファイルをユーザーにダウンロードさせる正しい方法は?