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
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