> ## Documentation Index
> Fetch the complete documentation index at: https://developers.telnyx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Quick Start

> Create a SQL database, apply a schema, bind it as env.DB in a TypeScript function, ship, then read the same data back from the CLI and the REST API.

## Prerequisites

* The `telnyx-edge` [CLI](/docs/edge-compute/reference/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.

<Tabs>
  <Tab title="CLI">
    ```bash theme={null}
    telnyx-edge storage sqldb create --name my-app-db
    ```
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    curl -X POST https://api.telnyx.com/v2/storage/sqldbs \
      -H "Authorization: Bearer $TELNYX_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"name": "my-app-db"}'
    ```
  </Tab>
</Tabs>

The response carries the database `id` — a UUID, and the only durable handle. Names are not resolution keys:

```json theme={null}
{
  "data": {
    "record_type": "storage_sqldb",
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "my-app-db",
    "status": "pending",
    "created_at": "2026-07-29T14:52:03Z",
    "updated_at": "2026-07-29T14:52:03Z"
  }
}
```

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.

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

  ```bash theme={null}
  telnyx-edge storage sqldb get 550e8400-e29b-41d4-a716-446655440000
  # or
  curl https://api.telnyx.com/v2/storage/sqldbs/550e8400-e29b-41d4-a716-446655440000 \
    -H "Authorization: Bearer $TELNYX_API_KEY"
  ```
</Note>

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

```bash theme={null}
telnyx-edge storage sqldb execute 550e8400-e29b-41d4-a716-446655440000 --remote \
  --command "CREATE TABLE links (
    id INTEGER PRIMARY KEY,
    slug TEXT NOT NULL UNIQUE,
    target TEXT NOT NULL,
    created_at TEXT NOT NULL DEFAULT (datetime('now'))
  )"
# ✓ Statement executed — no rows returned
```

Longer schemas belong in a file. The file is submitted as one multi-statement script — the engine splits it, the CLI does not:

```bash theme={null}
telnyx-edge storage sqldb execute 550e8400-e29b-41d4-a716-446655440000 --remote --file schema.sql
```

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.

<Note>
  `execute` is for one-off statements and inspection. For schema changes intended to be kept and replayed on another database, use [Migrations](/docs/edge-compute/sqldb/migrations) — numbered `.sql` files with a tracking table inside the database itself.
</Note>

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

```toml theme={null}
name = "links-api"
main = "src/index.ts"
compatibility_date = "2026-05-14"

[storage.sqldb.DB]
id = "550e8400-e29b-41d4-a716-446655440000"  # Database ID from step 1

[edge_compute]
func_id = "<written by new-func — do not edit>"
func_name = "links-api"
```

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

```bash theme={null}
npm install @telnyx/edge-runtime   # 0.9.0 or newer — the first release exporting SqlDatabase
telnyx-edge types                  # writes telnyx-env.d.ts
# env.DB → SqlDatabase
```

`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

<Warning>
  Bindings live on the `env` **imported** from `@telnyx/edge-runtime` — **not** on the second argument to `fetch(req, env)`. Reading `env.DB` off that argument type-checks cleanly and is `undefined` at runtime.
</Warning>

```ts theme={null}
import { env } from "@telnyx/edge-runtime";

type Link = { id: number; slug: string; target: string };

export default {
  async fetch(req: Request): Promise<Response> {
    if (req.method === "POST") {
      const { slug, target } = (await req.json()) as { slug: string; target: string };

      // RETURNING hands back the generated id
      const inserted = await env.DB.prepare(
        `INSERT INTO links (slug, target) VALUES (?, ?) RETURNING id`,
      )
        .bind(slug, target)
        .run<{ id: number }>();

      return Response.json({ id: inserted.results[0]!.id }, { status: 201 });
    }

    const slug = new URL(req.url).searchParams.get("slug");

    const { results } = slug
      ? await env.DB.prepare(`SELECT id, slug, target FROM links WHERE slug = ?`)
          .bind(slug)
          .all<Link>()
      : await env.DB.prepare(
          `SELECT id, slug, target FROM links ORDER BY id DESC LIMIT 20`,
        ).all<Link>();

    return Response.json({ links: results });
  },
};
```

`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

```bash theme={null}
telnyx-edge ship
```

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

```bash theme={null}
B=https://links-api-<func-id-prefix>.telnyxcompute.com

curl -sS -X POST $B -H 'content-type: application/json' \
  -d '{"slug":"docs","target":"https://developers.telnyx.com"}'
# → {"id":1}

curl -sS $B
# → {"links":[{"id":1,"slug":"docs","target":"https://developers.telnyx.com"}]}

curl -sS "$B?slug=docs"
# → {"links":[{"id":1,"slug":"docs","target":"https://developers.telnyx.com"}]}
```

## 7. Query It Without Deploying

The rows the function wrote are readable from the terminal against the same database:

```bash theme={null}
telnyx-edge storage sqldb execute 550e8400-e29b-41d4-a716-446655440000 --remote \
  --command "SELECT id, slug, target FROM links ORDER BY id DESC LIMIT 5"
```

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:

```bash theme={null}
curl -sS -X POST \
  https://api.telnyx.com/v2/storage/sqldbs/550e8400-e29b-41d4-a716-446655440000/actions/query \
  -H "Authorization: Bearer $TELNYX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"sql": "SELECT id, slug FROM links ORDER BY id DESC LIMIT 5"}'
```

```json theme={null}
{
  "data": {
    "results": [{ "id": 2, "slug": "home" }, { "id": 1, "slug": "docs" }],
    "meta": { "duration": 5.18, "rows_read": 0, "rows_written": 0, "last_row_id": 0, "changes": 0 }
  }
}
```

The body takes `sql`, plus an optional `params` array of values to bind to its `?` placeholders:

```bash theme={null}
curl -sS -X POST \
  https://api.telnyx.com/v2/storage/sqldbs/550e8400-e29b-41d4-a716-446655440000/actions/query \
  -H "Authorization: Bearer $TELNYX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"sql": "SELECT id, slug FROM links WHERE slug = ?", "params": ["home"]}'
```

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.

<Warning>
  **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](#5-write-the-function), `params` over REST, and `--param` / `--param-json` from [`sqldb execute`](/docs/edge-compute/sqldb/cli#binding-values).
</Warning>

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](/docs/edge-compute/sqldb/concepts/how-sqldb-works) — the shared-database model, consistency, and what `env.DB` is talking to
* [Migrations](/docs/edge-compute/sqldb/migrations) — numbered `.sql` files and the in-database tracking table
* [Runtime API](/docs/edge-compute/sqldb/reference) — the full `SqlDatabase` surface, including `batch()` and `exec()`
* [CLI Commands](/docs/edge-compute/sqldb/cli) — `create`, `list`, `get`, `delete`, `execute`, `migrations`
* [Limits](/docs/edge-compute/sqldb/limits) — database and value size, bound parameters, and query performance
* [Bindings](/docs/edge-compute/runtime/bindings) — how the `env` binding surface works across storage products
