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

# How SQL Databases Work

> How Telnyx SQL Databases work: one primary per database, what that means for writes and consistency, and how prepare(), exec(), and batch() differ.

A SQL database is stock SQLite, addressed by UUID, living outside any function. Everything in the model follows from one property: **a database has exactly one primary**, and every caller goes through it.

## A Database Is a Standalone Resource

Create a database with the [CLI](/docs/edge-compute/sqldb/cli) or the REST API and it comes back immediately with status `pending`; it reaches `provision_ok` a few seconds later — typically 2 to 11, since provisioning is picked up by a poll loop rather than done inline. A query issued while it is still `pending` returns HTTP 409 `Database is not ready`. In a script, poll `get` until the status is `provision_ok` rather than sleeping a fixed interval.

The **UUID is the only durable handle**. A `telnyx.toml` binding takes an id, never a name:

```toml theme={null}
[storage.sqldb.DB]
id = "0198c2c5-8f1e-7a3d-9b21-6e4a0d5f1c88"
```

The name is a label, unique within the organization, matching `^[a-z0-9-]+$`. It exists so humans can find a database in a list; the CLI resolves a name to an id locally as a convenience, while REST calls and manifests take the id. There is no rename, and deleting a database frees its name for a new, empty database with a different id.

Nothing is deployed per database, and nothing about a database is derived from a function. Shipping, rolling back, or deleting a function does not touch the data.

The engine is stock SQLite — 3.51.3 in production today — so the dialect and the built-in functions are SQLite's, not a subset invented for the edge. Foreign keys are on and enforced, which is worth knowing if you are arriving from a SQLite build that leaves them off: a `REFERENCES` violation fails the statement.

## One Primary, Serialized Writes

Today every read and every write for a given database funnels through a single primary, so a caller never contends with a second writer: not another region, not a replica, not a second connection opened by a different caller. That is the current topology rather than a promise about future ones; what you can design against is that statements against one database are serialized, and that every access path sees the same data.

Two consequences, both worth designing around:

* **Writes are serialized.** Concurrent writers queue behind one another instead of contending for the database. Lost-update anomalies are still possible at the *application* level — two callers that each read, compute, and write can still clobber each other — which is what [`batch()`](/docs/edge-compute/sqldb/reference/sql-database#batch-statements) exists for.
* **Statements run one at a time.** The database is single-threaded. A slow statement occupies it and other callers wait behind it. That is the cost of the property above.

Keep statements bounded. A simple `SELECT 1` round-trips in about 10 ms warm and a 5,000-row read in about 160 ms, so the cost that matters is the one a slow statement imposes on everything queued behind it. Long analytical scans belong in a warehouse, not here — see [Limits](/docs/edge-compute/sqldb/limits).

Indexes matter for the same reason they matter in any SQLite deployment, and more so because the cost is paid by every other caller:

```sql theme={null}
CREATE INDEX IF NOT EXISTS links_by_slug ON links(slug);
```

## Two Access Paths, One Database

A database is reachable from inside a function through its binding, and from outside through REST or the CLI. These are not two systems kept in sync — both run against the same primary.

|                 | `env` binding                                | REST API and CLI                                                                                             |
| --------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| **Where**       | Inside a TypeScript edge function            | Anywhere; no function needs to exist                                                                         |
| **Auth**        | Injected by the runtime                      | `TELNYX_API_KEY` bearer token                                                                                |
| **Entry point** | `env.DB.prepare(sql)`, `.batch()`, `.exec()` | `POST /v2/storage/sqldbs/{id}/actions/query`, `telnyx-edge storage sqldb execute`                            |
| **Typical use** | Reads and writes on the request path         | Schema, migrations, backfills, inspection — SQL you wrote yourself, since this path has no parameter binding |

Visibility between them is immediate and needs no coordination. A row inserted by a function is returned by the very next `sqldb execute`; a table created by `sqldb execute` is visible to the next request that runs. That is what makes schema management work before any code exists: create the database, apply [migrations](/docs/edge-compute/sqldb/migrations) from a terminal, then ship a function that binds it.

<Note>
  Inside a function the binding is read off the `env` **imported** from `@telnyx/edge-runtime`, not off the second argument to `fetch(request, env)`. That argument does not carry storage bindings, so `env.DB` read from it is `undefined` at runtime even though it type-checks. Requires `@telnyx/edge-runtime` **0.9.0 or later**.
</Note>

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

export default {
  async fetch(request: Request): Promise<Response> {
    const { results } = await env.DB
      .prepare("SELECT url FROM links WHERE slug = ?")
      .bind(new URL(request.url).pathname.slice(1))
      .all<{ url: string }>();

    return results.length
      ? Response.redirect(results[0].url, 302)
      : new Response("not found", { status: 404 });
  },
};
```

## Sharing Across Functions

A database is not owned by the function that first wrote to it. Any number of functions can declare a binding to the same id, and each one sees the same tables and the same rows.

The **binding name is a local alias**, declared per function. One function can call the database `DB` and another can call the same id `SHARED`; both statements land in the same place.

```toml theme={null}
# links-api/telnyx.toml
[storage.sqldb.DB]
id = "0198c2c5-8f1e-7a3d-9b21-6e4a0d5f1c88"
```

```toml theme={null}
# analytics-worker/telnyx.toml
[storage.sqldb.SHARED]
id = "0198c2c5-8f1e-7a3d-9b21-6e4a0d5f1c88"
```

Because the alias is local, nothing in the schema records which function created a table. Coordination between functions is ordinary database work: agree on a schema, own it in migrations, and use `batch()` where a sequence has to be atomic.

The id is validated when a function ships. An id that does not exist, belongs to another organization, or is not yet `provision_ok` fails the deploy rather than producing a function with a dead binding — though the failure currently surfaces as a generic `API error (HTTP 500)` that never names the binding, so if a ship starts failing right after a `[storage.sqldb]` edit, re-check the id and the database's status before believing the retry-later advice. The check reads the database record rather than opening a connection, so it is not a promise that the first query will succeed — only that the id resolves to a database this organization owns.

## Isolation

Isolation runs along two lines, both enforced by the platform rather than by convention.

* **Database to database.** Two bindings in the same function are two separate databases. `env.DB` cannot see `env.DB2`'s tables and `env.DB2` cannot see `env.DB`'s — each direction fails with `no such table`. There is no cross-database query: each statement addresses exactly one id, and one database cannot be joined to another.
* **Organization to organization.** A database belongs to the organization that created it. An id from another organization returns `404` — the same response as an id that never existed, so existence cannot be probed. A function can only ever reach its own organization's databases.

## Consistency and Durability

Because there is one primary and no replicas, consistency is the simple kind:

* **A write that resolves has committed.** When `all()`, `run()`, `batch()`, or `exec()` resolves without throwing, the transaction has committed on the primary and every later reader sees it.
* **Reads reflect prior writes.** Both access paths reach the same primary, so there is no replica lag to design around and no eventual-consistency window between the function path and the REST path.
* **Data outlives functions.** Rows written before a deploy are there after it — across redeploys of the functions that bind the database, across CLI and REST sessions, and across the creation of other databases in the same organization.

Deletion is asynchronous: for a few seconds the record reports `status: deleting` — queries return `409` and the name is still held — and then the id turns `404` and the name frees. Re-creating the same name after that produces a new, empty database under a new id, so poll `get` until the `404` before re-creating. Keep the schema in version-controlled [migration files](/docs/edge-compute/sqldb/migrations).

## Choosing Between SQL, KV, and Per-Actor SQLite

Three storage surfaces, three different scopes. They can be used together in one application.

|                       | SQL Database                                         | [KV](/docs/edge-compute/kv)            | [Per-actor SQLite](/docs/edge-compute/stateful-actors/guides/storage/sql) |
| --------------------- | ---------------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------- |
| **Data model**        | Relational tables, full SQL                          | Key to opaque bytes                    | Relational tables, full SQL                                               |
| **Scope**             | One dataset shared by every function bound to its id | One namespace in a single global store | One private database per actor instance                                   |
| **Reached from**      | Any bound function, the CLI, the REST API            | Any function, the CLI, the REST API    | Only inside that one actor instance                                       |
| **Call shape**        | `await env.DB.prepare(...).all()`                    | `await env.KV.get(key)`                | `ctx.storage.sql.exec(...)` — synchronous                                 |
| **Concurrency**       | One primary; statements serialized                   | Last-write-wins per key                | The actor instance is the lock                                            |
| **Reach for it when** | Data is relational and more than one caller reads it | Lookups are by key and reads dominate  | State belongs to one entity and nothing else reads it                     |

The distinction that catches people is the last column. Per-actor SQLite is the same engine but a *private* database per instance — it cannot be queried from the CLI, cannot be joined across instances, and has no REST path. A SQL database is the opposite: queryable from anywhere, but no caller ever holds it exclusively.

## Related Resources

* [Quick Start](/docs/edge-compute/sqldb/quick-start) — Create a database, apply a schema, bind it, query it
* [Runtime API](/docs/edge-compute/sqldb/reference) — The `SqlDatabase` binding surface method by method
* [Type Conversion](/docs/edge-compute/sqldb/reference/sql-database#type-conversion) — What `bind()` accepts and what queries hand back
* [Migrations](/docs/edge-compute/sqldb/migrations) — Versioned schema changes
* [Limits](/docs/edge-compute/sqldb/limits) — Size and duration ceilings, and known gaps
* [Bindings](/docs/edge-compute/runtime/bindings) — How `env` bindings resolve
* [Stateful Actor SQL](/docs/edge-compute/stateful-actors/guides/storage/sql) — The private, per-instance SQLite database
