Capítulo 13¶
Cloudflare D1¶
Bases de datos SQL distribuidas para aplicaciones modernas¶
Objetivo¶
Este capítulo tiene un propósito muy concreto: que el lector comprenda qué es Cloudflare D1, cómo funciona una base de datos relacional, de qué manera se integra con Cloudflare Workers y en qué tipo de proyectos conviene utilizarla. Cloudflare define D1 como una base de datos gestionada y serverless con semántica SQL de SQLite, recuperación ante desastres integrada y acceso desde Workers y por API HTTP.[cite:67][cite:78]
Al finalizar, el lector será capaz de diseñar una base de datos relacional profesional, escribir consultas SQL reales, aplicar buenas prácticas de modelado y conectar esa base de datos con aplicaciones desplegadas en la red global de Cloudflare.[cite:67][cite:64]
¿Qué aprenderás?¶
- Qué es Cloudflare D1 y por qué existe.[cite:67]
- Qué significa que sea una base de datos SQL serverless basada en SQLite.[cite:67]
- Cómo funciona una base de datos relacional: tablas, filas, columnas, claves, relaciones e índices.[cite:65][cite:71]
- Cómo crear una base de datos, administrarla y consultarla desde Workers.[cite:78][cite:73]
- Cómo diseñar un modelo relacional completo para una plataforma educativa.[cite:67][cite:78]
- Cuándo D1 es una gran opción y cuándo otro motor puede ser más conveniente.[cite:64][cite:67]
Introducción¶
Imagina una escuela que guarda toda su información en hojas sueltas. En una hoja aparecen los alumnos. En otra, los docentes. En otra, los cursos. En otra más, las credenciales digitales. Si una persona cambia de correo, hay que corregirlo en muchas hojas. Si un alumno se da de baja, todavía puede aparecer inscrito en cursos viejos. Si alguien escribe mal un nombre, el error se propaga a todo el sistema.
Una base de datos relacional nace precisamente para evitar ese caos. Su trabajo no consiste solo en “guardar datos”, sino en organizarlos con reglas claras, relaciones consistentes y consultas capaces de responder preguntas reales: qué alumnos pertenecen a qué escuela, qué docente imparte qué curso, quién generó una credencial y cuándo se registró un acceso.
Cloudflare D1 lleva ese modelo clásico al mundo de la infraestructura moderna. En lugar de obligarte a administrar un servidor de base de datos, instalar un motor, abrir puertos, configurar réplicas y programar respaldos manuales, D1 ofrece una base SQL gestionada que vive dentro del ecosistema de Cloudflare y se integra de forma natural con Workers y Pages.[cite:67]
Si en capítulos anteriores se explicó cómo funcionan el edge, Workers y R2, D1 aparece ahora como la pieza que faltaba para completar muchas arquitecturas reales. Workers procesa lógica, R2 almacena archivos y D1 organiza datos estructurados: usuarios, permisos, cursos, inscripciones, bitácoras y relaciones entre entidades.[cite:67][cite:78]
¿Qué es Cloudflare D1?¶
Cloudflare D1 es la propuesta de Cloudflare para construir bases de datos SQL serverless sobre su plataforma de desarrollo. En la documentación oficial se presenta como una base de datos gestionada con semántica SQL de SQLite, recuperación integrada y acceso desde Workers y Pages.[cite:67]
Historia¶
D1 surge como respuesta a una necesidad muy evidente dentro del ecosistema serverless: muchas aplicaciones modernas necesitan una base relacional, pero administrar MySQL o PostgreSQL para proyectos pequeños o medianos añade complejidad operativa innecesaria. D1 intenta reducir esa fricción y acercar SQL al mismo modelo operativo simple que ya popularizaron Workers y otros servicios administrados.[cite:67]
La idea no es reemplazar todos los motores de base de datos del mundo, sino ofrecer una opción integrada, sencilla y cercana al runtime donde vive la aplicación. Por eso D1 se conecta de manera directa con Workers mediante bindings de entorno y una API específica para preparar y ejecutar consultas.[cite:78]
Filosofía¶
La filosofía de D1 puede resumirse en cuatro ideas:
- SQL debe seguir siendo accesible para proyectos modernos.[cite:67]
- La infraestructura debe administrarse lo menos posible.[cite:67]
- El aislamiento por base de datos puede ser útil para escalar por tenant, usuario o entidad.[cite:67][cite:64]
- El costo debe depender principalmente de almacenamiento y consultas, no de mantener servidores encendidos.[cite:67][cite:77]
Cloudflare afirma que D1 permite crear miles de bases de datos sin costo extra por aislamiento, algo especialmente útil para arquitecturas multi-tenant o sistemas donde cada cliente, escuela o institución puede tener su propio espacio lógico.[cite:67][cite:64]
SQLite distribuido¶
D1 utiliza la semántica SQL de SQLite. Eso no significa simplemente “usar un archivo .sqlite” como en una laptop. Significa aprovechar un motor conocido, pequeño y confiable, adaptado a una plataforma gestionada y distribuida donde Cloudflare controla la operación, la recuperación y ciertas capacidades de replicación de lectura.[cite:67]
La documentación oficial destaca dos capacidades especialmente importantes: Time Travel, que permite restaurar la base a un punto en el tiempo, y Global Read Replication, que permite añadir réplicas de solo lectura más cerca de los clientes para reducir latencia en consultas de lectura.[cite:67]
Serverless SQL¶
Cuando Cloudflare habla de D1 como “serverless”, la idea central es que el desarrollador no administra una máquina ni un proceso de base de datos. No hay que instalar paquetes, definir tamaños de instancia, configurar un sistema operativo o programar backups manuales. La aplicación simplemente se conecta a la base a través de bindings y emite consultas SQL.[cite:67][cite:78]
En el modelo de precios descrito por Cloudflare, D1 cobra por almacenamiento y consultas, y no por capacidad computacional reservada de la base. Si no hay consultas, no se factura cómputo propio de D1, aunque Workers sigue teniendo su propio modelo de costos por ejecución.[cite:77]
Casos de uso¶
D1 encaja especialmente bien en escenarios como estos:
- Aplicaciones CRUD con estructura relacional clara.[cite:67]
- Sistemas multi-tenant con bases por cliente o por institución.[cite:64][cite:67]
- Plataformas educativas con usuarios, cursos, inscripciones y bitácoras.[cite:67]
- APIs en Workers que necesitan SQL sin administrar un servidor aparte.[cite:78]
- Proyectos donde la lectura puede beneficiarse de réplicas cercanas al usuario.[cite:67][cite:78]
¿Qué problema resuelve?¶
Para entender por qué existe D1, conviene mirar primero el modelo tradicional.
Bases de datos tradicionales¶
En un enfoque clásico, montar una base de datos relacional implica elegir un motor como MySQL o PostgreSQL, provisionar un servidor, configurar almacenamiento, abrir la red correcta, definir usuarios, monitorear recursos, atender actualizaciones y resolver respaldos. Todo eso funciona, y en muchos escenarios sigue siendo la mejor opción, pero también añade fricción operativa.
Para una startup pequeña, un docente que desarrolla software o un desarrollador independiente, esa carga puede sentirse desproporcionada. El trabajo principal debería centrarse en el modelo de datos y la lógica del sistema, no en mantener viva una instancia de base de datos durante meses.
Administración de servidores¶
D1 elimina buena parte de esa administración. Cloudflare lo presenta como una base gestionada con acceso directo desde Workers y Pages.[cite:67] En lugar de conectarse a un host TCP convencional, el código de la aplicación usa un binding del entorno y llama métodos como prepare(), batch() o exec().[cite:78]
La diferencia conceptual es importante. En un servidor tradicional, la base es una pieza de infraestructura que tú operas. En D1, la base es un recurso administrado de plataforma.
Escalabilidad¶
Cloudflare indica que D1 está diseñado para escalar horizontalmente a través de múltiples bases más pequeñas, con un límite por base de 10 GB en planes pagados, y con la posibilidad de tener miles o incluso millones de bases por cuenta bajo ciertos acuerdos de límite ampliado.[cite:64]
Eso cambia la forma de pensar la escala. En vez de imaginar una sola base gigantesca para todos los clientes, puedes diseñar una arquitectura por escuela, por campus, por organización o por tenant. Ese enfoque puede simplificar aislamiento, migraciones, respaldo y control de impacto.[cite:64][cite:67]
Alta disponibilidad y recuperación¶
D1 incorpora Time Travel para recuperación a un punto en el tiempo dentro de una ventana determinada y ofrece réplicas de lectura para mejorar latencia y throughput en lecturas.[cite:67][cite:64] Esto no elimina toda decisión arquitectónica, pero reduce el trabajo manual necesario para alcanzar un nivel razonable de resiliencia.
Comparación rápida¶
| Aspecto | Base tradicional | Cloudflare D1 |
|---|---|---|
| Operación | Requiere administrar servidor, red y backups | Servicio gestionado por Cloudflare.[cite:67] |
| Modelo de acceso | Conexión a motor externo | Binding desde Workers/Pages.[cite:67][cite:78] |
| Escalado | Vertical y/o clúster administrado | Escala horizontal con múltiples bases pequeñas.[cite:64] |
| Recuperación | Debe configurarse explícitamente | Time Travel integrado.[cite:67] |
| Lecturas globales | Requiere réplica propia | Réplicas de lectura disponibles.[cite:67] |
Arquitectura de D1¶
El flujo básico de D1 puede representarse así:
Ese diagrama parece simple, pero dentro de él ocurre algo muy poderoso: la lógica de aplicación y la capa de acceso a datos viven muy cerca una de otra en el ecosistema de Cloudflare. El Worker recibe la solicitud HTTP, valida datos, ejecuta consultas SQL contra D1, transforma resultados en JSON y responde al cliente.[cite:78]
Flujo completo¶
[ Navegador / App ]
|
| HTTP Request
v
[ Cloudflare Worker ]
|
| prepare(), bind(), first(), all(), batch()
v
[ D1 Database ]
|
| resultado SQL
v
[ Cloudflare Worker ]
|
| JSON / HTML / redirect / error
v
[ Cliente ]
Paso a paso, el flujo es este:
- El usuario hace una petición a una URL pública.
- Un Worker recibe la petición.
- El Worker obtiene acceso a D1 mediante
env.DBu otro binding configurado.[cite:78] - El código prepara una consulta SQL.
- Si la consulta recibe variables, estas se enlazan con
bind().[cite:66][cite:78] - El motor ejecuta la consulta y devuelve filas, metadatos o errores.
- El Worker serializa el resultado y lo envía al cliente.
La ventaja de este enfoque es que la API y la base no se sienten como piezas desconectadas. Forman parte de una misma superficie de despliegue.
¿Qué es una base de datos relacional?¶
Una base de datos relacional organiza la información en tablas. Cada tabla representa una entidad: usuarios, escuelas, alumnos, cursos, inscripciones o credenciales. Cada fila representa un registro concreto y cada columna describe una propiedad de ese registro.
Tablas, filas y columnas¶
Si se crea una tabla usuarios, probablemente tendrá columnas como id, nombre, email, rol y created_at. Cada fila corresponderá a una persona específica.
Tabla: usuarios
+----+----------------+----------------------+-----------+
| id | nombre | email | rol |
+----+----------------+----------------------+-----------+
| 1 | Ana López | ana@escuela.edu | docente |
| 2 | Luis Martínez | luis@escuela.edu | alumno |
+----+----------------+----------------------+-----------+
Registros¶
“Registro” es otra forma de llamar a una fila. En muchos contextos, fila y registro se usan como sinónimos.
Relaciones¶
Una base relacional recibe su nombre porque las tablas pueden relacionarse entre sí. Por ejemplo, una inscripción pertenece a un alumno y a un curso. Eso significa que la tabla inscripciones puede guardar alumno_id y curso_id para conectar ambos mundos.
Llave primaria¶
La llave primaria identifica de manera única cada registro. En SQLite y D1 suele usarse un INTEGER PRIMARY KEY, aunque también es posible usar IDs de texto según las necesidades del diseño.
Llave foránea¶
La llave foránea conecta una tabla con otra y ayuda a preservar la integridad de los datos. SQLite documenta el soporte de claves foráneas como un mecanismo para hacer cumplir relaciones entre tablas.[cite:65]
Ejemplo:
CREATE TABLE cursos (
id INTEGER PRIMARY KEY,
nombre TEXT NOT NULL
);
CREATE TABLE alumnos (
id INTEGER PRIMARY KEY,
nombre TEXT NOT NULL
);
CREATE TABLE inscripciones (
id INTEGER PRIMARY KEY,
alumno_id INTEGER NOT NULL,
curso_id INTEGER NOT NULL,
FOREIGN KEY (alumno_id) REFERENCES alumnos(id),
FOREIGN KEY (curso_id) REFERENCES cursos(id)
);
Aquí inscripciones no vive aislada. Depende de que existan un alumno y un curso válidos.
Índices¶
Un índice acelera búsquedas sobre columnas concretas. Si consultas con frecuencia por email, crear un índice sobre esa columna puede reducir mucho el tiempo de respuesta. SQLite recomienda crear índices apropiados, y Cloudflare insiste en ello para mantener consultas rápidas dentro de los límites de D1.[cite:64][cite:71]
Normalización¶
Normalizar significa evitar redundancia innecesaria. Si el nombre de una escuela se repite en miles de filas de alumnos, conviene tener una tabla escuelas y referenciarla por ID. Eso reduce inconsistencias y facilita actualización.
No obstante, normalizar no implica fragmentar todo sin criterio. El buen diseño relacional busca equilibrio entre claridad, integridad y rendimiento.
SQLite¶
Qué es¶
SQLite es un motor SQL muy conocido por su tamaño reducido, portabilidad y fiabilidad. Cloudflare describe D1 como una base con semántica SQL de SQLite.[cite:67] Esto resulta importante porque permite usar un dialecto ampliamente documentado y muy apto para aplicaciones pequeñas y medianas.
Cómo funciona¶
En su forma clásica, SQLite trabaja sobre un archivo de base de datos. Pero en D1 ese comportamiento se abstrae detrás de una plataforma gestionada. El desarrollador no manipula directamente el archivo; interactúa con la base a través de la API de D1 y de comandos de Wrangler.[cite:73][cite:78]
Ventajas¶
- Simplicidad conceptual.
- Sintaxis SQL conocida.
- Excelente para aplicaciones embebidas y escenarios con estructura clara.
- Buen rendimiento cuando el diseño y los índices son correctos.
Limitaciones¶
Cloudflare deja claro que cada base D1 es inherentemente single-threaded y procesa consultas una por una.[cite:64] Eso significa que el throughput de una base depende del tiempo promedio de las consultas. Si cada consulta tarda 1 ms, se pueden alcanzar aproximadamente 1,000 consultas por segundo; si tarda 100 ms, el rendimiento baja de forma drástica.[cite:64]
Además, D1 tiene límites por tamaño, duración de consulta, número de parámetros enlazados y número de consultas por invocación del Worker.[cite:64] No es el tipo de base pensado para cualquier carga imaginable; es una herramienta concreta con fortalezas muy útiles y límites muy reales.
Por qué Cloudflare lo eligió¶
Porque SQLite ofrece un modelo compacto, maduro y bien comprendido, ideal para un servicio SQL serverless que busca ser simple, rápido de adoptar y cercano al runtime de aplicación. La elección también facilita que el desarrollador trabaje con SQL estándar y con un motor sobradamente conocido en la industria.[cite:67][cite:71]
Crear una base de datos¶
Creación¶
Cloudflare proporciona comandos específicos de Wrangler para interactuar con D1.[cite:73] El flujo conceptual suele ser este:
- Crear la base de datos con Wrangler.
- Obtener el identificador de la base.
- Declarar el binding en la configuración del proyecto.
- Aplicar un esquema inicial o migraciones.
- Conectarla al Worker.
Ejemplo representativo del comando de creación:
Configuración¶
Una vez creada, se agrega un binding al archivo de configuración del proyecto para que el Worker pueda acceder a la base mediante env.[cite:78][cite:73]
Ejemplo de configuración conceptual:
name = "plataforma-educativa-api"
main = "src/index.ts"
compatibility_date = "2026-06-28"
[[d1_databases]]
binding = "DB"
database_name = "plataforma-educativa"
database_id = "TU_DATABASE_ID"
Conexión¶
Dentro del Worker, D1 se usa a través del objeto env.
Administración¶
D1 puede administrarse con comandos de Wrangler y con su API desde Workers.[cite:73][cite:78] El flujo sano consiste en tratar el esquema como código, versionar cambios y evitar modificar producción manualmente sin historial.
SQL básico¶
Una gran ventaja de D1 es que permite aprender SQL en un contexto práctico. A continuación aparece una guía compacta con ejemplos funcionales.
CREATE TABLE¶
CREATE TABLE usuarios (
id INTEGER PRIMARY KEY,
nombre TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
rol TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT¶
SELECT¶
UPDATE¶
DELETE¶
ALTER TABLE¶
SQLite soporta alteraciones de esquema con ciertas limitaciones documentadas en su propia referencia SQL.[cite:68]
DROP TABLE¶
WHERE¶
ORDER BY¶
GROUP BY¶
JOIN¶
SELECT a.nombre AS alumno, c.nombre AS curso
FROM inscripciones i
JOIN alumnos a ON a.id = i.alumno_id
JOIN cursos c ON c.id = i.curso_id;
LIMIT y OFFSET¶
COUNT, SUM, AVG, MAX, MIN¶
SELECT COUNT(*) AS total_usuarios FROM usuarios;
SELECT SUM(calificacion) AS suma FROM evaluaciones;
SELECT AVG(calificacion) AS promedio FROM evaluaciones;
SELECT MAX(calificacion) AS maximo FROM evaluaciones;
SELECT MIN(calificacion) AS minimo FROM evaluaciones;
Relaciones¶
Las relaciones son el corazón del modelo relacional.
Uno a uno¶
Se usa cuando un registro tiene exactamente un registro complementario. Ejemplo: un usuario y su perfil extendido.
Uno a muchos¶
Es la relación más común. Una escuela tiene muchos alumnos. Un docente tiene muchos cursos.
Muchos a muchos¶
Un alumno puede estar inscrito en muchos cursos, y un curso puede tener muchos alumnos. Esa relación se resuelve con una tabla intermedia.
Integridad referencial¶
La integridad referencial garantiza que no existan referencias rotas. Si una inscripción apunta a un alumno que no existe, el modelo se vuelve incoherente. Las claves foráneas ayudan precisamente a impedir esos problemas.[cite:65]
Índices¶
Qué son¶
Un índice es una estructura auxiliar que acelera búsquedas y ordenamientos sobre columnas específicas. No reemplaza el diseño de tablas, pero puede transformar el rendimiento de una consulta.
Cómo funcionan¶
Cuando consultas una columna indexada, el motor puede localizar coincidencias sin recorrer toda la tabla. En proyectos reales esto marca una diferencia enorme en endpoints de login, búsqueda por correo, consulta de credenciales, bitácoras y listados filtrados.
Cuándo utilizarlos¶
- En columnas usadas frecuentemente en
WHERE. - En columnas usadas para
JOIN. - En columnas usadas en
ORDER BY. - En claves foráneas del lado hijo.
SQLite documenta que conviene crear índices en columnas de claves foráneas hijas; estas no se indexan automáticamente solo por declarar la restricción.[cite:74]
Ejemplo¶
CREATE INDEX idx_usuarios_email ON usuarios(email);
CREATE INDEX idx_inscripciones_alumno_id ON inscripciones(alumno_id);
CREATE INDEX idx_inscripciones_curso_id ON inscripciones(curso_id);
Optimización¶
Un índice mal pensado también cuesta: consume espacio y puede ralentizar escrituras. La regla profesional no es “indexar todo”, sino indexar lo que el patrón real de consultas necesita.
Migraciones¶
Qué son¶
Una migración es un cambio versionado del esquema de la base: crear tablas, agregar columnas, crear índices o transformar datos. En equipos serios, el esquema nunca debería depender de memoria o cambios manuales improvisados.
Cómo funcionan¶
El enfoque recomendado consiste en guardar archivos SQL versionados y aplicarlos en orden. Cada migración representa un paso pequeño y reversible cuando sea posible.
Ejemplo:
Versionado¶
Versionar migraciones permite responder preguntas muy importantes:
- ¿Qué cambió entre staging y producción?
- ¿En qué momento se creó una tabla?
- ¿Qué despliegue introdujo una columna nueva?
Buenas prácticas¶
- Hacer migraciones pequeñas.
- Probarlas en local antes de producción.
- No mezclar cambios masivos de datos con cambios estructurales si puede evitarse.
- Ejecutar operaciones grandes en lotes, porque D1 tiene límites de duración y Cloudflare recomienda batchear trabajos pesados.[cite:64]
Integración con Workers¶
Aquí D1 se vuelve especialmente interesante, porque no es solo una base SQL: es una base SQL integrada con el runtime de Cloudflare.
Consultas¶
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const users = await env.DB.prepare(
'SELECT id, nombre, email, rol FROM usuarios ORDER BY id DESC LIMIT 20'
).all();
return Response.json(users.results);
}
};
La documentación oficial de D1 explica que prepare() devuelve un objeto de prepared statement y que luego pueden usarse métodos específicos para ejecutar la consulta y obtener resultados.[cite:78][cite:66]
Inserciones¶
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const body = await request.json() as { nombre: string; email: string; rol: string };
const result = await env.DB
.prepare('INSERT INTO usuarios (nombre, email, rol) VALUES (?, ?, ?)')
.bind(body.nombre, body.email, body.rol)
.run();
return Response.json({ ok: true, meta: result.meta }, { status: 201 });
}
};
Actualizaciones¶
Prepared Statements¶
Cloudflare recomienda el uso de prepared statements y el método bind() para enlazar parámetros de manera dinámica.[cite:66][cite:78] Esto no solo mejora claridad y reutilización; también es una defensa fundamental contra SQL Injection.
Batch y transacciones¶
La API batch() permite enviar múltiples sentencias en una sola llamada. Cloudflare documenta que estas sentencias se ejecutan de forma secuencial, no concurrente, y que un fallo puede abortar o revertir la secuencia completa, tratándose como una transacción.[cite:78]
Ejemplo:
const stmt1 = env.DB.prepare(
'INSERT INTO cursos (nombre) VALUES (?)'
).bind('Arquitectura Web Moderna');
const stmt2 = env.DB.prepare(
'INSERT INTO bitacora (evento, detalle) VALUES (?, ?)'
).bind('curso_creado', 'Se creó un curso nuevo');
await env.DB.batch([stmt1, stmt2]);
Manejo de errores¶
export default {
async fetch(request: Request, env: Env): Promise<Response> {
try {
const rows = await env.DB.prepare('SELECT * FROM usuarios').all();
return Response.json({ ok: true, data: rows.results });
} catch (error) {
console.error('D1 error', error);
return Response.json(
{ ok: false, error: 'Database operation failed' },
{ status: 500 }
);
}
}
};
Seguridad¶
Validación¶
La seguridad comienza antes del SQL. Todo dato proveniente del usuario debe validarse: tipo, longitud, formato, obligatoriedad, rango y permisos.
SQL Injection¶
El riesgo clásico de inyección SQL aparece cuando se concatenan cadenas manualmente.
Malo:
const email = new URL(request.url).searchParams.get('email');
const sql = `SELECT * FROM usuarios WHERE email = '${email}'`;
await env.DB.exec(sql);
Mejor:
const email = new URL(request.url).searchParams.get('email');
const row = await env.DB
.prepare('SELECT * FROM usuarios WHERE email = ?')
.bind(email)
.first();
Cloudflare señala que exec() es menos seguro y menos adecuado para uso frecuente; se reserva mejor para mantenimiento o tareas puntuales, mientras que las consultas habituales deben usar statements preparados.[cite:78]
Buenas prácticas¶
- Usar
prepare()ybind()para datos dinámicos.[cite:66][cite:78] - Validar entrada antes de consultar.
- No exponer detalles internos de errores SQL al cliente.
- Limitar permisos a nivel de aplicación.
- Registrar eventos sensibles en bitácora.
Rendimiento¶
Consultas eficientes¶
Cada D1 individual procesa consultas una por una.[cite:64] Eso vuelve crucial escribir SQL eficiente. Una consulta lenta no solo tarda más; también bloquea la capacidad de atender otras operaciones en esa misma base.
Índices y patrones de acceso¶
Cloudflare recomienda usar índices apropiados para mantener consultas rápidas y eficientes.[cite:64] En la práctica, cualquier columna usada en joins, filtros por identidad o búsquedas frecuentes debe revisarse con atención.
Límites importantes¶
Entre los límites documentados por Cloudflare se incluyen:[cite:64]
- Hasta 10 GB por base en plan pagado y 500 MB en free.
- Hasta 50,000 bases por cuenta en Workers Paid y 10 en Free.
- Hasta 1,000 consultas por invocación del Worker en Paid y 50 en Free.
- Duración máxima de consulta SQL de 30 segundos.
- Hasta 100 parámetros enlazados por consulta.
- Aproximadamente 6 conexiones simultáneas a D1 por invocación.
Escalabilidad¶
D1 está pensado para escalar horizontalmente con muchas bases pequeñas.[cite:64] Si un sistema crece por institución, por sede o por cliente, puede ser sensato repartir la carga en múltiples bases y no esperar que una sola absorba todo para siempre.
Comparativa¶
La pregunta correcta no es “qué base es mejor”, sino “qué base encaja mejor con este problema”.
| Opción | Fortalezas | Cuándo conviene |
|---|---|---|
| Cloudflare D1 | SQL gestionado, integración nativa con Workers/Pages, Time Travel, replicas de lectura.[cite:67][cite:78] | APIs serverless, multi-tenant, apps en ecosistema Cloudflare. |
| SQLite | Muy simple, ligero, excelente para entornos locales o embebidos.[cite:71] | Apps locales, prototipos, software embebido. |
| MySQL | Popular, maduro, ampliamente soportado. | Sistemas web tradicionales con operación propia. |
| PostgreSQL | Muy potente, extensible y sólido para cargas complejas. | Plataformas con consultas avanzadas y operación más sofisticada. |
| MariaDB | Familiar para equipos que vienen de MySQL. | Escenarios similares a MySQL. |
| SQL Server | Ecosistema fuerte en organizaciones Microsoft. | Entornos empresariales con stack Microsoft. |
| Firestore | NoSQL documental, flexible para ciertos patrones. | Apps con documentos y sincronización, no necesariamente relaciones complejas. |
| Supabase | PostgreSQL gestionado con ecosistema moderno. | Productos que quieren SQL clásico con capa SaaS. |
| PlanetScale | MySQL serverless/distribuido para alto crecimiento. | SaaS con fuerte necesidad de escalado sobre MySQL. |
Una comparación objetiva obliga a admitir que D1 no sustituye todos estos motores. Su gran ventaja aparece cuando la aplicación ya vive en Cloudflare o cuando el modelo operativo serverless tiene más valor que la flexibilidad extrema de un motor tradicional.
Casos reales¶
Credenciales Digitales¶
Un sistema de credenciales necesita usuarios, instituciones, plantillas, credenciales emitidas, vigencias, revocaciones y logs de consulta. D1 es adecuado porque todo ese dominio es relacional por naturaleza: una credencial pertenece a un usuario, una institución emite muchas credenciales, un acceso puede quedar registrado en una bitácora y un rol controla permisos de administración.[cite:67][cite:78]
Sistema de inscripciones¶
Inscribir alumnos en cursos es un caso clásico de relación muchos a muchos. D1 permite modelarlo con claridad mediante tablas intermedias y consultas SQL muy expresivas.
Sistema escolar¶
Escuelas, grupos, materias, docentes, horarios, evaluaciones y asistencias suelen tener relaciones claras y consultas estructuradas. SQL resulta natural aquí porque el dominio exige consistencia y reportes.
NLink¶
Incluso un acortador de enlaces puede beneficiarse de D1 cuando necesita guardar códigos cortos, URL destino, propietario, expiración, métricas resumidas y reglas de acceso. El esquema es pequeño, pero relacional.
Usuarios, roles y permisos¶
La autorización casi siempre termina siendo relacional. Un usuario puede tener uno o varios roles, y cada rol puede tener varios permisos. D1 encaja muy bien en este tipo de diseño.
Bitácoras y accesos¶
Registrar eventos de acceso, auditoría y operaciones administrativas es un patrón excelente para SQL: quién hizo qué, cuándo, desde dónde y sobre qué entidad.
Diagramas ASCII¶
Arquitectura general¶
+---------+ +-------------------+ +----------------+
| Usuario | ----> | Cloudflare Worker | ----> | Cloudflare D1 |
+---------+ +-------------------+ +----------------+
^ | |
| +------ JSON / HTML -------+
+-------------------------------------------------+
Worker hacia D1¶
Request
|
v
Worker
|
| prepare('SELECT * FROM usuarios WHERE id = ?')
| bind(25)
v
D1
|
| first()
v
Worker
|
v
Response
Modelo relacional sencillo¶
escuelas (1) --------- (N) alumnos
escuelas (1) --------- (N) docentes
cursos (1) --------- (N) inscripciones
alumnos (1) --------- (N) inscripciones
usuarios (1) --------- (N) bitacora
roles (N) --------- (N) usuarios_roles --------- (N) usuarios
Consulta con join¶
[alumnos] -- alumno_id --> [inscripciones] <-- curso_id -- [cursos]
| |
+-------------------- consulta JOIN -----------------+
Flujo completo¶
Cliente
|
| GET /api/alumnos/42/cursos
v
Worker
|
| valida parámetros
| prepara SQL
| ejecuta JOIN en D1
v
D1
|
| devuelve filas
v
Worker
|
| serializa JSON
v
Cliente
Buenas prácticas¶
- Diseñar primero el modelo de datos y después los endpoints.
- Usar nombres de tablas y columnas consistentes.
- Declarar claves primarias y foráneas desde el inicio.[cite:65]
- Crear índices para consultas reales, no por intuición.[cite:64][cite:74]
- Usar statements preparados para toda entrada dinámica.[cite:66][cite:78]
- Mantener migraciones pequeñas y versionadas.
- Procesar cambios masivos en lotes para no rebasar límites.[cite:64]
- Separar datos estructurados en D1 y archivos binarios en R2 cuando corresponda.
Errores comunes¶
- Pensar que una base relacional es solo “un Excel más elegante”.
- Repetir datos por comodidad en lugar de modelar relaciones.
- No crear índices en columnas críticas.
- Concatenar SQL con strings.
- Hacer una sola consulta masiva que intenta modificar millones de filas de una vez, algo que Cloudflare desaconseja por límites de ejecución.[cite:64]
- Esperar que una única base D1 resuelva por sí sola cualquier escala imaginable.
- Confundir D1 con Firestore o con un almacén de documentos.
Curiosidades¶
- Cloudflare indica que D1 permite crear miles de bases de datos sin costo extra por aislamiento, lo que invita a pensar arquitecturas por cliente, por escuela o por tenant de manera más natural que en infraestructuras tradicionales.[cite:67]
- D1 incorpora Time Travel para restauración a cualquier minuto dentro de la ventana soportada por el plan.[cite:67][cite:64]
- Para aprovechar réplicas de lectura hace falta usar la Sessions API; de otro modo, las consultas siguen ejecutándose sobre la primaria.[cite:78]
Resumen¶
Cloudflare D1 es una base de datos SQL gestionada que toma la semántica de SQLite y la integra con el ecosistema serverless de Cloudflare.[cite:67] Su propuesta principal no consiste en competir con todos los motores relacionales en todos los escenarios, sino en simplificar el desarrollo de aplicaciones modernas que necesitan SQL sin administrar servidores.[cite:67][cite:77]
Su verdadero valor aparece cuando se combina con Workers. El Worker recibe una petición, valida, consulta la base con prepared statements, transforma el resultado y responde.[cite:78][cite:66] Si el esquema está bien diseñado, las relaciones son claras y los índices correctos, D1 puede convertirse en una pieza muy poderosa para sistemas educativos, plataformas multi-tenant, APIs y servicios web modernos.[cite:64][cite:67]
Glosario¶
- Base de datos relacional: sistema que organiza información en tablas relacionadas entre sí.
- Tabla: conjunto estructurado de filas y columnas.
- Fila / registro: una entrada individual dentro de una tabla.
- Columna: atributo de una tabla.
- Llave primaria: identificador único de cada registro.
- Llave foránea: referencia a la llave primaria de otra tabla.[cite:65]
- Índice: estructura que acelera consultas sobre columnas concretas.
- Normalización: técnica para reducir redundancia y mejorar consistencia.
- SQLite: motor SQL ligero cuyas semánticas usa D1.[cite:67]
- D1: base de datos SQL serverless gestionada por Cloudflare.[cite:67]
- Prepared Statement: consulta preparada con parámetros enlazables.[cite:66][cite:78]
- bind(): método para asociar valores a una consulta preparada.[cite:66]
- batch(): método para ejecutar varias sentencias secuenciales como transacción.[cite:78]
- Time Travel: capacidad de restaurar a un punto en el tiempo.[cite:67]
- Read Replica: copia de solo lectura para reducir latencia y aumentar throughput en lectura.[cite:67]
- Worker binding: recurso enlazado al entorno del Worker, por ejemplo una base D1.[cite:78]
- JOIN: operación SQL para combinar datos de varias tablas.
- Migración: cambio versionado del esquema de la base.
- Integridad referencial: consistencia entre relaciones de tablas.[cite:65]
- SQL Injection: ataque que intenta alterar consultas mediante entrada manipulada.
20 preguntas de autoevaluación¶
- ¿Qué es Cloudflare D1 y cómo lo define Cloudflare oficialmente?[cite:67]
- ¿Qué significa que D1 use semántica SQL de SQLite?[cite:67]
- ¿Qué diferencia a D1 de una base relacional tradicional autogestionada?
- ¿Qué ventajas ofrece el modelo serverless aplicado a SQL?[cite:67][cite:77]
- ¿Qué papel cumplen Workers en la arquitectura de D1?[cite:78]
- ¿Qué es una tabla en una base de datos relacional?
- ¿Qué diferencia hay entre llave primaria y llave foránea?[cite:65]
- ¿Cómo se modela una relación muchos a muchos?
- ¿Por qué conviene usar índices en columnas usadas para
WHEREoJOIN?[cite:64][cite:74] - ¿Qué problema resuelven las migraciones?
- ¿Qué ventajas tiene
prepare()frente aexec()?[cite:78] - ¿Por qué
bind()ayuda a prevenir SQL Injection?[cite:66] - ¿Qué hace
batch()en la API de D1?[cite:78] - ¿Qué límites importantes impone D1 por base o por invocación?[cite:64]
- ¿Qué significa que cada base D1 sea single-threaded?[cite:64]
- ¿Qué es Time Travel y para qué sirve?[cite:67][cite:64]
- ¿Cómo se relacionan D1 y las réplicas de lectura globales?[cite:67][cite:78]
- ¿En qué tipo de sistema educativo D1 puede ser una opción adecuada?
- ¿Cuándo sería mejor usar PostgreSQL o MySQL en lugar de D1?
- ¿Cómo diseñarías una arquitectura multi-tenant usando varias bases D1?[cite:64][cite:67]
Proyecto práctico¶
A continuación se diseña una base de datos para una plataforma educativa completa. La meta no es solo “tener tablas”, sino construir un modelo coherente para operar escuelas, usuarios, docentes, alumnos, cursos, credenciales y auditoría.
Objetivo del proyecto¶
Construir una base de datos relacional profesional y conectarla con un Worker para exponer operaciones comunes mediante consultas parametrizadas.
Modelo general¶
escuelas
|
+-- docentes
|
+-- alumnos
|
+-- cursos --< inscripciones >-- alumnos
|
+-- credenciales
usuarios --< usuarios_roles >-- roles --< roles_permisos >-- permisos
usuarios ------------------------------------------------------< bitacora
Esquema SQL¶
CREATE TABLE escuelas (
id INTEGER PRIMARY KEY,
nombre TEXT NOT NULL,
clave TEXT NOT NULL UNIQUE,
municipio TEXT,
estado TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE usuarios (
id INTEGER PRIMARY KEY,
escuela_id INTEGER,
nombre TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
activo INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (escuela_id) REFERENCES escuelas(id)
);
CREATE TABLE roles (
id INTEGER PRIMARY KEY,
nombre TEXT NOT NULL UNIQUE,
descripcion TEXT
);
CREATE TABLE permisos (
id INTEGER PRIMARY KEY,
codigo TEXT NOT NULL UNIQUE,
descripcion TEXT
);
CREATE TABLE usuarios_roles (
usuario_id INTEGER NOT NULL,
rol_id INTEGER NOT NULL,
PRIMARY KEY (usuario_id, rol_id),
FOREIGN KEY (usuario_id) REFERENCES usuarios(id),
FOREIGN KEY (rol_id) REFERENCES roles(id)
);
CREATE TABLE roles_permisos (
rol_id INTEGER NOT NULL,
permiso_id INTEGER NOT NULL,
PRIMARY KEY (rol_id, permiso_id),
FOREIGN KEY (rol_id) REFERENCES roles(id),
FOREIGN KEY (permiso_id) REFERENCES permisos(id)
);
CREATE TABLE docentes (
id INTEGER PRIMARY KEY,
escuela_id INTEGER NOT NULL,
usuario_id INTEGER,
nombre TEXT NOT NULL,
numero_empleado TEXT,
especialidad TEXT,
FOREIGN KEY (escuela_id) REFERENCES escuelas(id),
FOREIGN KEY (usuario_id) REFERENCES usuarios(id)
);
CREATE TABLE alumnos (
id INTEGER PRIMARY KEY,
escuela_id INTEGER NOT NULL,
usuario_id INTEGER,
matricula TEXT NOT NULL UNIQUE,
nombre TEXT NOT NULL,
curp TEXT,
grado TEXT,
grupo TEXT,
FOREIGN KEY (escuela_id) REFERENCES escuelas(id),
FOREIGN KEY (usuario_id) REFERENCES usuarios(id)
);
CREATE TABLE cursos (
id INTEGER PRIMARY KEY,
escuela_id INTEGER NOT NULL,
docente_id INTEGER,
nombre TEXT NOT NULL,
descripcion TEXT,
activo INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY (escuela_id) REFERENCES escuelas(id),
FOREIGN KEY (docente_id) REFERENCES docentes(id)
);
CREATE TABLE inscripciones (
id INTEGER PRIMARY KEY,
alumno_id INTEGER NOT NULL,
curso_id INTEGER NOT NULL,
fecha_inscripcion TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
estado TEXT NOT NULL DEFAULT 'activa',
FOREIGN KEY (alumno_id) REFERENCES alumnos(id),
FOREIGN KEY (curso_id) REFERENCES cursos(id)
);
CREATE TABLE credenciales (
id INTEGER PRIMARY KEY,
alumno_id INTEGER NOT NULL,
folio TEXT NOT NULL UNIQUE,
url_qr TEXT,
estatus TEXT NOT NULL DEFAULT 'vigente',
emitida_en TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
vence_en TEXT,
FOREIGN KEY (alumno_id) REFERENCES alumnos(id)
);
CREATE TABLE bitacora (
id INTEGER PRIMARY KEY,
usuario_id INTEGER,
evento TEXT NOT NULL,
entidad TEXT,
entidad_id INTEGER,
detalle TEXT,
ip TEXT,
creado_en TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (usuario_id) REFERENCES usuarios(id)
);
Índices del proyecto¶
CREATE INDEX idx_usuarios_escuela_id ON usuarios(escuela_id);
CREATE INDEX idx_docentes_escuela_id ON docentes(escuela_id);
CREATE INDEX idx_alumnos_escuela_id ON alumnos(escuela_id);
CREATE INDEX idx_cursos_escuela_id ON cursos(escuela_id);
CREATE INDEX idx_cursos_docente_id ON cursos(docente_id);
CREATE INDEX idx_inscripciones_alumno_id ON inscripciones(alumno_id);
CREATE INDEX idx_inscripciones_curso_id ON inscripciones(curso_id);
CREATE INDEX idx_credenciales_alumno_id ON credenciales(alumno_id);
CREATE INDEX idx_bitacora_usuario_id ON bitacora(usuario_id);
CREATE INDEX idx_bitacora_creado_en ON bitacora(creado_en);
Explicación de tablas¶
escuelas: representa cada institución.usuarios: concentra cuentas de acceso al sistema.rolesypermisos: definen autorización.usuarios_rolesyroles_permisos: resuelven relaciones muchos a muchos.docentesyalumnos: contienen perfiles académicos ligados a escuelas.cursos: oferta académica.inscripciones: relación entre alumnos y cursos.credenciales: emisión y control de credenciales digitales.bitacora: auditoría y trazabilidad del sistema.
Consultas reales¶
1. Crear un alumno¶
INSERT INTO alumnos (escuela_id, matricula, nombre, curp, grado, grupo)
VALUES (1, 'A20260001', 'María Hernández', 'HEMM010101MJCXXX01', '3', 'A');
2. Listar cursos de un alumno¶
SELECT c.id, c.nombre, c.descripcion, i.fecha_inscripcion, i.estado
FROM inscripciones i
JOIN cursos c ON c.id = i.curso_id
WHERE i.alumno_id = ?
ORDER BY i.fecha_inscripcion DESC;
3. Contar alumnos por escuela¶
SELECT e.nombre, COUNT(a.id) AS total_alumnos
FROM escuelas e
LEFT JOIN alumnos a ON a.escuela_id = e.id
GROUP BY e.id, e.nombre
ORDER BY total_alumnos DESC;
4. Obtener credencial vigente por alumno¶
SELECT c.folio, c.url_qr, c.emitida_en, c.vence_en
FROM credenciales c
WHERE c.alumno_id = ?
AND c.estatus = 'vigente'
LIMIT 1;
5. Registrar evento en bitácora¶
INSERT INTO bitacora (usuario_id, evento, entidad, entidad_id, detalle, ip)
VALUES (?, 'credencial_consultada', 'credenciales', ?, ?, ?);
6. Listar docentes y cursos asignados¶
SELECT d.nombre AS docente, c.nombre AS curso
FROM docentes d
LEFT JOIN cursos c ON c.docente_id = d.id
WHERE d.escuela_id = ?
ORDER BY d.nombre, c.nombre;
Integración con Workers¶
export interface Env {
DB: D1Database;
}
function json(data: unknown, status = 200): Response {
return new Response(JSON.stringify(data), {
status,
headers: { 'content-type': 'application/json; charset=utf-8' }
});
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
try {
if (request.method === 'GET' && url.pathname.startsWith('/api/alumnos/')) {
const alumnoId = Number(url.pathname.split('/')[3]);
if (!Number.isInteger(alumnoId) || alumnoId <= 0) {
return json({ ok: false, error: 'ID inválido' }, 400);
}
const alumno = await env.DB
.prepare('SELECT id, nombre, matricula, grado, grupo FROM alumnos WHERE id = ?')
.bind(alumnoId)
.first();
if (!alumno) {
return json({ ok: false, error: 'Alumno no encontrado' }, 404);
}
const cursos = await env.DB
.prepare(`
SELECT c.id, c.nombre, i.fecha_inscripcion, i.estado
FROM inscripciones i
JOIN cursos c ON c.id = i.curso_id
WHERE i.alumno_id = ?
ORDER BY i.fecha_inscripcion DESC
`)
.bind(alumnoId)
.all();
return json({ ok: true, alumno, cursos: cursos.results });
}
if (request.method === 'POST' && url.pathname === '/api/alumnos') {
const body = await request.json() as {
escuela_id: number;
matricula: string;
nombre: string;
curp?: string;
grado?: string;
grupo?: string;
};
if (!body.escuela_id || !body.matricula || !body.nombre) {
return json({ ok: false, error: 'Faltan campos obligatorios' }, 400);
}
const result = await env.DB
.prepare(`
INSERT INTO alumnos (escuela_id, matricula, nombre, curp, grado, grupo)
VALUES (?, ?, ?, ?, ?, ?)
`)
.bind(
body.escuela_id,
body.matricula,
body.nombre,
body.curp ?? null,
body.grado ?? null,
body.grupo ?? null
)
.run();
return json({ ok: true, meta: result.meta }, 201);
}
return json({ ok: false, error: 'Ruta no encontrada' }, 404);
} catch (error) {
console.error(error);
return json({ ok: false, error: 'Error interno de base de datos' }, 500);
}
}
};
Cómo escalar con miles de usuarios¶
Aquí conviene pensar con mentalidad de arquitectura, no solo de tablas. Cloudflare explica que D1 está diseñado para escalar horizontalmente con múltiples bases pequeñas.[cite:64] Por tanto, una plataforma educativa con muchas instituciones puede crecer de varias maneras:
- Base única inicial para etapa temprana, cuando el volumen todavía es moderado.
- Separación por institución cuando cada escuela justifica aislamiento de datos y carga.
- Réplicas de lectura para escenarios con muchas consultas de lectura distribuidas globalmente.[cite:67][cite:78]
- Indices y consultas optimizadas para mantener throughput alto en cada base.[cite:64]
- R2 para archivos grandes y D1 solo para metadatos, evitando mezclar binarios en SQL.
Evolución arquitectónica sugerida¶
Fase 1:
Worker único -> D1 único -> R2 opcional
Fase 2:
Worker único -> D1 por escuela -> R2 compartido -> logs centralizados
Fase 3:
Workers por dominio funcional -> D1 por tenant -> replicas de lectura -> colas/eventos para procesos asíncronos
La gran lección de este capítulo es que una base relacional bien diseñada no empieza por la tecnología, sino por el modelo de información. D1 facilita la operación, pero sigue exigiendo criterio técnico: relaciones correctas, SQL limpio, índices útiles, validación estricta y una arquitectura pensada para el crecimiento.[cite:64][cite:67][cite:78]