Skip to content
PutPut is shutting down on October 21, 2026. New signups, uploads, and paid plans are closed. Please download your files before then.

Ruby on Rails

Use PutPut as a lightweight alternative to Active Storage. No gem needed — just plain HTTP calls from a controller.

1. Environment variable

# .env or Rails credentials
PUTPUT_TOKEN=pp_guest_...

2. Service class

# app/services/putput_service.rb
class PutputService
  API = "https://putput.io/api/v1"
  TOKEN = ENV.fetch("PUTPUT_TOKEN")

  def presign(filename:, content_type:, size_bytes:)
    uri = URI("#{API}/upload/presign")
    req = Net::HTTP::Post.new(uri)
    req["Authorization"] = "Bearer #{TOKEN}"
    req["Content-Type"] = "application/json"
    req.body = { filename:, content_type:, size_bytes: }.to_json

    res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
    JSON.parse(res.body)
  end

  def confirm(upload_id:)
    uri = URI("#{API}/upload/confirm")
    req = Net::HTTP::Post.new(uri)
    req["Authorization"] = "Bearer #{TOKEN}"
    req["Content-Type"] = "application/json"
    req.body = { upload_id: }.to_json

    res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
    JSON.parse(res.body)
  end
end

3. Controller

# app/controllers/uploads_controller.rb
class UploadsController < ApplicationController
  def presign
    service = PutputService.new
    result = service.presign(
      filename: params[:filename],
      content_type: params[:content_type],
      size_bytes: params[:size_bytes].to_i
    )
    render json: result
  end

  def confirm
    service = PutputService.new
    result = service.confirm(upload_id: params[:upload_id])
    render json: result
  end
end

# config/routes.rb
Rails.application.routes.draw do
  post "uploads/presign", to: "uploads#presign"
  post "uploads/confirm", to: "uploads#confirm"
end

4. JavaScript upload (Stimulus or plain JS)

// app/javascript/upload.js
async function uploadFile(file) {
  // 1. Presign via Rails backend
  const presign = await fetch("/uploads/presign", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-CSRF-Token": document.querySelector("[name='csrf-token']").content,
    },
    body: JSON.stringify({
      filename: file.name,
      content_type: file.type,
      size_bytes: file.size,
    }),
  }).then((r) => r.json());

  // 2. Upload directly to R2
  await fetch(presign.presigned_url, {
    method: "PUT",
    headers: { "Content-Type": file.type },
    body: file,
  });

  // 3. Confirm
  const result = await fetch("/uploads/confirm", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-CSRF-Token": document.querySelector("[name='csrf-token']").content,
    },
    body: JSON.stringify({ upload_id: presign.upload_id }),
  }).then((r) => r.json());

  return result.file.public_url;
}
v0.4.78