mcpField
R
railway-storage

railway-storage

github.com/blink-newCloud & DevOps50 installs
Repository

About railway-storage

railway-storage is a reference skill for using Railway's private S3-compatible storage with the AWS SDK. It covers configuration, uploads, file serving, and common errors, with guidance for proxy endpoints or presigned URLs when files must be accessed publicly.

Key features

  1. 01Configures the S3 client with Railway variables
  2. 02Explains lazy client initialization
  3. 03Uploads and reads bucket objects
  4. 04Serves private files through a proxy endpoint
  5. 05Describes presigned URLs for file access
  6. 06Covers common configuration errors

Use cases

  1. 01Configure an application to use a Railway storage bucket
  2. 02Serve stored files through an API route
  3. 03Troubleshoot upload or endpoint configuration issues

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/blink-new/claude --skill railway-storage

After installing, type/railway-storagein Claude Code to confirm it loaded.

Details

From the project README

Railway S3-Compatible Storage

Railway provides S3-compatible storage buckets that work with standard AWS SDK. However, there are critical differences from AWS S3.

Critical: Private Buckets Only

Railway buckets are private by default and do not support public buckets. The ACL: "public-read" setting is ignored.

To serve files publicly:

  1. Proxy endpoint (recommended) - API route that fetches from S3 and serves to client
  2. Presigned URLs - Generate time-limited signed URLs for direct access

Environment Variables

Railway auto-injects these when you link a storage bucket to your service. Use these exact names:

AWS_ENDPOINT_URL=https://storage.railway.app
AWS_DEFAULT_REGION=auto
AWS_S3_BUCKET_NAME=your-bucket-name
AWS_ACCESS_KEY_ID=tid_xxx
AWS_SECRET_ACCESS_KEY=tsec_xxx

Important: Railway uses AWS_* prefixed names by default. Do NOT use S3_* prefixes as they won't match Railway's injected variables.

S3 Client Setup

Use lazy initialization to avoid build-time errors (env vars unavailable during Docker builds):

import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";

let s3Client: S3Client | null = null;

function getS3Client(): S3Client {
  if (!s3Client) {
    s3Client = new S3Client({
      endpoint: process.env.AWS_ENDPOINT_URL,
      region: process.env.AWS_DEFAULT_REGION ?? "auto",
      credentials: {
        accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
        secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
      },
      forcePathStyle: true, // Required for Railway
    });
  }
  return s3Client;
}

Key points:

  • forcePathStyle: true is required
  • Never access process.env at module level
  • Region is typically "auto"

Upload Implementation

export async function uploadToS3(key: string, body: Buffer, contentType: string): Promise<string> {
  const client = getS3Client();
  const bucket = process.env.S3_BUCKET_NAME!;

  await client.send(new PutObjectCommand({
    Bucket: bucket,
    Key: key,
    Body: body,
    ContentType: contentType,
    // Note: ACL is ignored - Railway buckets are always private
  }));

  // Return proxy URL (not direct S3 URL)
  return `/uploads/${key}`;
}

Proxy Endpoint Pattern

Create an API route to serve files from S3:

// src/app/api/uploads/[...path]/route.ts
import { NextRequest, NextResponse } from "next/server";

export async function GET(
  req: NextRequest,
  { params }: { params: Promise<{ path: string[] }> }
) {
  const { path } = await params;
  const key = path.join("/");

  const result = await getS3Object(key);
  if (!result) {
    return NextResponse.json({ error: "File not found" }, { status: 404 });
  }

  return new NextResponse(result.body, {
    headers: {
      "Content-Type": result.contentType,
      "Content-Length": result.contentLength.toString(),
      "Cache-Control": "public, max-age=31536000, immutable",
    },
  });
}

Helper to read from S3:

export async function getS3Object(key: string) {
  const client = getS3Client();
  const response = await client.send(
    new GetObjectCommand({ Bucket: process.env.AWS_S3_BUCKET_NAME!, Key: key })
  );
  if (!response.Body) return null;

  return {
    body: response.Body.transformToWebStream(),
    contentType: response.ContentType || "application/octet-stream",
    contentLength: response.ContentLength || 0,
  };
}

Next.js Rewrite Rule

// next.config.ts
const nextConfig: NextConfig = {
  async rewrites() {
    return [{ source: "/uploads/:path*", destination: "/api/uploads/:path*" }];
  },
};

Dependencies

bun add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner

Common Issues

IssueCauseSolution
Files upload but return 403Railway ignores ACLUse proxy endpoint
Build fails with missing env varsS3 client at module levelUse lazy initialization
"Invalid endpoint" errorMissing forcePathStyleAdd forcePathStyle: true
Images don't update after uploadBrowser/React Query cachingAdd invalidateQueries()

URL Format

Store proxy URLs in database, not direct S3 URLs:

  • Correct: /uploads/{teamId}/avatar/{filename}
  • Wrong: https://storage.railway.app/bucket/{key}

Comments

Markdown supported