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). Hyphen placement is not constrained — a leading hyphen is accepted — but avoid one: that name then reads as a flag when passed to CLI commands, so you are left addressing the database by id. Keep names short: past 255 characters the create fails with a 500 instead of a validation error.
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 exactly one key, sql. Any other key — including params — is rejected with 400 Invalid request body. The response carries results and nothing else: no meta, no row counts, no duration. A script may hold several ;-separated statements, and the rows returned are the last statement’s.
This endpoint has no parameter binding. There is no way to send values separately from the statement, so any value that varies has to be written into the SQL text itself. Never interpolate a string that came from a request, a user, or any other untrusted source into a statement sent this way — that is a SQL injection, and the endpoint gives you nothing to defend with.Use this path for schema, migrations, backfills, and inspection, where the SQL is written by you and fixed in advance. For request-path queries whose values come from callers, use a TypeScript function and the env binding, where prepare().bind() keeps the statement and the data separate.
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 embeds the SQLite message inside the same longer transport string described in step 5, 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