Use case

Accept SFTP uploads in Cloudflare Workers, Vercel or Lambda

Your customers upload over SFTP. Your function gets a signed webhook with a download link.

The problem. Sound familiar?

  • Cloudflare Workers, Vercel and Lambda can't listen on port 22 or keep files on a disk.

  • Your customers or partners still insist on sending files over SFTP.

  • Running a VM just for SFTP means patching, monitoring and storage you didn't want to own.

How it works with CloudFile, in about a minute.

  1. Step 1

    Create a server and users

    One user per customer of your app, created in the dashboard.

    customer-42 SFTP password + key
  2. Step 2

    Add a webhook listener

    Point it at your function's URL and choose the events.

    upload → https://api.example.com/sftp
  3. Step 3

    Handle it in your function

    Verify the signature and fetch the file from the presigned URL. No disk and no long-running process.

    POST /sftp /reports/q3.csv 200 42 ms

What you get, and nothing you have to run.

  • Fits any runtime

    Workers, Vercel and Netlify functions, Lambda, Cloud Run, Fly. Anything that can receive an HTTPS POST.

  • No file in the request

    The webhook is small JSON with a presigned download URL, so you stay within body-size and memory limits.

  • Signed with HMAC-SHA256

    Verify with the Web Crypto API in a few lines. Stale timestamps are easy to reject.

  • Retries with fresh links

    If your function fails, every retry carries a new download URL that still works.

  • Filters at the source

    Only call your function for the paths, users and events you care about.

  • EU data residency

    Files are stored in the EU, which matters when your app serves European customers.

A Cloudflare Worker that receives uploads. Uses only the Web Crypto API, so it runs unchanged on Workers, Vercel Edge and Deno.

export default {
  async fetch(request: Request, env: { CLOUDFILE_SECRET: string; BUCKET: R2Bucket }) {
    const body = await request.text();
    const header = request.headers.get("X-CloudFile-Signature") ?? "";
    const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
    if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return new Response("stale", { status: 401 });

    const key = await crypto.subtle.importKey(
      "raw", new TextEncoder().encode(env.CLOUDFILE_SECRET),
      { name: "HMAC", hash: "SHA-256" }, false, ["verify"],
    );
    const sig = Uint8Array.from(v1.match(/../g)!.map((h) => parseInt(h, 16)));
    const ok = await crypto.subtle.verify("HMAC", key, sig, new TextEncoder().encode(`${t}.${body}`));
    if (!ok) return new Response("bad signature", { status: 401 });

    const event = JSON.parse(body);
    const file = await fetch(event.download_url);
    await env.BUCKET.put(event.full_path.slice(1), file.body);
    return new Response("ok");
  },
};

Questions about this setup.

Is the file sent in the webhook body?

No. The webhook carries metadata and a download_url that's valid for an hour. Fetch the file with a plain GET when you're ready.

My function times out on big files. What can I do?

Respond 200 quickly and process the file in a background task or queue, such as waitUntil on Workers or a queue on Lambda. The link stays valid for an hour, and retries get a fresh one.

Can I create SFTP users from my app?

Today users are managed in the dashboard. A public API for users is on our roadmap.

Can webhooks reach a private network or localhost?

No. Webhook URLs must use HTTPS and resolve to a public address.

More in the docs: Webhooks, payload and signatures · Quickstart

Other jobs for the same server.

Your SFTP server is one click away. Free for 7 days, no card needed.