Back to flin
flin

FlinDB: base de datos embebida sin configuración

Cómo construimos FlinDB, una base de datos embebida sin configuración para el lenguaje de programación FLIN -- sin cadenas de conexión, sin migraciones, sin servidor externo. Solo guardar y listo.

Thales & Claude | March 30, 2026 12 min flin
EN/ FR/ ES
flinrust

Todos los frameworks web que existen te obligan a configurar una base de datos antes de poder almacenar un solo registro. Instalar PostgreSQL. Configurar cadenas de conexión. Escribir migraciones. Configurar un ORM. Gestionar credenciales. Es un ritual tan profundamente arraigado que la mayoría de los desarrolladores nunca lo cuestionan.

Nosotros lo cuestionamos.

Cuando Thales planteó la visión de FLIN -- un lenguaje de programación diseñado para eliminar la complejidad accidental -- la base de datos fue el primer objetivo. No porque las bases de datos sean malas, sino porque la ceremonia que las rodea es absurda. Un desarrollador que escribe save user no debería necesitar primero pasar una hora configurando infraestructura. Los datos simplemente deberían persistir.

Esta es la historia de FlinDB: la base de datos embebida nativa de FLIN, construida desde cero en Rust, que no requiere configuración, ni cadenas de conexión, ni migraciones. Es la base que hace que FLIN se sienta como magia -- y la ingeniería detrás de ella es todo menos eso.

El problema que nos negamos a aceptar

Considera lo que una aplicación web típica requiere antes de poder almacenar su primer dato:

bash# Step 1: Install a database server
brew install postgresql@17
brew services start postgresql@17

# Step 2: Create a database
createdb myapp_development

# Step 3: Configure connection
DATABASE_URL=postgres://localhost:5432/myapp_development

# Step 4: Set up an ORM
npm install prisma @prisma/client
npx prisma init

# Step 5: Write a schema
# prisma/schema.prisma -- 30 lines

# Step 6: Run migrations
npx prisma migrate dev --name init

# Step 7: Generate client
npx prisma generate

Siete pasos. Tres herramientas. Dos archivos de configuración. Y todavía no has escrito una sola línea de lógica de aplicación. Para un desarrollador profesional que ha hecho esto cien veces, toma quince minutos. Para un estudiante en Abiyán descubriendo el desarrollo web por primera vez, es un muro.

Thales fue categórico: las aplicaciones FLIN deben persistir datos sin configuración. Sin servidores externos. Sin cadenas de conexión. Sin archivos de migración. El lenguaje mismo se encargaría del almacenamiento.

La arquitectura: dos capas, una experiencia

FlinDB se divide en dos capas distintas. La capa de cara al usuario habla FLIN -- la propia sintaxis del lenguaje para definir entidades y realizar operaciones. La capa interna es ZeroCore, un motor de almacenamiento escrito en Rust que maneja la mecánica de persistencia, indexación, versionado y recuperación.

+---------------------------------------------------------+
|                   FlinDB (Product Layer)                  |
|                                                          |
|   What users interact with:                              |
|   - entity definitions                                   |
|   - save / delete commands                               |
|   - queries (where, find, all)                           |
|   - temporal queries (@)                                 |
|   - semantic search                                      |
|                                                          |
+----------------------------------------------------------+
|                  ZeroCore Engine (Internal)               |
|                                                          |
|   Implementation details:                                |
|   - Storage format                                       |
|   - Indexing                                             |
|   - Version management                                   |
|   - Vector embeddings                                    |
|   - Disk persistence                                     |
|                                                          |
|   File: src/database/zerocore.rs                         |
+----------------------------------------------------------+

Esta separación importa. Un desarrollador FLIN escribe save user y nunca piensa en los detalles internos de almacenamiento. Pero bajo la superficie, ZeroCore está realizando escritura anticipada en el log, checksums CRC-32, indexación automática y gestión de versiones. La simplicidad en la superficie está respaldada por ingeniería de base de datos real debajo.

Definición de entidades: sin archivos de esquema, sin migraciones

En FLIN, defines las estructuras de datos usando la palabra clave entity, directamente en el código de tu aplicación. Sin archivo de esquema separado. Sin directorio de migraciones. Sin paso de traducción de DSL a SQL.

flinentity User {
    name: text
    email: text
    age: int
}

Esa es toda la "configuración de la base de datos". Cuando ZeroCore encuentra esta definición de entidad, automáticamente:

  1. Crea almacenamiento para el tipo de entidad User
  2. Asigna IDs únicos a cada instancia
  3. Rastrea marcas temporales de creación y actualización
  4. Mantiene un historial completo de versiones
  5. Indexa la clave primaria

Sin sentencia CREATE TABLE. Sin ALTER TABLE cuando agregas un campo después. Sin un ejecutor de migraciones que falla a las 3 AM porque alguien olvidó hacer commit de un archivo de migración.

Cada entidad recibe automáticamente cuatro campos del sistema que el desarrollador nunca declara pero siempre puede acceder:

flinuser = User.find(1)
user.id          // 1 (auto-generated)
user.created_at  // 2026-01-13T10:00:00Z
user.updated_at  // 2026-01-13T14:30:00Z
user.version     // 3

El campo version merece atención. FlinDB es una base de datos temporal -- cada cambio crea una nueva versión, y la versión anterior se preserva. Esto no es una idea añadida sobre la marcha. Es la base del modelo de almacenamiento.

El motor ZeroCore

ZeroCore es el motor en Rust que hace que todo funcione. Su estructura de datos principal es engañosamente simple:

rustpub struct ZeroCore {
    // Entity schemas
    schemas: HashMap<String, EntitySchema>,

    // Data storage: entity_type -> id -> versions
    data: HashMap<String, HashMap<EntityId, Vec<VersionedEntity>>>,

    // Indexes for fast queries
    indexes: HashMap<String, Index>,

    // Vector store for semantic search
    vectors: VectorStore,

    // Disk persistence
    storage: Storage,
}

Cinco campos. Esquemas, datos, índices, vectores y almacenamiento. Ese es todo el motor de base de datos en una estructura. Cada operación -- guardar, eliminar, consultar, buscar -- es un método en esta estructura.

El campo data es la decisión de diseño clave. Es un HashMap anidado: tipo de entidad a ID de entidad a un vector de entidades versionadas. Esto significa que buscar una entidad específica por tipo e ID es O(1), y acceder a su historial completo es simplemente indexar en un Vec. Sin joins. Sin recorrido de B-tree. Acceso directo a memoria.

Almacenamiento en disco

FlinDB almacena todo en un directorio .flindb/ junto a la aplicación FLIN:

.flindb/
+-- wal.log                    # Write-ahead log
+-- lock                       # Process lock file
+-- meta.json                  # Database metadata
+-- data/
|   +-- Todo.flindb            # All Todo records
|   +-- User.flindb            # All User records
|   +-- ChatMessage.flindb     # All ChatMessage records
+-- schema.flindb              # Persisted entity schemas
+-- semantic/                  # Vector embeddings

El log de escritura anticipada (WAL) es la fuente de verdad entre checkpoints. Cada mutación -- guardar, eliminar, actualizar -- primero se escribe en wal.log. Los archivos de datos en data/ son snapshots creados durante los checkpoints. En la recuperación, ZeroCore lee primero los archivos de datos, luego reproduce el WAL para ponerse al día con el estado más reciente.

Esta es la misma arquitectura utilizada por PostgreSQL y SQLite. Escribir primero en el log, hacer checkpoint periódicamente, reproducir en la recuperación. La diferencia es que ZeroCore lo hace sin configuración, y toda la base de datos vive en un solo directorio que puedes copiar, mover o respaldar con cp -r.

La experiencia del desarrollador

Así se siente construir con FlinDB. Sin ceremonia de configuración. Sin boilerplate. Declaras entidades y empiezas a usarlas.

flinentity Todo {
    title: text
    done: bool = false
    created: time = now
    priority: int = 1
}

// Create
todo = Todo { title: "Ship FlinDB article", priority: 3 }
save todo

// Read
all_todos = Todo.all
urgent = Todo.where(priority > 2)
first_todo = Todo.find(1)

// Update
first_todo.done = true
save first_todo

// Delete
delete first_todo

Compara esto con el equivalente en un stack típico de Node.js + PostgreSQL:

javascript// After 7 setup steps and 15+ dependencies...
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();

const todo = await prisma.todo.create({
  data: { title: "Ship FlinDB article", priority: 3 }
});

const allTodos = await prisma.todo.findMany();
const urgent = await prisma.todo.findMany({
  where: { priority: { gt: 2 } }
});

await prisma.todo.update({
  where: { id: todo.id },
  data: { done: true }
});

await prisma.todo.delete({ where: { id: todo.id } });

Ambos logran lo mismo. Pero la versión FLIN no requirió instalación, ni configuración, ni cadena de conexión, ni migración, ni sentencia de importación. La base de datos simplemente está ahí, integrada en el lenguaje.

Configuración: opcional, no requerida

FlinDB funciona de serie con valores predeterminados sensatos. Pero para casos de uso avanzados, puedes configurarlo a través de un archivo flin.config:

flindatabase {
    path = ".flindb"
    wal = true
    compaction_interval = "1h"
    embedding_model = "default"
}

O a través de variables de entorno:

bashFLIN_DB_PATH=./.flindb
FLIN_DB_WAL=true
FLIN_DB_MAX_WAL_ENTRIES=1000
FLIN_DB_MAX_WAL_BYTES=10485760

El sistema de configuración soporta tres modos con diferentes valores predeterminados:

ModoUbicación de la base de datosComportamiento
dev.flindb/Logging detallado, auto-recarga
test:memory:En memoria, reinicio por prueba
prod.flindb/Logging mínimo, optimizado

En modo prueba, la base de datos es completamente en memoria -- sin E/S de disco, sin limpieza entre pruebas, sin problemas de aislamiento de pruebas. En modo producción, el logging se reduce solo a advertencias, y el motor optimiza para rendimiento sobre depurabilidad.

¿Por qué no simplemente usar SQLite?

Esta es la pregunta que escuchamos más a menudo. SQLite es embebido. SQLite es sin configuración (más o menos). SQLite está probado en batalla. ¿Por qué construir el nuestro?

La respuesta corta: SQLite es una base de datos relacional con SQL como su interfaz. FlinDB es una base de datos temporal y orientada a entidades con FLIN como su interfaz. Resuelven problemas diferentes.

La respuesta más larga involucra varias brechas específicas que SQLite no puede llenar para el caso de uso de FLIN:

Sin versionado temporal. SQLite no rastrea el historial de entidades. Si actualizas una fila, el valor anterior desaparece. Construir consultas temporales sobre SQLite requiere un sistema de event sourcing personalizado -- fácilmente mil líneas de código que cada aplicación necesitaría duplicar.

Sin búsqueda semántica. SQLite tiene FTS5 para búsqueda de texto completo, pero no embeddings vectoriales, ni búsqueda por similitud, ni consultas potenciadas por IA. FlinDB tiene todo esto integrado.

Sin suscripciones en tiempo real. FlinDB soporta sintaxis watch que notifica a los clientes cuando los datos cambian. SQLite no tiene mecanismo de notificación.

SQL es la interfaz equivocada para FLIN. FLIN no es SQL. Hacer que los desarrolladores de FLIN escriban cadenas SQL dentro de un lenguaje diseñado para eliminar interfaces basadas en cadenas sería una contradicción. FlinDB habla FLIN nativamente.

Cubrimos la comparación completa -- con benchmarks, ejemplos de código e historias de migración -- en el Artículo 069 de esta serie.

El viaje de implementación

Construir FlinDB no fue una sola sesión. Fue un esfuerzo sostenido a lo largo de múltiples sesiones, cada una añadiendo una capa crítica:

  • Sesión 160 sentó las bases: creación de entidades, operaciones CRUD, aplicación de restricciones y 37 pruebas.
  • Sesión 161 añadió restricciones avanzadas: unique compuesto, aplicación de claves foráneas, eliminación en cascada, validación de patrones -- 31 pruebas más.
  • Sesión 162 trajo analítica: agregaciones, GROUP BY, DISTINCT, operadores IN/NOT IN -- 12 pruebas.
  • Sesión 163 hizo las consultas rápidas: población de índices, mantenimiento en save/delete/restore, y optimización de consultas de O(n) a O(1) -- 9 pruebas.
  • Sesión 164 implementó relaciones: carga eager, consultas de referencia, consultas inversas, auto-indexación -- 13 pruebas.
  • Sesión 166 fue la maratón: transacciones ACID, backup/restore, consultas de grafo y búsqueda semántica en una sola sesión -- 94 pruebas.

Al final de este arco, FlinDB tenía más de 200 pruebas, soportaba cada operación que una aplicación de producción necesita, y requería exactamente cero líneas de configuración del desarrollador.

Lo que hizo posible la configuración cero

Tres decisiones de diseño hicieron que la configuración cero funcionara en la práctica.

Primero, convención sobre configuración. La base de datos siempre vive en .flindb/ relativo a la raíz del proyecto. El WAL siempre es wal.log. Los esquemas de entidades se derivan del código, no de archivos de configuración. Hay exactamente un lugar correcto para todo, así que no hay nada que configurar.

Segundo, evolución automática del esquema. Cuando un desarrollador agrega un campo a una entidad, ZeroCore detecta el cambio y lo maneja. Sin archivo de migración. Sin ALTER TABLE. El esquema se deriva de la declaración de entidad al inicio, se compara con el esquema persistido, y se actualiza de forma transparente.

Tercero, valores predeterminados sensatos que funcionan para el 99% de los casos. El modo WAL está activado. El auto-checkpoint se activa a 1.000 entradas o 10 MB. La eliminación suave es el predeterminado. Los índices se crean automáticamente para claves primarias y foráneas. Un desarrollador que nunca mira la configuración obtiene una base de datos que se comporta correctamente bajo carga normal.

El 1% restante que necesita ajustar el comportamiento puede hacerlo a través de variables de entorno o el archivo de configuración -- pero nunca tiene que hacerlo.

El momento en que todo encajó

El momento en que FlinDB realmente se demostró fue la Sesión 201, cuando construimos la demo embebida de Todo. Una aplicación Todo completa con almacenamiento persistente, ejecutándose en el navegador, respaldada por FlinDB. Toda la aplicación eran 40 líneas de FLIN. Sin npm install. Sin prisma migrate. Sin docker-compose up. Solo flin dev y una aplicación funcional y persistente.

Cuando Thales refrescó la página y los todos seguían ahí -- persistidos a través del WAL, recuperados al reiniciar, con historial de versiones completo -- ese fue el momento en que la promesa de configuración cero se hizo real. No una demo. No un prototipo. Una aplicación real con persistencia real que requirió exactamente cero configuración de base de datos.

Eso es FlinDB. Un motor de base de datos que desaparece. Defines entidades. Las guardas. Persisten. Todo lo demás se maneja automáticamente.


Esta es la Parte 1 de la serie "Cómo construimos FlinDB", documentando cómo construimos un motor de base de datos embebido completo para el lenguaje de programación FLIN.

Navegación de la serie: - [056] FlinDB: Zero-Configuration Embedded Database (estás aquí) - [057] Entities, Not Tables: How FlinDB Thinks About Data - [058] CRUD Without SQL - [059] Constraints and Validation in FlinDB - [060] Aggregations and Analytics

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles