ファイルストレージ

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

Railwayストレージバケットの作成

プラットフォームアプリは Railway環境ごとに1つのバケット を共有します(ステージングと本番は別インスタンス・別認証情報)。

1. Railwayでバケットを作成

2回行います — Railwayで staging を選択した状態で1回、production を選択した状態で1回。

  1. クライアントのRailwayプロジェクトを開く(例: shimomoto-vibe-coding-platform)。
  2. 環境ドロップダウンで staging(または production)に切り替える。
  3. プロジェクトキャンバスで CreateBucket
  4. リージョンを選択(後から変更不可)。表示名を設定(例: shimomoto-storage)。
  5. バケットのデプロイ完了を待つ。
  6. バケットを開き Credentials タブから以下を控える:
Railway Credentialsプラットフォーム環境変数備考
ENDPOINTBUCKET_ENDPOINT例: https://storage.railway.app末尾スラッシュなし
ACCESS_KEY_IDBUCKET_ACCESS_KEY_IDS3アクセスキー
SECRET_ACCESS_KEYBUCKET_SECRET_ACCESS_KEYS3シークレットキー
BUCKETBUCKET_NAMES3 API用バケット名(ハッシュ付き)。RAILWAY_BUCKET_NAME ではない
REGIONBUCKET_REGION通常 auto

Railwayドキュメント: Storage Buckets

2. GitHub Environmentsに値を登録

クライアントリポジトリ(algoritmi-tech/<client>-vibe-coding-platform)で Settings → Environments を開く。

stagingproduction の両方に登録 — 値はそのRailway環境で作成したバケットの Credentials から:

GitHubキー推奨タイプ取得元
BUCKET_ENDPOINTVariableRailway ENDPOINT
BUCKET_ACCESS_KEY_IDSecretRailway ACCESS_KEY_ID
BUCKET_SECRET_ACCESS_KEYSecretRailway SECRET_ACCESS_KEY
BUCKET_NAMEVariableRailway BUCKET
BUCKET_REGIONVariableRailway REGION

非機密(ENDPOINTBUCKETREGION)は Variables、キー類は Secrets に保存。

3. ストレージを使うアプリを再デプロイ

CIはデプロイ時にGitHub Environmentの vars/secrets を読み、deploy-reusable.yml 経由で Railway サービスに BUCKET_* を設定します。

アプリワークフロー転送するバケット設定
Database Accessordeploy-app-database-accessor-staging/production.ymlBUCKET_* 5つすべて
App Portaldeploy-app-app-portal-staging/production.ymlBUCKET_SECRET_ACCESS_KEY(他は Variable から)
Vibe Coding Guidedeploy-app-vibe-coding-guide-staging/production.ymlBUCKET_SECRET_ACCESS_KEY(他は Variable から)

GitHub更新後、該当デプロイワークフローを実行(または main に push)。Railway → サービス → VariablesBUCKET_ENDPOINT 等が表示されることを確認。

バケット必須のアプリ: database-accessor(ストレージブラウザ)、および storage.ts / アップロードAPIを追加したアプリ。

環境変数(ランタイム)

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

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

オプション:

  • BUCKET_REGION — 未設定時は auto(Railwayバケットは通常 auto)。

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

バケット設定のトラブルシュート

症状対処
ストレージ画面で環境変数不足正しいGitHub Environmentに BUCKET_* を設定し再デプロイ
SignatureDoesNotMatchBUCKET_ENDPOINT の末尾スラッシュを削除
ステージングのみ動作本番用バケット + GitHub production 用キーが別途必要
Railwayで BUCKET_SECRET_ACCESS_KEY だけ空アプリのデプロイワークフロー secrets: で転送する

クライアントの使い方

サーバー側での利用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

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