Skip to main content
A Cloud Storage binding gives a Telnyx Edge Function a pre-authenticated handle to one of your buckets. You declare the binding in func.toml; the runtime resolves it to env.<BINDING> and injects the credential — your code holds no access key or secret key, and nothing sensitive appears in your bundle or logs. The handle is a small, focused surface — get, put, head, delete, list. It is the in-function counterpart of the S3-compatible API: same buckets, same objects, reached from inside a function instead of over HTTP.
Cloud Storage bindings are TypeScript-only — the typed env handle comes from the @telnyx/edge-runtime SDK (≥ 0.3.0). Other runtimes (JS, Go, Python) don’t get a typed binding.
This guide builds one complete function end to end: a small file API backed by a bucket — PUT, GET, and DELETE an object by key, and list objects by prefix.

1. Scaffold the function

Create a TypeScript function. You also need an existing bucket for it to use — the binding points at a bucket, it doesn’t create one. If you don’t have one, create it first via the Mission Control portal, the AWS CLI, or an S3 SDK.
telnyx-edge new-func --language ts --name file-api
cd file-api

2. Declare the binding

Add a [storage.cloudstorage.<name>] block to the generated func.toml. The block key is a name you choose — it becomes the property on env. Here it’s ASSETS, reached as env.ASSETS:
[edge_compute]
func_id   = "…"           # filled in by new-func
func_name = "file-api"

[storage.cloudstorage.ASSETS]
bucket_name = "my-assets"   # an existing bucket
region      = "us-east-1"   # us-central-1 | us-east-1 | us-west-1 | eu-central-1
Declare more than one bucket by adding more blocks — each [storage.cloudstorage.<name>] becomes env.<name>.

3. Install and generate types

new-func already lists @telnyx/edge-runtime and @aws-sdk/client-s3 in package.json. Install them, then generate the typed env:
npm install
telnyx-edge types
telnyx-edge types writes telnyx-env.d.ts, which types env.ASSETS as CloudStorageBucket so the calls below type-check.

4. Write the function

Replace index.ts with the complete file API. Every bucket operation — list, put, get, delete — goes through env.ASSETS; there are no credentials anywhere in the code.
// index.ts
import * as http from "node:http";
import { env } from "@telnyx/edge-runtime";

// A small file API backed by a Cloud Storage bucket binding (env.ASSETS):
//   PUT    /files/<key>   store the request body as an object
//   GET    /files/<key>   download an object
//   GET    /files         list objects (optional ?prefix=)
//   DELETE /files/<key>   delete an object
const bucket = env.ASSETS;

function sendJson(res: http.ServerResponse, status: number, body: unknown) {
  res.writeHead(status, { "content-type": "application/json" });
  res.end(JSON.stringify(body));
}

async function readBody(req: http.IncomingMessage): Promise<Buffer> {
  const chunks: Buffer[] = [];
  for await (const chunk of req) chunks.push(chunk as Buffer);
  return Buffer.concat(chunks);
}

const server = http.createServer(async (req, res) => {
  // Health probes must stay unauthenticated
  if (req.url === "/health" || req.url?.startsWith("/health/")) {
    res.writeHead(200);
    res.end();
    return;
  }

  const url = new URL(req.url ?? "/", "http://localhost");
  const isCollection = url.pathname === "/files" || url.pathname === "/files/";
  const key = decodeURIComponent(url.pathname.replace(/^\/files\//, ""));

  try {
    // List: GET /files?prefix=
    if (req.method === "GET" && isCollection) {
      const { objects, truncated, cursor } = await bucket.list({
        prefix: url.searchParams.get("prefix") ?? undefined,
        limit: 100,
      });
      return sendJson(res, 200, {
        objects: objects.map((o) => ({ key: o.key, size: o.size, uploaded: o.uploaded })),
        truncated,
        cursor,
      });
    }

    // Upload: PUT /files/<key>
    if (req.method === "PUT" && key) {
      const body = await readBody(req);
      const put = await bucket.put(key, new Uint8Array(body), {
        httpMetadata: { contentType: req.headers["content-type"] ?? "application/octet-stream" },
      });
      return sendJson(res, 200, { key: put?.key, etag: put?.etag });
    }

    // Download: GET /files/<key>
    if (req.method === "GET" && key) {
      const obj = await bucket.get(key);
      if (obj === null) return sendJson(res, 404, { error: "not found" });
      const bytes = Buffer.from(await obj.arrayBuffer());
      res.writeHead(200, {
        "content-type": obj.httpMetadata?.contentType ?? "application/octet-stream",
        "content-length": String(bytes.byteLength),
      });
      res.end(bytes);
      return;
    }

    // Delete: DELETE /files/<key>  (idempotent)
    if (req.method === "DELETE" && key) {
      await bucket.delete(key);
      res.writeHead(204);
      res.end();
      return;
    }

    sendJson(res, 405, { error: "method not allowed" });
  } catch (err: any) {
    sendJson(res, 500, { error: err?.message ?? "internal error" });
  }
});

server.listen(Number(process.env.PORT ?? 8080), () => console.log("file-api up"));

5. Ship it

telnyx-edge ship
When the deploy finishes, get the function’s invoke URL:
telnyx-edge list   # shows STATUS and the INVOKE URL for file-api

6. Try it

With URL set to your function’s invoke URL:
# Upload an object
curl -X PUT "$URL/files/hello.txt" -H "content-type: text/plain" --data "hello from the edge"
# → {"key":"hello.txt","etag":"11c9dad6fdb6ae2efe36b9c7aef39031"}

# Download it back
curl "$URL/files/hello.txt"
# → hello from the edge

# List objects
curl "$URL/files"
# → {"objects":[{"key":"hello.txt","size":19,"uploaded":"2026-07-04T…Z"}],"truncated":false}

# Delete it (idempotent — 204 whether or not it existed)
curl -i -X DELETE "$URL/files/hello.txt"
# → HTTP/1.1 204 No Content