Every web application is, at its core, a system for managing data. Users create records, read them, update them, delete them. The data has structure -- fields with types, relationships between records, constraints that enforce business rules. In the JavaScript and TypeScript ecosystem, managing this data requires an ORM like Prisma or TypeORM, a database migration tool, a validation library like Zod or Joi, and hundreds of lines of glue code connecting these layers.
FLIN collapses all of that into two constructs: entities and enums. An entity is a data structure that knows how to persist itself. An enum is a constrained set of values that the runtime enforces automatically. Together, they replace the schema file, the migration tool, the ORM client, the validation layer, and much of the application logic that traditionally surrounds data management.
This article documents the design patterns we developed for entities and enums -- how they work, what they replace, and why they reduce a typical data layer from hundreds of lines to dozens.
Entity Declaration: What You Get for Free
A FLIN entity declaration looks deceptively simple:
entity Product {
name: text
price: decimal = 0.0
stock: int = 0
active: bool = true
description: text
category: text
image: file
cost: money
}Behind those eight field declarations, the runtime provides five auto-generated fields, a complete persistence layer, soft delete support, and automatic versioning. Every entity gets id (auto-incrementing primary key), created_at (set on first save), updated_at (set on every save), deleted_at (set on soft delete), and version (incremented on every save). None of these need to be declared. None of them need configuration.
Compare this with the Prisma equivalent:
model Product {
id Int @id @default(autoincrement())
name String
price Float @default(0.0)
stock Int @default(0)
active Boolean @default(true)
description String?
category String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// No soft delete support
// No versioning support
}The Prisma version requires explicit declaration of the ID field with three decorators, explicit timestamps with their own decorators, and provides no soft delete or versioning at all. Adding soft delete to a Prisma application means adding a deletedAt field, modifying every query to filter deleted records, and writing middleware to intercept delete operations. In FLIN, it is one keyword: delete.
The CRUDD Pattern
FLIN uses CRUDD -- Create, Read, Update, Delete, Destroy -- instead of the traditional CRUD. The distinction between Delete (soft, recoverable) and Destroy (hard, permanent) is built into the language:
// Create
user = User { name: "Alice", email: "[email protected]" }
save user // Inserts new record, sets id, created_at, version=1// Update user.name = "Alice Smith" save user // Updates existing record, sets updated_at, version=2
// Delete (soft -- record hidden but recoverable) delete user // Sets deleted_at timestamp
// Restore (undo soft delete) restore user // Clears deleted_at
// Destroy (hard -- permanent removal) destroy user // Gone forever ```
The save keyword handles both creation and update. If the entity has an id, it updates. If it does not, it creates. This eliminates the branching logic that every application needs to distinguish between "new record" and "existing record" -- a pattern that generates subtle bugs in every codebase that implements it manually.
Soft-deleted records are automatically excluded from all queries. User.all, User.where(...), User.find(...), User.first, and User.last all skip records with a non-null deleted_at. The developer never needs to remember to add a where deletedAt is null clause. The runtime handles it.
Enum Types: Compile-Time Safety for Domain Values
Many fields in a database have a fixed set of valid values: status codes, priority levels, roles, categories. In JavaScript applications, these are typically represented as strings with no enforcement. A developer might type "active" in one place and "Active" in another, and the inconsistency silently propagates.
FLIN enums solve this at the language level:
enum Status {
Pending,
Active,
Completed,
Cancelled
}enum Priority { Low, Medium, High, Critical }
entity Task { title: text @required status: Status = "Pending" priority: Priority = "Medium" assignee: text } ```
When a Task is saved with a status field, the runtime automatically validates that the value is one of the enum's variants. Invalid values are rejected before they reach the database. The implementation is elegant: enum fields are stored as text in the database, and the emitter auto-injects a @one_of(...) validation with the enum's variants. No new opcodes were needed. The existing validation infrastructure handles everything.
This design means that enums work with the database's native string type, which simplifies migrations and makes the data readable in raw SQL queries. But the application layer has compile-time guarantees that no invalid value will ever be stored.
The 49 Validators
FLIN provides forty-nine built-in field validators as decorators. In the JavaScript ecosystem, field validation requires a separate library -- Zod, Joi, Yup, class-validator -- with its own API, its own error format, and its own learning curve. FLIN's validators are part of the entity declaration:
entity User {
email: text @required @email @unique
name: text @required @minLength(2) @maxLength(100)
age: int @min(13) @max(150)
role: text @required @one_of("user", "admin", "moderator") @default("user")
avatar: file @image @max_size("5MB")
website: text @url
phone: text @phone
tags: [text] @min_items(1) @max_items(20) @unique_items
}Each decorator translates to a runtime validation check that executes on every save operation. The validators cover string formats (email, URL, UUID, IP address, phone number, credit card, IBAN, ISBN, slug, hex color, MAC address, MIME type, semantic version, postal code, country code), numeric constraints (min, max, positive, negative, latitude, longitude), list constraints (min items, max items, unique items), file constraints (max size, min size, extension, MIME pattern), and field-level constraints (unique, immutable, default).
The key insight is that these validators compose naturally. @required @email @unique reads as a sentence: this field is required, must be a valid email, and must be unique across all records. There is no separate schema definition, no separate validation pass, no mapping between the schema and the validation rules. The entity declaration is the schema, the validation, and the documentation all in one place.
Entity Constraints
Beyond field-level validators, FLIN supports entity-level constraints that apply across multiple fields:
entity User {
email: text
role: text
phone: text@unique(email) // Single-field unique @unique(email, role) // Composite: same email OK for different roles @index(email) // Query performance @index(role, phone) // Composite index } ```
Composite unique constraints are a common requirement that ORMs handle awkwardly. In Prisma, you write @@unique([email, role]). In FLIN, you write @unique(email, role). The syntax is nearly identical, but the FLIN version lives inside the entity declaration alongside all other field definitions, not in a separate section.
FLIN's constraint system supports thirteen constraint types: PrimaryKey (automatic), Unique, UniqueComposite, NotNull, ForeignKey, Check, RequiredIf, Pattern, UniqueWhere (conditional unique), UniqueIgnoreCase, Immutable, and Index. Several of these -- UniqueWhere, UniqueIgnoreCase, RequiredIf, Immutable -- have no equivalent in Prisma or most ORMs. They address real business requirements that typically require custom application logic.
The Query Builder Pattern
FLIN's query builder chains directly from entity names, with no client library to import and no connection to manage:
// Basic queries
users = User.all // All non-deleted records
count = User.count // Total count
first = User.first // First by ID
found = User.find(42) // By ID (returns none if missing)
admin = User.get(42) // By ID (errors if missing)// Filtering and chaining products = Product .where(active == true && price > 10) .order_by("price", "desc") .limit(20) .offset(0) ```
The query builder works directly inside template interpolation, which eliminates the boilerplate of fetching data and passing it to the view:
<h2>Stats</h2>
<p>Total users: {User.count}</p>
<p>Active users: {User.where(active == true).count}</p>
<p>Pending tasks: {Task.where(done == false).count}</p>{for task in Task.where(done == false).order_by("priority", "desc").limit(5)}
In a React application, displaying a list of tasks requires useState to hold the data, useEffect to fetch it, an API endpoint to query the database, and a map call to render the list. In FLIN, it is one line inside a for loop. The runtime handles the database query, the data transfer, and the rendering.
Transactions: ACID Without the Ceremony
FLIN supports ACID transactions with a minimal API:
begin_transaction()
user.balance = user.balance - amount
save userrecipient.balance = recipient.balance + amount save recipient commit_transaction() ```
If any save within a transaction fails, all changes are rolled back automatically. The developer can also trigger explicit rollback:
begin_transaction()
save order
if order.total > user.balance {
rollback_transaction()
}
commit_transaction()This is simpler than Prisma's $transaction() API because it does not require wrapping operations in a callback or managing a transaction client. The transaction boundaries are explicit code blocks, not function parameters.
Temporal Queries: Data Through Time
Every entity in FLIN is automatically versioned. Each save increments the version counter and preserves the previous state. This enables temporal queries -- the ability to ask what an entity looked like at a previous point in time:
// Find entity at a specific version
old_user = User.find_at_version(42, version: 3)// Get full version history history = User.get_history(42)
// Query at a specific timestamp snapshot = User.at_timestamp(1706745600) ```
Temporal data is a requirement that most applications eventually need but few frameworks support natively. Audit logs, undo functionality, compliance reporting, debugging production issues -- all of these require knowing what the data looked like at a previous point in time. In a typical stack, this requires an audit log table, triggers or middleware to capture changes, and custom query logic to reconstruct historical state. In FLIN, it is built in.
Semantic Search on Entities
FLIN's semantic text field type enables AI-powered natural language search with zero configuration:
entity Article {
title: text
content: semantic text // Embeddings generated on save
author: text
}// Natural language search results = search "machine learning tutorials" in Article
// Use directly in templates {for article in results}
{article.title}
{article.content}
When an entity with a semantic text field is saved, the runtime automatically generates vector embeddings. The search keyword converts a natural language query to an embedding and finds similar records using HNSW vector indexing. No Elasticsearch, no Algolia, no external service. The search infrastructure is part of the entity definition.
Pattern: The Complete Admin Panel
Combining entities, enums, validators, the query builder, and template integration, a complete CRUD admin panel emerges in under sixty lines:
entity Product {
name: text @required
description: text
price: decimal @positive
stock: int @min(0)
active: bool = true
}products = Product.all editing = none showForm = false formName = "" formPrice = 0.0
fn saveProduct() { if editing != none { editing.name = formName editing.price = formPrice save editing } else { save Product { name: formName, price: formPrice } } editing = none showForm = false }
Products
| {product.name} | {product.price} |
This replaces a React admin panel that would require a component library, state management, API endpoints, database queries, validation middleware, and likely a form library. The FLIN version handles persistence, validation, soft delete, versioning, and rendering in a single file.
The Prisma Migration Path
For teams migrating from a Prisma-based stack, the conversion is mechanical. Prisma models become FLIN entities. Prisma enums become FLIN enums. Prisma's @@index becomes FLIN's @index. Prisma's @@unique becomes FLIN's @unique. The concepts map one-to-one, but the FLIN versions are shorter and include features that Prisma does not support: soft delete, versioning, 49 validators, semantic search, graph queries, temporal queries, immutable fields, and conditional unique constraints.
The entity and enum system is the foundation on which everything else in FLIN is built. Routes query entities. Templates render entities. Forms save entities. Validation decorates entities. Search indexes entities. The entire application revolves around data, and FLIN makes data management a first-class concern of the language rather than an afterthought delegated to external libraries.
---
This is Part 192 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO designed and built a programming language from scratch.
Series Navigation: - [191] JavaScript and TypeScript Compatibility - [192] Entity and Enum Patterns (you are here) - [193] The FLIN Showcase App