Skip to content

Latest commit

 

History

31 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

🚀 Telnyx Edge Compute CLI

Deploy serverless functions to the edge with simple commands.


🚀 Quick Start

# Install (Linux x64)
VERSION=$(curl -s https://api.github.com/repos/team-telnyx/edge-compute/releases/latest | grep '"tag_name"' | cut -d'"' -f4)
curl -sSL https://github.com/team-telnyx/edge-compute/releases/download/$VERSION/telnyx-edge-$VERSION-linux-amd64.tar.gz | tar -xzf -
sudo mv telnyx-edge-$VERSION-linux-amd64/telnyx-edge /usr/local/bin/telnyx-edge
chmod +x /usr/local/bin/telnyx-edge

# Authenticate
telnyx-edge auth login

# Check status
telnyx-edge status

🧑‍💻 Usage

telnyx-edge [global options] <command> [command options]
Command Description
help Show help and usage information
status Check CLI status and configuration
list List your functions
secrets Manage secrets
bindings Manage Telnyx API key bindings
storage Manage storage resources (KV namespaces, SQL databases)
types Generate TypeScript types for your bindings
new-func Create a new function
update Update the CLI to the latest release
ship Deploy function to edge
delete-func Delete a function
reset-func Reset a failed function back to created so it can be re-shipped
revisions List a function's deploy history
rollback Instantly revert a function to a previous revision
inspect Show a function's full details
metrics Show a function's recent request and resource metrics
actors Manage StatefulActor types
network Inspect and configure your Edge Compute networks and their routes
tunnel Expose a service on your machine to your functions
dev Run a telnyx.toml project locally (function + actors)
config View and change CLI preferences

Flags

Flag Description
--verbose / -v Enable verbose logging (available on all commands)
--version Print the CLI version and exit — root command only (telnyx-edge --version), works without auth or config
# Check which version you're running (handy when reporting bugs)
telnyx-edge --version

Core Examples

# Create a new function from template
telnyx-edge new-func --language=go --name=hello-world
cd hello-world

# Deploy to edge
telnyx-edge ship

# Start from code you already have (advanced)
telnyx-edge new-func --from-dir=./my-existing-app --name=my-webhook
telnyx-edge ship --from-dir=my-webhook

# Give a slow build even more room than the 30m default
telnyx-edge ship --timeout=45m

# List your functions
telnyx-edge list

# Delete a function (asks for confirmation)
telnyx-edge delete-func hello-world

# ...or skip the prompt, for scripts and CI
telnyx-edge delete-func hello-world --yes

ship stays attached and reports each stage until the function is live, which is why an unqualified ship can sit there for a while: it waits up to 30 minutes before giving up. That is a monitoring limit, not a deploy limit — on timeout the deploy carries on server-side and telnyx-edge ship status <func> tells you how it ended. Pass --timeout to wait longer, or to cap the wait shorter for CI.

Destructive commands name what they are about to destroy and wait for you to type yes — the mistake worth catching is a wrong id, which a plain "are you sure?" would miss. Without a terminal they fail with an error naming --yes rather than prompting, so a pipeline never hangs on input that will not arrive.

Stop being asked entirely with TELNYX_EDGE_SKIP_CONFIRMATIONS=1 for a shell or CI job, or telnyx-edge config set skip_confirmations true permanently.

Private functions

To disable public access, add private = true to the existing [edge_compute] section in func.toml or telnyx.toml:

[edge_compute]
func_id = "your-function-id"
func_name = "my-function"
private = true

Ship the function to apply the change. After deployment, Knative restricts the function to cluster-local routing and removes its public Kourier route. This flag does not create a private network connection.

To restore public access, set private = false or remove the field. Then ship again.

Project shape

Your manifest picks how the project is built, so ship checks the entrypoint agrees before uploading anything.

A func.toml project is built by the buildpack and starts its own HTTP server. A telnyx.toml project is served by the Telnyx function runtime and exports a handler:

export default {
  async fetch(request: Request): Promise<Response> {
    return Response.json({ ok: true });
  },
};

That handler in a func.toml project fails locally with the fix named: the container would export the object, exit, and the deploy would time out. Either rename the manifest to telnyx.toml and add main and compatibility_date, or start a server and reach bindings with import { env } from "@telnyx/edge-runtime". The reverse pairing is only a warning, and the check stays quiet whenever it cannot tell — it never fails a ship on a guess.

ship also warns when your code reaches a StatefulActor your manifest does not declare:

⚠️  src/rooms.ts:4 calls `env.ROOM.idFromName(…)`, but no [[actors]] block in telnyx.toml declares the binding `ROOM`.

That combination is worth catching early because nothing downstream reports it: env is built from the manifest, so an undeclared env.ROOM is simply undefined and only throws on the code path that uses it. The build succeeds, the deploy succeeds, and the health check and your plain-HTTP routes all keep answering — while a WebSocket upgrade handed to the actor comes back as a bare 500. For a telnyx.toml project the whole bundled module graph is checked, so a call in any file that ships is found; a func.toml project uploads its sources unbundled, so only its entrypoint is read. It is always a warning and never blocks a ship.

For telnyx.toml projects, ship asks before upload if its check detects an actor type whose owning function was deleted and whose class this bundle exports. Shipping makes this function the owner and redeploys the actor with this function's code and actor environment variables, preserving its state. Use ship --yes to confirm in scripts; otherwise a required confirmation fails without a terminal. The check is advisory and is skipped if the actor lookup fails. Non-fatal deployment-service warnings are printed after upload.

Revisions & Rollback

Every successful ship creates an immutable revision. Inspect a function's deploy history and instantly revert to a previous revision — no rebuild or re-upload required.

# Show a function's recent deploys, and why any of them failed (newest first)
telnyx-edge deployments my-func

# Roll back to a previous revision (instant traffic switch)
telnyx-edge rollback my-func <revision-id>

deployments prints one row per ship: the revision id (a short image SHA), the author, timestamp, outcome, build duration, which revision is currently active, the commit, and — for a ship that failed — the stage it failed at and the reason, as columns on that ship's own row. A long reason is shortened with … to keep the table readable; --json gives you the full text. rollback switches the active revision to an existing one within seconds; a ship that never reached a healthy deploy cannot be a rollback target, and one whose build failed has no revision id at all.

Why a Ship Failed

When a ship fails, ship status tells you why — classified by where it failed — so you don't have to read the raw build log.

# One actionable line: the failure reason (or ✅ if the last ship succeeded)
telnyx-edge ship status my-func

# Add the build log or crash output, when the platform sent one
telnyx-edge ship status my-func --logs

The reason comes straight from the platform. A build failure shows the compiler/build error (and the log snippet under --logs); a deploy failure shows why — e.g. the function crashed on startup (with its own crash output under --logs) or never became ready; a platform or security review failure shows a short explanation on its own line. ship status is read-only and accepts a function name or id.

Logs

Once a function is deployed, logs shows either what it printed or the traffic it served.

# The last hour of runtime output (console.log/error), up to 50 lines
telnyx-edge logs my-func

# One line per HTTP request served, emitted by the platform
telnyx-edge logs my-func --type invocations

# Narrow the window
telnyx-edge logs my-func --since 10m

# More lines, or machine-readable output
telnyx-edge logs my-func --last 200
telnyx-edge logs my-func --json | jq -r '.data[].message'

# Watch new lines as they happen, until Ctrl-C
telnyx-edge logs my-func --tail

--type picks the stream: runtime (default) is your code's own output; invocations is one record per request, so it reports traffic even for a function that logs nothing. Runtime lines print as [timestamp] [level] message; invocation lines print as [timestamp] method status_code <duration>ms. Both are oldest first. --since (default 1h, max 24h) chooses the window; --last (default 50, max 250) caps how many lines are shown. Values above the maximum are clamped, not rejected.

--tail watches instead of reading history. It stays attached and prints lines as they arrive, until you press Ctrl-C. With no --type it shows both streams interleaved — your output and one line per request — which is the difference from the default mode, where no --type means runtime. --since and --last do nothing here, since there is no history to page through; --json prints one object per line rather than a single array, because the stream has no end. A dropped connection reconnects on its own; an authentication failure or a missing function stops instead of retrying.

Tail is best-effort and carries no history: it shows what arrives while you are attached, and nothing from before. Use logs without --tail when you need the persisted record.

Three things worth knowing:

  • Logs are not live by default. A line reaches the platform a few seconds after your function writes it or serves a request — make a request, wait a moment, then run logs. Use --tail to watch them arrive instead.
  • level is best-effort (runtime logs only) and is often wrong on stack traces, so don't use it to decide whether something failed.
  • A multi-line message arrives as several lines. A stack trace is not grouped into one entry.

If logs could not be read from every location your function runs in, logs says so on stderr rather than quietly returning a partial answer.

Log Export

log-export pushes a function's runtime and/or invocation logs to an external OTLP endpoint (Honeycomb, Datadog, Grafana, your own collector, ...) as they happen, instead of only being readable via logs.

# Push both runtime and invocation logs to an OTLP endpoint, attaching an auth header your collector expects
telnyx-edge log-export set my-func --endpoint https://api.honeycomb.io/v1/logs --header x-honeycomb-team=abc123

# Push only invocation logs (one record per HTTP request)
telnyx-edge log-export set my-func --endpoint https://api.honeycomb.io/v1/logs --invocations

# Show the current destination and which types are exported
telnyx-edge log-export get my-func

# Stop exporting
telnyx-edge log-export delete my-func

set replaces any existing destination for the function. --runtime/--invocations pick which types go to it: naming neither exports both (the default, same as before these flags existed); naming just one exports only that type. --header may be repeated to attach multiple headers to every push. Headers are encrypted at rest and never shown back — get only reports the endpoint and which log types are exported. delete is idempotent: it succeeds even if nothing was configured.

Metrics

Use metrics to summarize recent traffic and resource usage for a deployed function.

# Last 24 hours
telnyx-edge metrics my-func

# Narrow the window
telnyx-edge metrics my-func --since 6h

# Focus on HTTP 5xx errors
telnyx-edge metrics my-func --errors

# Machine-readable aggregate response
telnyx-edge metrics my-func --json

The command accepts a function name or id. --since defaults to 24h and accepts Go durations such as 10m, 2h, or 168h; the maximum is 168 hours. The summary includes request counts, HTTP 2xx/4xx/5xx rates, latency percentiles, average request latency, CPU usage, and memory usage. Detailed exception categories, top error messages, duration GB-s totals, and outbound fetch metrics are not available from the function metrics API yet.

Resetting a Failed Function

If a function gets stuck in a failed state (build_failed, deploy_failed, or delete_failed), reset-func tears down its deployed resources and returns it to created — without changing the function's id, name, or config — so you can fix the issue and re-ship it.

# Reset a failed function, then re-deploy it
telnyx-edge reset-func broken-func
telnyx-edge ship --from-dir=broken-func

Reset only applies to a failed function. A healthy function (deploy_ok) cannot be reset — use delete-func instead — and a function with an operation already in progress must finish (or time out) first. Teardown runs asynchronously; the function returns to created shortly after the command succeeds.

Bindings (Telnyx API Integration)

Bindings provide automatic Telnyx API key injection for your edge functions, enabling secure access to Telnyx APIs without hardcoding credentials.

# Create a binding for your organization
telnyx-edge bindings create

# View current binding
telnyx-edge bindings get

# Validate your binding works
telnyx-edge bindings validate

# View binding details
telnyx-edge bindings get

# Update binding (regenerates tokens)
telnyx-edge bindings update

# Delete binding
telnyx-edge bindings delete

Once a binding is created, your functions automatically receive TELNYX_API_KEY and TELNYX_BASE_URL environment variables, allowing seamless use of the Telnyx SDK.

Custom Domains

Custom domains let you route your own hostname to an edge compute function over HTTPS. You verify DNS ownership with a TXT record, then upload a TLS certificate to enable HTTPS routing.

# Add a custom domain mapping (returns a verification token and TXT record instructions)
telnyx-edge domains add api.example.com <function_id>

# Verify domain ownership — checks DNS for a TXT record at _telnyx-verification.<hostname>
telnyx-edge domains verify api.example.com

# List all custom domains for your organization
telnyx-edge domains list

# Upload a TLS certificate (PEM-encoded cert + key) to enable HTTPS routing
telnyx-edge domains cert upload api.example.com --cert cert.pem --key key.pem

# Delete a custom domain mapping and its associated routing
telnyx-edge domains delete api.example.com

Flow: domains add → add the TXT record → domains verify → domains cert upload → HTTPS routing is live.

Tunnels

A tunnel exposes a service running on your machine to a function, without opening an inbound port or running as root. The CLI keeps an outbound connection to a Telnyx relay; when traffic reaches an address you have routed to the tunnel, the relay carries it back down that connection and the CLI dials the address on your machine.

# 1. Create a tunnel and copy its token (shown once)
telnyx-edge tunnel create chicago-dc

# 2. Route a CIDR on one of your networks through it
telnyx-edge network route add acme-prod 10.20.0.0/16 --tunnel chicago-dc

# 3. Run it on the machine hosting the service — the token is all it needs
telnyx-edge tunnel run --token tnl_...

# 4. Check what is connected
telnyx-edge tunnel get chicago-dc

network route add takes the network's name or id, and --tunnel takes the tunnel's name or id. Once a route is in place, traffic from a function on that network to an address inside the CIDR is carried to the machine running the tunnel, which dials that address — so the address has to be reachable from that machine.

The token is the only thing run needs. It connects to the relay (tunnel-relay.telnyx.com:8443, over TLS), not the service API, so run does not read your CLI config and works on a host with no ~/.telnyx-edge and no writable home — a container, or a machine where you are not root. tunnel create and tunnel rotate-token print the token once and never again; tunnel get and tunnel list do not show it, and there is no way to read it back. Store it when you create it.

# List your tunnels (name, id, status, connections)
telnyx-edge tunnel list

# Show a tunnel's status and the processes connected to it
telnyx-edge tunnel get chicago-dc

# Issue a new token and invalidate the old one
telnyx-edge tunnel rotate-token chicago-dc

# List a network's routes, including the ones the platform writes itself
telnyx-edge network route list acme-prod

# Remove a route
telnyx-edge network route delete <route-id>

# Remove a tunnel (refused while a route still points at it)
telnyx-edge tunnel delete chicago-dc

A tunnel with no process connected to it (tunnel get shows CONNECTIONS of 0) relays nothing, so check that first if traffic is not arriving. tunnel delete is refused while a network route still names the tunnel and lists the routes to remove — delete those first, then the tunnel. A route CIDR must be the network address (10.20.0.0/16, not 10.20.3.7/16) and must not overlap ranges the platform reserves for itself.

tunnel run reconnects on its own if the session drops. If the relay refuses the token — a token that rotate-token has replaced — it stops with a non-zero exit instead of retrying forever. If a route was added just now, the first call through it can fail once while it is applied; retry.

KV Storage (Key-Value Storage)

KV storage provides persistent key-value storage for your edge functions. Create namespaces to store and retrieve data at the edge.

Namespace management:

# List all KV namespaces
telnyx-edge storage kv list

# Create a new KV namespace
telnyx-edge storage kv create --name my-data-store

# Get details of a specific namespace
telnyx-edge storage kv get <namespace-id>

# Delete a namespace
telnyx-edge storage kv delete <namespace-id>

# List with pagination
telnyx-edge storage kv list --page 2 --page-size 20

Key operations:

# List keys in a namespace (with optional prefix filter)
telnyx-edge storage kv key list <namespace-id>
telnyx-edge storage kv key list <namespace-id> --prefix config/

# Get a key's value (raw bytes printed to stdout)
telnyx-edge storage kv key get <namespace-id> <key>

# Put a text value
telnyx-edge storage kv key put <namespace-id> <key> "hello world"

# Put a binary value from a file (application/octet-stream)
telnyx-edge storage kv key put <namespace-id> <key> --path ./data.bin

# Put a value with a time-to-live (key auto-expires after the duration)
telnyx-edge storage kv key put <namespace-id> <key> "session token" --ttl 30m

# Delete a key (idempotent)
telnyx-edge storage kv key delete <namespace-id> <key>

Keys support hierarchical paths using / as a separator (e.g. config/db/host). Key list results include cursor-based pagination — when there are more results, use --cursor <value> to fetch the next page.

Use --ttl to give a key a time-to-live; it is set when the value is written and the key is automatically deleted after the duration elapses. The value is a Go duration string (e.g. 30s, 5m, 1h, 24h) and must be a whole number of seconds — at least 1s, no sub-second precision. Keys written without --ttl persist until explicitly deleted.

Once a KV namespace is created, you can bind it to your functions and access it via the runtime API to store and retrieve data.

SQL Databases

SQL databases give your functions a real SQLite database at the edge. Create a database, apply a schema, and bind it to a function — the function reaches it through env.<BINDING> with a familiar prepare/bind/query API.

Database management:

# List all SQL databases
telnyx-edge storage sqldb list

# Create a new database
telnyx-edge storage sqldb create --name my-app-db

# Get details of a specific database
telnyx-edge storage sqldb get <database-id>

# Delete a database
telnyx-edge storage sqldb delete <database-id>

A database is ready to use as soon as it reports provision_ok — there is no server to size and nothing to deploy per database.

Running SQL:

# Run a statement directly against the database
telnyx-edge storage sqldb execute <database-id> --remote \
  --command "CREATE TABLE links (id INTEGER PRIMARY KEY, url TEXT NOT NULL)"

# Run a .sql file (schema, seed data, an import)
telnyx-edge storage sqldb execute <database-id> --remote --file ./schema.sql

# Bind values instead of writing them into the SQL
telnyx-edge storage sqldb execute <database-id> --remote \
  --command "SELECT * FROM links WHERE url = ?" --param "https://example.com"

# Machine-readable results
telnyx-edge storage sqldb execute <database-id> --remote \
  --command "SELECT * FROM links" --json

execute runs SQL out-of-band, so you can bootstrap a schema before writing a single line of function code.

Local databases (--local):

# Same commands, against a database file on this machine instead of the deployed one
telnyx-edge storage sqldb execute DB --local \
  --command "CREATE TABLE links (id INTEGER PRIMARY KEY, url TEXT NOT NULL)"
telnyx-edge storage sqldb migrations list DB --local
telnyx-edge storage sqldb migrations apply DB --local

Every execute and migrations list/apply command takes --remote (the deployed database) or --local; one of the two is required. A local database is separate from the deployed one: it never reads or changes deployed data, and no deployed database has to exist first. It runs on the same SQL engine as deployed databases, inside the actor-runtime container image, so --local needs Docker running (the image is pulled on first use).

Pass the [storage.sqldb] binding name or id from telnyx.toml (any other name gets its own local database). Local databases live in .telnyx/sqldb/, are git-ignored automatically, and persist between runs; telnyx-edge dev serves env.<BINDING> from the same files. To start over, delete .telnyx/sqldb/. --persist-to <dir> keeps a database elsewhere instead (binding names still resolve against telnyx.toml in the current directory, so run it from the project that owns the binding); dev always reads .telnyx/sqldb/, so it does not see those, and you delete that directory yourself.

--param binds a value to the next ? placeholder and is repeatable; values fill the placeholders left to right, in the order the flags appear. It always sends a string — use --param-json for a number, boolean, or null (--param-json 42, --param-json true, --param-json null), since a shell cannot otherwise express the difference between 42 and "42". The number of values must match the number of placeholders. Bind anything that came from outside your own script this way rather than pasting it into the statement.

Migrations:

# Create a numbered migration file
telnyx-edge storage sqldb migrations create <database-id> add_links_table

# See what is applied and what is pending
telnyx-edge storage sqldb migrations list <database-id> --remote

# Apply everything pending, in order
telnyx-edge storage sqldb migrations apply <database-id> --remote

Migration files live in migrations/<database>/ with an auto-incrementing numeric prefix. Applied migrations are recorded inside the database itself, so apply is safe to re-run — it only applies what is still pending.

Exporting:

# Everything: schema and data
telnyx-edge storage sqldb export <database-id> --remote --output ./database.sql

# One table, or several — --table is repeatable
telnyx-edge storage sqldb export <database-id> --remote --table links --output ./links.sql
telnyx-edge storage sqldb export <database-id> --remote --table links --table clicks --output ./subset.sql

# Schema only, or data only
telnyx-edge storage sqldb export <database-id> --remote --no-data --output ./schema.sql
telnyx-edge storage sqldb export <database-id> --remote --no-schema --output ./data.sql

# Copy one database into another (dumps within the ~4 MiB remote script limit)
telnyx-edge storage sqldb export <source-id> --remote --output - \
  | telnyx-edge storage sqldb execute <target-id> --remote --file -

export writes the database out as SQL text — CREATE statements, then INSERT statements, then the indexes, views and triggers. That makes it the way to take a backup, move a database between environments, or just read what is actually stored. execute --remote --file, piped or from a file, sends the whole file as one request, so it re-imports a dump only if the dump fits the ~4 MiB SQL script limit for remote execution, and refuses a larger one as too large. The dump is still a complete, valid SQL file: execute --local --file is not subject to the remote request limit, and sqlite3 reads the file directly. Exports themselves go up to 4 GiB. --output is required; - writes to stdout so the dump can be piped. The file is written in full and then moved into place, so an interrupted export never overwrites a previous backup with a partial one. On macOS and Linux it is created readable only by you, since a dump is every row of your database in plaintext — on Windows, file permissions are left to the directory you write into.

Values keep their exact type on the way out and back: integers too large for a JSON number, full-precision floats, BLOBs and text that is not valid UTF-8 all re-import as what they were. AUTOINCREMENT counters are restored too, so a re-imported database will not re-issue a row id the original had already used.

An export is a job: the command queues it, waits, then downloads the result. The dump is written to your own Telnyx cloud storage and fetched from there — nothing extra to configure, since the CLI uses the credentials you are already signed in with, and the copy in your bucket is cleaned up once the download window closes.

Databases containing virtual tables (for example FTS5) cannot be exported, because their contents live in shadow tables that do not round-trip; the export refuses rather than writing a file that would fail on import. An export is also not a point-in-time snapshot: a write committed while it runs may be partially reflected.

Binding a database to a function — add the block to your function's manifest (func.toml, or telnyx.toml if your project uses one):

[storage.sqldb.DB]
id = "<database-id>"

Every function bound to the same id shares one database.

Then use the database from your function:

import { env } from "@telnyx/edge-runtime";

// One statement, no results.
await env.DB.exec(
  "CREATE TABLE IF NOT EXISTS links (id INTEGER PRIMARY KEY, url TEXT NOT NULL)",
);

// Bind values instead of concatenating them into the SQL.
await env.DB.prepare("INSERT INTO links (url) VALUES (?)").bind(url).run();

// .all() returns every row; .first() returns one value or row.
const { results } = await env.DB.prepare("SELECT id, url FROM links ORDER BY id").all();
const count = await env.DB.prepare("SELECT COUNT(*) AS n FROM links").first<number>("n");

Bindings are read from the env object exported by @telnyx/edge-runtime, so import it as shown above. env.DB requires @telnyx/edge-runtime 0.9.0 or newer.

Run telnyx-edge types to give env.DB its real type.

Typed Bindings

Every binding you declare is reachable in TypeScript as env.<BINDING>. telnyx-edge types reads your manifest and writes a telnyx-env.d.ts at the project root giving each one its real type:

telnyx-edge types
✓ Generated binding types for 2 binding(s) at telnyx-env.d.ts
    env.DB → SqlDatabase
    env.CACHE → KvNamespace

With that file in place, env.DB autocompletes with the full query API and a misspelled or undeclared binding fails to compile instead of surfacing as undefined at runtime.

import { env } from "@telnyx/edge-runtime";

const { results } = await env.DB.prepare("SELECT id FROM links").all();
await env.CACHE.put("last-run", new Date().toISOString());

env.TYPOED_NAME;  // compile error — not a declared binding

Re-run it whenever you add, rename, or remove a binding. It reads only your local manifest — no network, no authentication — so it is safe to run in a build script or a pre-commit hook.

Binding types require a recent @telnyx/edge-runtime; types checks the installed version and tells you which one a given binding needs.

StatefulActors

A StatefulActor is a named, addressable object that owns its state. Each instance is identified by a name you choose, and the platform guarantees there is one live instance per name — so calls to the same name are serialized and never race. It is the right tool when a piece of state has a natural identity: one room, one session, one tenant, one document.

Actors are TypeScript-only and declared in a telnyx.toml manifest:

name = "chat"
main = "src/index.ts"
compatibility_date = "2026-05-01"

[[actors]]
binding = "ROOM"     # how your code reaches it
type    = "ChatRoom" # the exported class name

Scaffold a project with the actor wiring already in place:

telnyx-edge new-func --actor --name chat

Your module exports both the actor class and the HTTP handler:

import { ChatRoom } from "./chat-room.js";
export { ChatRoom };            // registers the class with the runtime

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    // Address an instance by name — same name, same instance, every time.
    const room = env.ROOM.idFromName("general");
    const messages = await room.history();
    return Response.json({ messages });
  },
};

Extend StatefulActor to get this.ctx. ctx.storage is that instance's private state — a key/value API, plus ctx.storage.sql for a full SQLite database belonging to that one instance. sql.exec(...) takes the statement and its bind parameters and returns a cursor; call .toArray() for the rows:

import { StatefulActor } from "@telnyx/edge-runtime";

type Message = { id: number; body: string };

export class ChatRoom extends StatefulActor {
  async post(body: string) {
    this.ctx.storage.sql.exec(
      "CREATE TABLE IF NOT EXISTS messages (id INTEGER PRIMARY KEY AUTOINCREMENT, body TEXT)",
    );
    this.ctx.storage.sql.exec("INSERT INTO messages (body) VALUES (?)", body);
  }

  async history(): Promise<Message[]> {
    return this.ctx.storage.sql
      .exec<Message>("SELECT id, body FROM messages ORDER BY id")
      .toArray();
  }
}

Writes are acknowledged only once they are durable, and an instance rehydrates its database when it next activates — so state survives the pod being replaced.

Managing deployed actor types:

# List the actor types in your account
telnyx-edge actors list

# Inspect one type
telnyx-edge actors inspect ChatRoom

# List a type's persisted instances (metadata only, no stored values)
telnyx-edge actors instances ChatRoom

# Recent request, resource, and storage metrics for a type (storage is the type's
# total ctx.storage.sql size across all instances, not any one instance's size).
# --since maxes out at 90h here, not the 168h of the root `metrics` command.
telnyx-edge actors metrics ChatRoom

# Recent runtime (console.log) and invocation logs for a type, interleaved by time
telnyx-edge actors logs ChatRoom

# --type narrows to just one stream
telnyx-edge actors logs ChatRoom --type invocations

# Stay attached and stream new lines as they happen (Ctrl-C stops)
telnyx-edge actors logs ChatRoom --tail

# Delete a type and its instances
telnyx-edge actors delete ChatRoom

Running a project locally:

telnyx-edge dev runs a telnyx.toml project on your machine — your function and its StatefulActors — so you can try changes without shipping. It needs Docker with Compose v2.17 or later.

# Run the project in the current directory; edits reload automatically (Ctrl-C stops watching)
telnyx-edge dev

# Boot and return, for scripts and CI
telnyx-edge dev --no-watch

# Stop it, or stop it and delete the local state
docker compose -f .telnyx/dev/compose.yaml down
docker compose -f .telnyx/dev/compose.yaml down -v
# ...and delete local [storage.sqldb] databases too, for a full reset
rm -rf .telnyx/sqldb

To restart the stack, re-run the same telnyx-edge dev command: it recreates every container and keeps your local state. Restarting the function or an actor container on its own (docker compose restart function-runtime, or docker restart) also works. As in production, each actor type runs in its own container, named actor-rt-<type>-<hash>-runtime; dev prints each name when the stack is ready.

Your function is served at http://127.0.0.1:8787 (--port changes it). The first run downloads the runtime images (about 1 GB).

What works locally: [[actors]], including ctx.storage (key-value and SQL) and alarms, your function's [env_vars] in process.env, and each actor type's actor-scoped env vars ([env_vars.shared] + [env_vars.actor] + that type's [[actors]] env_vars) in that actor's process.env. Local state persists between runs until you down -v. [storage.sqldb] bindings work against local databases kept in .telnyx/sqldb/ — the same ones telnyx-edge storage sqldb execute <BINDING> --local reads and writes — which survive down -v. [[secrets]], [telnyx], [storage.kv], [storage.cloudstorage] and [[ratelimits]] are not available locally yet: dev lists the ones your project declares when it starts, and they are undefined in your code. dev writes its files under .telnyx/; add that to your .gitignore.

In telnyx.toml, environment variables can also use [env_vars.shared], [env_vars.function], [env_vars.actor], and per-actor env_vars tables. Values must be strings. The function resolves shared, flat, then function-specific values; each actor type resolves shared, actor-wide, then per-type values. Unknown scopes fail validation; conflicting flat and function-specific values warn, with the function-specific value winning. Local dev applies the resolved function values to the function and each actor type's resolved values to that type's own container, validated as ship validates them. Keep secrets out of these tables.

telnyx-edge inspect <function> shows every binding a function declares — actors, SQL databases, KV namespaces and secrets — alongside its other details. Each row gives the env.<NAME> handle, the kind of binding, what it targets, and its status.

ctx.storage.sql is not the same thing as a SQL database. Both give you SQLite, and the difference is ownership:

ctx.storage.sql storage sqldb
belongs to one actor instance your account
created by existing — activate an instance telnyx-edge storage sqldb create
reached with ctx.storage.sql inside the class env.DB from any bound function
shared between functions no yes
CLI access none execute, migrations

Use ctx.storage.sql when the data belongs to one entity and nothing else should touch it. Use a SQL database when several functions query the same data.


📦 Installation

Choose one of the following methods:

Binary Download (Recommended)

# Linux x64 (example)
VERSION=$(curl -s https://api.github.com/repos/team-telnyx/edge-compute/releases/latest | grep '"tag_name"' | cut -d'"' -f4)
if [ -z "$VERSION" ]; then
  echo "Error: Could not fetch latest version. Please check your internet connection or specify a version manually."
  echo "Example: VERSION=v0.0.3"
  exit 1
fi
curl -sSL https://github.com/team-telnyx/edge-compute/releases/download/$VERSION/telnyx-edge-$VERSION-linux-amd64.tar.gz | tar -xzf -
sudo mv telnyx-edge-$VERSION-linux-amd64/telnyx-edge /usr/local/bin/telnyx-edge
chmod +x /usr/local/bin/telnyx-edge

Other platforms: Replace linux-amd64 in the URL above with:

  • macos-amd64 (macOS Intel)
  • macos-arm64 (macOS Apple Silicon)
  • linux-arm64 (Linux ARM64)
  • windows-amd64.zip (Windows)

⚙️ Configuration

The CLI stores configuration in:

  • ~/.telnyx-edge/config.toml

Configuration is managed automatically by the CLI. Run telnyx-edge auth login to authenticate and configure.

Example config.toml (auto-generated):

api_endpoint = "https://api.telnyx.com"

[oauth]
client_id = "your-client-id"

[tokens]
access_token = "your-access-token"
refresh_token = "your-refresh-token"

🔔 Staying up to date

The CLI checks once a day for a newer release and prints an upgrade notice to stderr if you're behind. It never blocks your command. Disable with TELNYX_NO_UPDATE_CHECK=1.

To upgrade, run telnyx-edge update. It downloads the latest release for your platform, verifies it against the release's published SHA-256 checksums (telnyx-edge-<version>-SHA256SUMS; an older release without one installs with a warning), checks that the new binary runs, and replaces the current one. It asks for confirmation first; pass --yes to skip the prompt (for scripts and CI). If the CLI is installed somewhere your user cannot write, such as /usr/local/bin, re-run it with sudo or install a release manually.


📖 Documentation


🆘 Support