railway-storage

Configure and use Railway's S3-compatible storage buckets. Use when implementing file uploads with Railway storage, setting up S3 clients for Railway, or troubleshooting Railway bucket access issues.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "railway-storage" with this command: npx skills add blink-new/claude/blink-new-claude-railway-storage

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}

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

code-review

No summary provided by upstream source.

Repository SourceNeeds Review
General

saas-sidebar

No summary provided by upstream source.

Repository SourceNeeds Review
General

seo-article-writing

No summary provided by upstream source.

Repository SourceNeeds Review
General

kanban-dnd

No summary provided by upstream source.

Repository SourceNeeds Review