wandres.dev
D1 · SQLite en el edge

Crear una base D1 y enlazarla al Worker

El camino de cero a consultar: crear la base con wrangler d1 create, leer el identificador que devuelve, declarar el binding en wrangler.jsonc y aplicar el esquema inicial con un archivo SQL. Por qué el binding sustituye a la cadena de conexión de siempre y cómo las migraciones convierten el esquema en algo versionado y reproducible.

⏱ 14 min

Tener claro qué es D1 no sirve de nada hasta que una base existe y tu Worker puede hablarle. El camino es corto y tiene tres pasos que conviene no confundir: crear la base en tu cuenta, declararla como binding para que aparezca en env, y darle una forma con un esquema. Al final de esta lección env.DB dejará de ser una promesa y será un objeto vivo contra el que consultar, y habrás visto por qué el modelo de bindings elimina de un golpe las cadenas de conexión y los secretos que arrastraba la base de datos tradicional.

🎯 Al terminar esta lección sabrás
  • Crear una base con wrangler d1 create y leer lo que devuelve.
  • Declarar el binding en wrangler.jsonc con su nombre e identificador.
  • Aplicar el esquema inicial desde un archivo .sql.
  • Entender por qué el binding sustituye a la cadena de conexión.

Crear la base con Wrangler

Una base D1 se crea una sola vez y vive en tu cuenta. El comando es directo: le das un nombre legible y Wrangler aprovisiona la base, le asigna un identificador único y te imprime el fragmento de configuración listo para pegar.

# crea la base y devuelve el fragmento de binding
wrangler d1 create mi-app

La salida contiene los dos datos que importan: el database_name que tú elegiste y el database_id, un UUID que es el identificador real de la base en la plataforma.

✅ Successfully created DB 'mi-app'

[[d1_databases]]
binding = "DB"
database_name = "mi-app"
database_id = "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d"

Wrangler imprime ese bloque en formato TOML por costumbre, pero si tu proyecto usa wrangler.jsonc —lo recomendado hoy— solo tienes que traducir esos mismos campos a JSON. El database_id es lo único que no debes inventar: identifica la base real y es lo que conecta tu configuración con lo que existe en tu cuenta.

Si más adelante pierdes de vista ese identificador, no hace falta recrear nada: Wrangler puede enumerar las bases de tu cuenta con sus nombres e identificadores.

# lista las bases D1 de tu cuenta y sus identificadores
wrangler d1 list

Declarar el binding

Crear la base no basta para que tu Worker la vea: hay que declararla como binding. En wrangler.jsonc, el array d1_databases enumera las bases que tu Worker podrá tocar, cada una con el nombre lógico por el que la leerás en env.

{
  "name": "mi-app",
  "main": "src/index.ts",
  "compatibility_date": "2026-01-01",
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "mi-app",
      "database_id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
    }
  ]
}

Los tres campos cumplen papeles distintos. El binding es el nombre con el que accederás a la base desde el código: al declararlo como DB, la plataforma pondrá un D1Database en env.DB. El database_name es la etiqueta legible para las herramientas y la CLI. El database_id es el enlace real al recurso. Cambia el binding y cambias cómo lo llamas en el código; cambia el database_id y apuntas a otra base.

# regenera los tipos para que env.DB tenga el tipo D1Database
wrangler types
💡
Deja que los tipos describan tu env

Tras declarar el binding, ejecuta wrangler types para regenerar la interfaz Env. A partir de ahí, env.DB no es un any: tu editor sabe que es un D1Database, te autocompleta prepare, batch y compañía, y te avisa si escribes mal un método. El binding declarado en la configuración y el tipo generado son dos caras de la misma verdad, y mantenerlos sincronizados es lo que hace que trabajar con D1 se sienta tipado de extremo a extremo.

Aplicar el esquema inicial

Una base recién creada está vacía: no tiene tablas. El esquema —las sentencias CREATE TABLE e índices que definen tu modelo— vive en un archivo .sql versionado junto al código, no en comandos sueltos que nadie recuerda.

CREATE TABLE IF NOT EXISTS usuarios (
  id         INTEGER PRIMARY KEY,
  email      TEXT NOT NULL UNIQUE,
  creado_en  TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE INDEX IF NOT EXISTS idx_usuarios_email ON usuarios (email);

Ese archivo se aplica con wrangler d1 execute, que ejecuta SQL contra la base. Y aquí aparece la distinción más importante del flujo: --local toca la base de tu máquina y --remote toca la base real de tu cuenta. Son universos separados, y aplicar el esquema en uno no lo aplica en el otro.

# aplica el esquema a la base local de desarrollo
wrangler d1 execute mi-app --local --file=./schema.sql

# aplica el mismo esquema a la base remota real
wrangler d1 execute mi-app --remote --file=./schema.sql

Para algo más que un primer arranque, el camino disciplinado son las migraciones: en vez de reaplicar archivos a mano, generas migraciones numeradas y Wrangler lleva la cuenta de cuáles ya se ejecutaron, de modo que el esquema evoluciona de forma reproducible entre entornos.

# crea una migracion versionada y aplicala
wrangler d1 migrations create mi-app crear_usuarios
wrangler d1 migrations apply mi-app --remote

Aplicado el esquema, conviene confirmar que las tablas existen antes de dar el paso por bueno. Una consulta rápida contra la base correspondiente basta para verlo, y es un buen hábito hacerlo en cada entorno por separado.

# confirma que la tabla existe en la base local
wrangler d1 execute mi-app --local --command "SELECT name FROM sqlite_master WHERE type = 'table'"
flowchart LR
C[wrangler d1 create] --> ID[database id y name]
ID --> B[binding en wrangler jsonc]
B --> S[aplicar esquema con execute]
S --> E[env DB listo para consultar]
style B fill:#89b4fa,color:#11111b
style E fill:#a6e3a1,color:#11111b
⚠️
Local y remoto no comparten datos ni esquema

El error más común al empezar con D1 es aplicar el esquema en --local, verlo funcionar en wrangler dev y desplegar convencido de que la base de producción ya tiene las tablas. No las tiene. La base local vive en tu disco y la remota en tu cuenta: cada CREATE TABLE, cada migración y cada dato hay que llevarlo al entorno donde lo necesites. Antes de desplegar, pregúntate siempre si la base remota ha recibido el mismo esquema que la local.

Por qué el binding y no una cadena de conexión

El binding elimina toda una clase de problemas de la base de datos

Piensa en cómo se conectaba una aplicación a una base de datos hasta ahora: una cadena de conexión con host, puerto, usuario y contraseña, guardada en una variable de entorno, rotada a mano cuando alguien la filtraba, apuntando a un servidor que había que tener encendido, con un pool de conexiones que dimensionar para no agotar los sockets del servidor bajo carga. Cada una de esas piezas es una fuente de fallos y de fugas. El binding de D1 las borra todas a la vez, y esa es su idea profunda. No hay host ni puerto porque no hablas con una máquina en una dirección, sino con un recurso lógico que la plataforma resuelve por ti; no hay usuario ni contraseña porque la autoridad no viaja como un secreto que se pueda robar, sino que se concede al declarar el binding en la configuración del Worker; no hay pool que dimensionar porque no abres conexiones, sino que invocas métodos sobre un objeto que la plataforma ya tiene listo. El database_id no es una credencial: es una referencia, y quien no tenga el binding declarado no alcanza la base aunque conozca el identificador. Ese desplazamiento —de credencial a capacidad, de dirección de red a referencia lógica, de conexión gestionada por ti a recurso gestionado por la plataforma— es el mismo patrón que viste con KV, R2 y todo lo demás, y por eso aprender a enlazar una base D1 no es aprender un procedimiento nuevo, sino aplicar por enésima vez el gesto que vertebra la plataforma entera: declaras lo que tu Worker necesita, y lo recibes ya resuelto en env. La base de datos, ese componente que durante décadas fue el más celoso de sus credenciales y el más frágil en su conexión, se ha vuelto tan simple de alcanzar como cualquier otra capacidad del edge.

⚔️ De cero a env.DB
  1. Crea una base con wrangler d1 create y localiza en la salida el database_id.
  2. Declara el binding en wrangler.jsonc como DB y ejecuta wrangler types; confirma que env.DB es un D1Database.
  3. Escribe un schema.sql con una tabla y un índice, y aplícalo con --local y luego con --remote.
  4. Explica con tus palabras por qué el database_id no es un secreto y qué papel cumple entonces el binding.