Skip to main content

Prerequisites

  • The telnyx-edge CLI, authenticated (telnyx-edge auth api-key set <YOUR_API_KEY>), on a release that includes storage sqldb.
  • A Telnyx API key, if calling the REST API directly.
  • An Edge Compute function project with a telnyx.toml (or func.toml) manifest, and Node.js for npm.

1. Create a Database

A database is an isolated SQLite database with its own schema. Create one with the CLI or the API.
The response carries the database id — a UUID, and the only durable handle. Names are not resolution keys:
A name may contain lowercase letters, numbers, and hyphens, and must be unique within the organization — a duplicate returns 409, and the API rejects an empty or upper-case name with 422 (the CLI stops an empty name before any request is made). It must also start and end on a letter or number: hyphens go in between, so my-db and a--b are fine but -tenant and tenant- are 422. That rule exists partly because a leading hyphen reads as a flag when passed to CLI commands, leaving you addressing the database by id. Names are capped at 255 characters; past that the create returns 422 naming the length, like every other name failure.
A new database starts at status: "pending". SQL sent to it from outside a function — execute, migrations, or the REST query endpoint — returns 409 ("Database is not ready (status: pending)") until provisioning finishes, typically in 2 to 11 seconds. If scripting, poll until the status is provision_ok rather than sleeping a fixed interval:

2. Create the Schema

Create your tables from the terminal, before any code exists. execute takes exactly one of --command or --file, and --remote is required — there is no local emulation.
Longer schemas belong in a file. The file is submitted as one multi-statement script — the engine splits it, the CLI does not:
Add --json to get the raw result object instead of the rendered table. For scripting note one wrinkle: the current CLI writes the JSON to stderr, so capture it with 2>&1 >/dev/null rather than piping stdout.
execute is for one-off statements and inspection. For schema changes intended to be kept and replayed on another database, use Migrations — numbered .sql files with a tracking table inside the database itself.

3. Bind the Database to a Function

Declare the database in the function’s manifest. The manifest itself — and the [edge_compute] identity in it — comes from the function, not from this guide: telnyx-edge new-func registers the function and writes func_id when it scaffolds a project. Starting from nothing, run telnyx-edge new-func -n links-api -l ts first and work in the project it creates (it writes a classic func.toml; for the umbrella telnyx.toml form shown here, copy the generated [edge_compute] block across). The only part this step adds is the [storage.sqldb.DB] block. The example below is a telnyx.toml; the [storage.sqldb.<NAME>] block parses the same way in a func.toml project, though a classic func.toml project’s entry point has a different shape from the export default { fetch } handler in step 5:
The block key — DB here — is a name of your choosing, and it is what the code sees on env: this manifest produces env.DB. It must be a valid identifier (letters, digits, underscores; no leading digit, no __) and unique across every binding in the manifest. id must be the database UUID. Binding by name, or creating a database from the manifest, is rejected before deploy. Declare as many [storage.sqldb.<NAME>] blocks as needed; two bindings that carry different ids are two different databases, and neither can see the other’s tables.

4. Generate Types

telnyx-edge types reads the manifest — plus the @telnyx/edge-runtime version declared in package.json — and nothing else: it runs offline and needs no authentication. Re-run it after adding, renaming, or removing a binding. Types are for the compiler only. The binding itself resolves at runtime from the manifest, so a stale telnyx-env.d.ts does not change what the deployed function sees — in either direction.

5. Write the Function

Bindings live on the env imported from @telnyx/edge-runtimenot on the second argument to fetch(req, env). Reading env.DB off that argument type-checks cleanly and is undefined at runtime.
prepare() is synchronous and returns a statement. .bind() returns a new statement with the values attached — it never mutates the one it was called on. The terminal calls are async, and each resolves to a different shape: run() and all() give { results, success, meta }, first() gives a single row or null (first("col") gives that column’s value), and raw() gives an array of value-arrays. run() and all() are the same call: both return rows, including the rows a RETURNING clause produces. Bind every value that comes from a request with ? placeholders rather than building SQL by concatenation. Ordered ?NNN placeholders work too. A failing statement — a syntax error, a UNIQUE violation, a missing table — rejects with a plain Error. There is no typed error class or error-code taxonomy: the SQLite message is nested inside a longer transport string on error.message.

6. Ship and Call It

ship bundles the project, uploads it, and prints the function host — {func-name}-{func-id-prefix}.telnyxcompute.com, without a scheme; add https:// when calling it.

7. Query It Without Deploying

The rows the function wrote are readable from the terminal against the same database:
The CLI renders the rows as a table followed by a row count. This is the same database the function holds on env.DB, reached through the same primary — the CLI is not a second writer, and no schema or data is duplicated. Use it to inspect state, apply a fix-up statement, or seed a table without redeploying.

8. Query It From Any Language

The CLI is a wrapper over one HTTP endpoint. Anything that can make a request can reach the same database — a Go or Python function, a backfill script, a CI job:
The body takes sql, plus an optional params array of values to bind to its ? placeholders:
Each value is a string, number, boolean, or null — booleans bind as 1/0. The count must match the placeholders exactly: supply too few or too many and the call is a 422 naming both counts, rather than binding null for the ones you left out. Nested arrays and objects have no single-value binding and are 422 with a /params/{index} pointer, as is an integer too large to survive the JSON boundary exactly (pass it as a string). Omitting params entirely is still valid — that is the raw-SQL form above. A script may hold several ;-separated statements; the rows returned are the last statement’s, and params binds to that statement.
Bind values instead of interpolating them. Any value that came from a request, a user, or another untrusted source belongs in params, never concatenated into the sql string — that is a SQL injection, and binding is what defends against it.The same applies wherever you reach the database: prepare().bind() from a function through the env binding, params over REST, and --param / --param-json from sqldb execute.
Binary columns come back as JSON arrays of byte values ([9, 8, 7]), not base64. Errors arrive in the standard Telnyx envelope: a SQL failure is a 422 whose detail is the database’s own message (no such table: links, near "SELEC": syntax error), a database that has not finished provisioning is a 409, and an unknown or other-organization id is a 404.

Next Steps

  • How SQL Databases Work — the shared-database model, consistency, and what env.DB is talking to
  • Migrations — numbered .sql files and the in-database tracking table
  • Runtime API — the full SqlDatabase surface, including batch() and exec()
  • CLI Commandscreate, list, get, delete, execute, migrations
  • Limits — database and value size, bound parameters, and query performance
  • Bindings — how the env binding surface works across storage products