Every web framework in existence makes you set up a database before you can store a single record. Install PostgreSQL. Configure connection strings. Write migrations. Set up an ORM. Manage credentials. It is a ritual so deeply ingrained that most developers never question it.
We questioned it.
When Thales laid out the vision for FLIN -- a programming language designed to eliminate accidental complexity -- the database was the first target. Not because databases are bad, but because the ceremony surrounding them is absurd. A developer who writes save user should not need to first spend an hour configuring infrastructure. The data should just persist.
This is the story of FlinDB: FLIN's native embedded database, built from scratch in Rust, requiring zero configuration, zero connection strings, and zero migrations. It is the foundation that makes FLIN feel like magic -- and the engineering behind it is anything but.
The Problem We Refused to Accept
Consider what a typical web application requires before it can store its first piece of data:
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 generateSeven steps. Three tools. Two configuration files. And you still have not written a single line of application logic. For a professional developer who has done this a hundred times, it takes fifteen minutes. For a student in Abidjan discovering web development for the first time, it is a wall.
Thales was categorical: FLIN applications must persist data with zero setup. No external servers. No connection strings. No migration files. The language itself would handle storage.
The Architecture: Two Layers, One Experience
FlinDB is split into two distinct layers. The user-facing layer speaks FLIN -- the language's own syntax for defining entities and performing operations. The internal layer is ZeroCore, a storage engine written in Rust that handles the mechanics of persistence, indexing, versioning, and recovery.
+---------------------------------------------------------+
| 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 |
+----------------------------------------------------------+This separation matters. A FLIN developer writes save user and never thinks about storage internals. But under the hood, ZeroCore is performing write-ahead logging, CRC-32 checksums, automatic indexing, and version management. The simplicity at the surface is backed by real database engineering underneath.
Entity Definition: No Schema Files, No Migrations
In FLIN, you define data structures using the entity keyword, directly in your application code. No separate schema file. No migration directory. No DSL-to-SQL translation step.
flinentity User {
name: text
email: text
age: int
}That is the entire "database setup." When ZeroCore encounters this entity definition, it automatically:
- Creates storage for the
Userentity type - Assigns unique IDs to every instance
- Tracks creation and update timestamps
- Maintains a complete version history
- Indexes the primary key
No CREATE TABLE statement. No ALTER TABLE when you add a field later. No migration runner that breaks at 3 AM because someone forgot to commit a migration file.
Every entity automatically receives four system fields that the developer never declares but can always access:
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 // 3The version field deserves attention. FlinDB is a temporal database -- every change creates a new version, and the old version is preserved. This is not an afterthought bolted on top. It is the foundation of the storage model.
The ZeroCore Engine
ZeroCore is the Rust engine that makes everything work. Its core data structure is deceptively 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,
}Five fields. Schemas, data, indexes, vectors, and storage. That is the entire database engine in one struct. Every operation -- save, delete, query, search -- is a method on this struct.
The data field is the key design decision. It is a nested HashMap: entity type to entity ID to a vector of versioned entities. This means that looking up a specific entity by type and ID is O(1), and accessing its complete history is just indexing into a Vec. No joins. No B-tree traversal. Direct memory access.
Storage on Disk
FlinDB stores everything in a .flindb/ directory alongside the FLIN application:
.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 embeddingsThe write-ahead log (WAL) is the source of truth between checkpoints. Every mutation -- save, delete, update -- is first written to wal.log. Data files in data/ are snapshots created during checkpoints. On recovery, ZeroCore reads the data files first, then replays the WAL to catch up to the latest state.
This is the same architecture used by PostgreSQL and SQLite. Write to the log first, checkpoint periodically, replay on recovery. The difference is that ZeroCore does it with zero configuration, and the entire database lives in a single directory that you can copy, move, or back up with cp -r.
The Developer Experience
Here is what building with FlinDB actually feels like. No setup ceremony. No boilerplate. You declare entities and start using them.
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_todoCompare this to the equivalent in a typical Node.js + PostgreSQL stack:
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 } });Both accomplish the same thing. But the FLIN version required no installation, no configuration, no connection string, no migration, and no import statement. The database is simply there, built into the language.
Configuration: Optional, Not Required
FlinDB works out of the box with sensible defaults. But for advanced use cases, you can configure it through a flin.config file:
flindatabase {
path = ".flindb"
wal = true
compaction_interval = "1h"
embedding_model = "default"
}Or through environment variables:
bashFLIN_DB_PATH=./.flindb
FLIN_DB_WAL=true
FLIN_DB_MAX_WAL_ENTRIES=1000
FLIN_DB_MAX_WAL_BYTES=10485760The configuration system supports three modes with different defaults:
| Mode | Database Location | Behavior |
|---|---|---|
dev | .flindb/ | Verbose logging, auto-reload |
test | :memory: | In-memory, reset per test |
prod | .flindb/ | Minimal logging, optimized |
In test mode, the database is entirely in-memory -- no disk I/O, no cleanup between tests, no test isolation issues. In production mode, logging drops to warnings only, and the engine optimizes for throughput over debuggability.
Why Not Just Use SQLite?
This is the question we heard most often. SQLite is embedded. SQLite is zero-configuration (sort of). SQLite is battle-tested. Why build our own?
The short answer: SQLite is a relational database with SQL as its interface. FlinDB is a temporal, entity-oriented database with FLIN as its interface. They solve different problems.
The longer answer involves several specific gaps that SQLite cannot fill for FLIN's use case:
No temporal versioning. SQLite does not track entity history. If you update a row, the old value is gone. Building temporal queries on top of SQLite requires a custom event sourcing system -- easily a thousand lines of code that every application would need to duplicate.
No semantic search. SQLite has FTS5 for full-text search, but no vector embeddings, no similarity search, no AI-powered queries. FlinDB has all of these built in.
No real-time subscriptions. FlinDB supports watch syntax that notifies clients when data changes. SQLite has no notification mechanism.
SQL is the wrong interface for FLIN. FLIN is not SQL. Making FLIN developers write SQL strings inside a language designed to eliminate string-based interfaces would be a contradiction. FlinDB speaks FLIN natively.
We cover the full comparison -- with benchmarks, code examples, and migration stories -- in Article 069 of this series.
The Implementation Journey
Building FlinDB was not a single session. It was a sustained effort across multiple sessions, each adding a critical layer:
- Session 160 laid the foundation: entity creation, CRUD operations, constraint enforcement, and 37 tests.
- Session 161 added advanced constraints: composite unique, foreign key enforcement, cascade delete, pattern validation -- 31 more tests.
- Session 162 brought analytics: aggregations, GROUP BY, DISTINCT, IN/NOT IN operators -- 12 tests.
- Session 163 made queries fast: index population, maintenance on save/delete/restore, and query optimization from O(n) to O(1) -- 9 tests.
- Session 164 implemented relationships: eager loading, reference queries, inverse queries, auto-indexing -- 13 tests.
- Session 166 was the marathon: ACID transactions, backup/restore, graph queries, and semantic search in a single session -- 94 tests.
By the end of this arc, FlinDB had over 200 tests, supported every operation a production application needs, and required exactly zero lines of configuration from the developer.
What Made Zero-Config Possible
Three design decisions made zero-configuration work in practice.
First, convention over configuration. The database always lives at .flindb/ relative to the project root. The WAL is always wal.log. Entity schemas are derived from code, not configuration files. There is exactly one right place for everything, so there is nothing to configure.
Second, automatic schema evolution. When a developer adds a field to an entity, ZeroCore detects the change and handles it. No migration file. No ALTER TABLE. The schema is derived from the entity declaration at startup, compared to the persisted schema, and updated transparently.
Third, sensible defaults that work for 99% of cases. WAL mode is on. Auto-checkpointing triggers at 1000 entries or 10 MB. Soft delete is the default. Indexes are created automatically for primary keys and foreign keys. A developer who never looks at configuration gets a database that behaves correctly under normal load.
The remaining 1% who need to tune behavior can do so through environment variables or the config file -- but they never have to.
The Moment It Clicked
The moment FlinDB truly proved itself was Session 201, when we built the embedded Todo demo. A complete Todo application with persistent storage, running in the browser, backed by FlinDB. The entire application was 40 lines of FLIN. No npm install. No prisma migrate. No docker-compose up. Just flin dev and a working, persistent application.
When Thales refreshed the page and the todos were still there -- persisted through the WAL, recovered on restart, with full version history -- that was the moment the zero-configuration promise became real. Not a demo. Not a prototype. An actual application with actual persistence that required exactly zero database setup.
That is what FlinDB is. A database engine that disappears. You define entities. You save them. They persist. Everything else is handled.
This is Part 1 of the "How We Built FlinDB" series, documenting how we built a complete embedded database engine for the FLIN programming language.
Series Navigation: - [056] FlinDB: Zero-Configuration Embedded Database (you are here) - [057] Entities, Not Tables: How FlinDB Thinks About Data - [058] CRUD Without SQL - [059] Constraints and Validation in FlinDB - [060] Aggregations and Analytics