Concepts Overview
Loopar is a meta-framework β a framework that can build itself. At its core lies a powerful concept: Entity, the master builder that creates other builders.
The Philosophy
Traditional frameworks require you to write code for every model, view, and controller. Loopar inverts this: you define what you want, and the framework generates everything else.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β THE LOOPAR WAY β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Traditional: Code β Application β
β β
β Loopar: Definition β Code β Application β
β β β
β (drag & drop) β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Core Concepts
| Concept | Description |
|---|---|
| Entity | The master builder β defines data models and generates everything |
| Builder | Specialized entity types (Page, View, Form, Controller) |
| App | A self-contained application package |
| Module | Groups related entities within an app |
| Document | A data-centric entity (stored in database) |
| Page | A presentation-centric entity (web pages) |
| Component | UI elements used in drag-and-drop design |
The Auto-Recursive Magic
What makes Loopar unique is that Entity is built using Entity. The system bootstraps itself:
- Entity defines the structure of all models
- Entity creates Builders (specialized entity types)
- Builders create specific model types (Pages, Forms, Views)
- You use Builders to create your application
This means you can extend Loopar using Loopar itself.
Entity
Entity is the foundational concept in Loopar. It's the master builder that defines data models and automatically generates all the code needed to work with them.
What Entity Creates
When you define an Entity, Loopar automatically generates:
| Generated | Description |
|---|---|
| Database Table | With proper schema and automatic migrations |
| Model Class | BaseDocument subclass with hooks & validations |
| HTTP Actions | list, view, create, update, delete... under /api and /desk |
| React Form | With all field types rendered |
| List View | With search, filters, and pagination |
| Controller | BaseController subclass dispatching the actions |
Entity Structure
An entity lives inside a module and can carry up to four kinds of files β only the .json is required:
/modules/[module]/entities/[entity-name]/
βββ entity-name.json # Metadata & visual structure (required)
βββ entity-name.js # Model β server-side business logic
βββ entity-name-controller.js # Controller β HTTP actions
βββ client/ # Client-side React views
βββ entity-name-form.jsx
βββ entity-name-list.jsx
βββ entity-name-view.jsx
The JSON File (Required)
You normally never write it by hand β the visual designer maintains it. Fields are elements inside doc_structure, mixed freely with layout elements:
{
"name": "Customer",
"module": "crm",
"is_single": 0,
"doc_structure": "[
{ \"element\": \"input\", \"data\": { \"name\": \"customer_name\", \"label\": \"Name\", \"required\": 1 } },
{ \"element\": \"input\", \"data\": { \"name\": \"email\", \"format\": \"email\", \"unique\": 1 } },
{ \"element\": \"select\", \"data\": { \"name\": \"status\", \"options\": \"Lead\nActive\nInactive\" } },
{ \"element\": \"switch\", \"data\": { \"name\": \"vip\" } }
]"
}
Every form element (input, select, switch, currency, date, textarea, ...) becomes a database column; layout and design elements (row, col, card, title, ...) only shape the UI.
The Model File (Optional)
entity-name.js β server-side business logic and lifecycle hooks:
import { BaseDocument, loopar } from "loopar";
export default class Customer extends BaseDocument {
async beforeSave() {
if (this.__IS_NEW__) {
this.status = this.status || "Lead";
}
}
async afterSave() {
if (this.__IS_NEW__) await this.sendWelcomeEmail();
}
async validate() {
await super.validate();
if (!this.email.includes("@")) {
loopar.throw("Invalid email format");
}
}
}
The Controller File (Optional)
entity-name-controller.js β HTTP actions:
import { BaseController, loopar } from "loopar";
export default class CustomerController extends BaseController {
// GET/POST /api/Customer/stats
async actionStats() {
return { total: await loopar.db.count("Customer") };
}
}
The Client Files (Optional)
client/entity-name-form.jsx β customize the React views by extending the matching context:
import FormContext from "@context/form-context";
export default class CustomerForm extends FormContext {
// custom form behavior
}
Entity Types
| Type | is_single | Description |
|---|---|---|
| Document | 0 | Multiple records (customers, products) |
| Single | 1 | One record only (settings, config) |
Document vs Single
Document entities store multiple records:
- Customer, Product, Order, Invoice
- Have list views with pagination
- CRUD operations on individual records
Single entities store only one record:
- System Settings, Company Info, App Config
- No list view, direct access to the form
- Perfect for configuration data
Builders
Builders are specialized entities designed to create specific types of models. While Entity is the general-purpose builder, each Builder is optimized for a particular use case.
Why Builders?
Entity is powerful but generic. Builders provide:
- Focused metadata β Only relevant fields for the model type
- Optimized UI β Tailored design experience
- Specialized behavior β Logic specific to the model type
Builder Types
Page Builder
Creates complete web pages: homepages, landing pages, blogs, documentation.
| Feature | Description |
|---|---|
| Purpose | Design full web pages visually |
| Output | Standalone page with route |
| Components | Sections, rows, columns, text, images, etc. |
| Use Cases | Homepage, About, Contact, Blog posts |
/pages/homepage/
βββ homepage.json # Page structure & components
βββ homepage.js # Page controller (optional)
βββ homepage.jsx # Custom React component (optional)
View Builder
Creates data display components that connect to a model.
| Feature | Description |
|---|---|
| Purpose | Visualize data from an entity |
| Output | Connected view component |
| Components | Tables, cards, charts, grids |
| Use Cases | Dashboards, reports, data displays |
Views connect to a source entity and render its data with custom layouts.
Form Builder
Creates forms for data collection without requiring a full entity.
| Feature | Description |
|---|---|
| Purpose | Collect and submit data |
| Output | Form that submits to a controller |
| Components | Inputs, selects, checkboxes, buttons |
| Use Cases | Login, registration, contact forms |
/forms/login-form/
βββ login-form.json # Form fields & layout
βββ login-form.js # Form submission handler
Controller Builder
Creates server-side controllers for custom actions without a model.
| Feature | Description |
|---|---|
| Purpose | Handle specific backend actions |
| Output | API routes and actions |
| Components | N/A (server-side only) |
| Use Cases | Auth endpoints, webhooks, integrations |
// login-controller.js
import { BaseController } from "loopar";
export default class LoginController extends BaseController {
async actionLogin() {
const { email, password } = this.data;
// Authentication logic
}
async actionLogout() {
// Logout logic
}
}
Choosing the Right Builder
| Need | Builder |
|---|---|
| Database model with CRUD | Entity |
| Web page design | Page Builder |
| Display existing data | View Builder |
| Standalone form | Form Builder |
| Custom API endpoints | Controller Builder |
App
App is Loopar's packaging system. An App is a self-contained application that bundles modules, entities, pages, and assets into an installable unit.
What is an App?
Think of an App as an independent program within Loopar:
- Contains all its modules and entities
- Can be installed/uninstalled on any tenant
- Can be shared, distributed, or sold (via git)
- Has its own version and dependencies
App Structure
/apps/
βββ my-crm/ # App directory
βββ installer.json # App manifest: metadata + documents to seed
βββ installer.js # Install/update logic (extends CoreInstaller)
βββ modules/ # App modules
β βββ contacts/ # Module: Contacts
β β βββ entities/
β β βββ customer/ # Entity: Customer
β β β βββ customer.json
β β β βββ customer.js
β β β βββ customer-controller.js
β β β βββ client/*.jsx
β β βββ lead/
β β βββ lead.json
β βββ sales/ # Module: Sales
β βββ entities/...
β βββ pages/... # Page Builder pages
βββ public/ # Static assets
βββ images/
βββ styles/
App Manifest (installer.json)
Declares the app's identity and every document it installs β entities, modules, pages β with a __root__ pointing at its file:
{
"App": {
"name": "my-crm",
"version": "1.0.0",
"description": "Customer relationship management app",
"autor": "you@example.com"
},
"documents": [
{ "entity": "Module", "name": "contacts", "app_name": "my-crm" },
{ "entity": "Entity", "name": "Customer", "__root__": "modules/contacts/entities/customer" }
]
}
The version bumps automatically when you save documents through the UI. If you edit an entity's .json by hand, bump the app version in installer.json yourself β otherwise the schema migration is skipped on update.
App Lifecycle
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β Create β β β Develop β β β Install β
β (in dev) β β (add models)β β (on tenant) β
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β Publish β β β Export β β β Test β
β (git) β β (package) β β (staging) β
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
App Manager
Open App Manager from the desk to:
| Action | Description |
|---|---|
| Create | Start a new app |
| Install | Install app on current tenant |
| Uninstall | Remove app from tenant |
| Update | Update to newer version |
| Import | Install from a git repository |
Default App Creation
When you create an App, Loopar automatically:
- Creates the app directory structure
- Generates a module with the same name
- Sets up the installer manifest
- Registers the app in the system
All entities you create are linked to the app through their module.
Module
Module is Loopar's organizational unit. Modules group related entities within an app, providing logical separation and structure.
What is a Module?
A Module is like a folder that contains related functionality:
- Groups entities by domain (sales, inventory, HR)
- Provides namespace isolation
- Enables selective installation
- Organizes the Desk sidebar
Module Structure
/apps/my-app/modules/
βββ core/ # Core functionality
β βββ user/
β βββ role/
β βββ settings/
βββ sales/ # Sales module
β βββ customer/
β βββ quote/
β βββ invoice/
βββ inventory/ # Inventory module
βββ product/
βββ warehouse/
βββ stock-entry/
Module in the Desk
Modules appear in the Desk sidebar, grouping their entities:
βββββββββββββββββββββββββββββββ
β DESK β
βββββββββββββββββββββββββββββββ€
β βΌ Core β
β Users β
β Roles β
β Settings β
β βΌ Sales β
β Customers β
β Quotes β
β Invoices β
β βΌ Inventory β
β Products β
β Warehouses β
β Stock Entries β
βββββββββββββββββββββββββββββββ
Creating a Module
- Go to Desk β Module β New
- Enter module details:
- Name: Module identifier (e.g.,
sales) - Label: Display name (e.g.,
Sales) - App: Parent app
- Icon: Module icon for the sidebar
- Save the module
Module vs App
| Aspect | App | Module |
|---|---|---|
| Scope | Complete application | Functional group |
| Contains | Modules, assets, config | Entities only |
| Installable | Yes | No (part of app) |
| Example | CRM App | Sales Module |
Fields & Components
In Loopar, fields and UI components are the same thing: elements. Form elements store data; layout and design elements shape the page.
Form Elements (stored in the database)
| Element | Description | Default Column |
|---|---|---|
input | Single line text (set format for email, int, decimal...) | VARCHAR(255) |
textarea | Multi-line text | LONGTEXT |
integer | Integer number | INTEGER |
decimal | Decimal number | DECIMAL (default 10,2) |
currency | Money values | DECIMAL (default 18,4; precision/scale configurable) |
date | Date only | DATE |
date_time | Date and time | TIMESTAMP |
time | Time only | TIME |
checkbox / switch | Boolean (yes/no) | INTEGER (0/1) |
select | Dropdown β static options or another entity | TEXT |
form_table | Child table (one-to-many) | β (child rows) |
password | Hashed password | TEXT |
color_picker | Color picker | TEXT |
image_input / file_input / file_uploader | Uploads (managed by File Manager) | LONGTEXT (ref) |
markdown_input | Markdown editor | TEXT |
text_editor | Rich text editor | LONGTEXT |
designer | Nested visual structure | LONGTEXT |
metadata | JSON data | JSON |
id | Auto-increment primary key | INCREMENTS |
input accepts a format in its data (data, email, password, text, int, float, decimal, currency, date, datetime, uuid...) that controls both validation and the column type.
Layout Elements
Structure the page (no database storage):
| Element | Description |
|---|---|
section | Full-width container |
row | Horizontal container with columns |
col | Column within a row |
card | Card-like container |
div / generic | Generic containers |
tabs / tab | Tabbed content |
panel | Styled container |
banner / banner_image | Hero banners |
contact_form / form | Embedded forms |
Design Elements
| Element | Description |
|---|---|
title / subtitle / paragraph | Text hierarchy |
text_block / text_block_icon | Text compositions |
image / gallery / carousel / slider | Media |
icon | Lucide icon |
button / link | Actions |
markdown / html_block | Rendered content |
feature_card | Feature highlight card |
review | Reviews block |
collection / collection_view | Data-driven collections |
stripe / stripe_embebed / stripe_plans | Stripe payments |
particles | Animated particle background |
seo | Page SEO metadata |
Element Properties
Common properties in an element's data:
| Property | Description |
|---|---|
name | Field identifier (snake_case) |
label | Display label |
format | Column/validation format (on input) |
required | Must have a value (1) |
unique | Must be unique in database (1) |
default_value | Default value |
hidden | Hide from UI (1) |
readonly | Cannot be edited (1) |
options | For select: newline-separated values, or an entity name |
description | Help text |
class | CSS classes |
Lifecycle Hooks
Loopar provides hooks at key points in a document's lifecycle. Use these to add custom business logic.
Model Hooks
Defined in the entity's model file (invoice.js). These are the hooks the document layer calls:
import { BaseDocument, loopar } from "loopar";
export default class Invoice extends BaseDocument {
async onLoad() {
// Runs after the document is loaded
}
async beforeSave() {
// Before any save (insert or update)
this.calculateTotals();
if (this.__IS_NEW__) {
// New record β set defaults
this.status = this.status || "Draft";
}
}
async validate() {
// Runs during save (skipped with save({ validate: false }))
await super.validate(); // keep the metadata-driven field validation
if (this.total < 0) {
loopar.throw("Total cannot be negative");
}
}
async afterSave() {
// After any save (insert or update)
if (this.__IS_NEW__) {
await this.notifyAccountant();
}
}
async afterDelete() {
// After deleting the record
await this.reverseStockEntries();
}
}
Hook Execution Order
On Save (insert or update)
1. beforeSave()
2. validate() β unless save({ validate: false })
3. β DATABASE INSERT / UPDATE β
4. child tables & file uploads
5. afterSave()
Branch on this.__IS_NEW__ inside beforeSave()/afterSave() to run insert-only or update-only logic.
On Delete
1. connected-documents guard β blocks the delete if other records reference this one
2. β DATABASE DELETE β
3. afterDelete()
ORM-level Hooks (create/update/delete granularity)
For per-operation events β or to react to another entity's writes β subscribe with loopar.hook() from anywhere on the server:
import { loopar } from "loopar";
loopar.hook("Invoice", "afterCreate", async (data) => {
await notifyAccountant(data);
});
loopar.hook("Invoice", "beforeDelete", async (data) => {
// runs before any Invoice row is deleted
});
Valid events: beforeCreate, afterCreate, beforeUpdate, afterUpdate, beforeDelete, afterDelete, plus beforeSave/afterSave as create+update shorthand.
Accessing Data in Hooks
import { loopar } from "loopar";
async beforeSave() {
// Access field values
const customerName = this.customer_name;
// Current user
this.modified_by = loopar.currentUser?.name;
// Query other documents
const customer = await loopar.getDocument("Customer", this.customer);
// Set field values
this.total = this.calculateTotal();
}
Client vs Server
Loopar separates client-side and server-side code clearly. Understanding this separation is key to extending the framework.
File Responsibilities
/entities/entity-name/
βββ entity-name.json β Definition (shared)
βββ entity-name.js β Model (server, Node.js)
βββ entity-name-controller.js β Controller (server, HTTP actions)
βββ client/*.jsx β Views (browser, React)
| File | Runs On | Purpose |
|---|---|---|
.json | Both | Element definitions, metadata |
.js | Server | Business logic, hooks, database |
-controller.js | Server | HTTP actions (action* / publicAction*) |
client/*.jsx | Client | UI components, interactions |
Server-Side (.js / -controller.js)
Runs in Node.js. Has access to:
- Database operations
- File system
- Environment variables
- External APIs (server-to-server)
- Authentication and permissions
- Business logic hooks
// entity-name.js β Model
import { BaseDocument, loopar } from "loopar";
export default class MyEntity extends BaseDocument {
async beforeSave() {
const count = await loopar.db.count("Customer");
const apiKey = process.env.API_KEY;
}
}
// entity-name-controller.js β Controller (URL-reachable)
import { BaseController, loopar } from "loopar";
export default class MyEntityController extends BaseController {
// GET/POST /api/My Entity/customEndpoint
async actionCustomEndpoint() {
return this.success("Done");
}
}
Client-Side (client/*.jsx)
Runs in the browser (React). Has access to:
- UI rendering
- User interactions
- Browser APIs
- HTTP requests to the server (via
loopar.call) - Component state
// client/my-entity-form.jsx
import FormContext from "@context/form-context";
import { loopar } from "loopar";
export default class MyEntityForm extends FormContext {
async runCustomAction() {
const r = await loopar.call("My Entity", "customEndpoint");
loopar.notify(r.message, "success");
}
}
Communication Between Client & Server
βββββββββββββββββββ βββββββββββββββββββ
β CLIENT β β SERVER β
β (Browser) β β (Node.js) β
βββββββββββββββββββ€ βββββββββββββββββββ€
β β HTTP β β
β React View β βββββββ β Controller β
β (client/*.jsx) β Request β (-controller) β
β β β β β
β β βββββββ β Model (.js) β
β β Responseβ β
βββββββββββββββββββ βββββββββββββββββββ
When to Use Each
| Use Model (.js) For | Use Controller For | Use Client (.jsx) For |
|---|---|---|
| Data validation | Custom endpoints | UI customization |
| Business rules | Access control | User interactions |
| Computed fields | Response shaping | Form behavior |
| Hooks on save/delete | Webhooks (publicAction*) | Conditional rendering |
Multi-Tenant Architecture
Loopar's multi-tenant system allows you to run multiple isolated sites from a single codebase.
What is Multi-Tenancy?
Each tenant is a completely independent site with:
- Own database β Complete data isolation
- Own domain β Custom URL for each client
- Own config β Independent settings
- Own users β Separate user base
- Shared code β Same apps, different data
Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SINGLE CODEBASE β
β /apps, /packages β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β βββββββββββββββββ βββββββββββββββββ βββββββββββββββββ β
β β TENANT A β β TENANT B β β TENANT C β β
β β (dev) β β (client-1) β β (client-2) β β
β βββββββββββββββββ€ βββββββββββββββββ€ βββββββββββββββββ€ β
β β Database: dev β β Database: c1 β β Database: c2 β β
β β Port: 3000 β β Port: 3001 β β Port: 3002 β β
β β Domain: β β β app1.com β β app2.com β β
β βββββββββββββββββ βββββββββββββββββ βββββββββββββββββ β
β β β β β
β ββββββββββββββ¬βββββ΄βββββββββββββββββ β
β β β
β PM2 β
β (Process Manager) β
β β β
β CADDY β
β (Reverse Proxy + SSL) β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Site Directory Structure
/sites/
βββ dev/ # Development tenant
β βββ .env # Environment config
β βββ config/
β β βββ db.config.json # Database connection
β βββ database/ # SQLite file (when using SQLite)
β βββ public/
β β βββ uploads/ # Tenant-specific uploads
β βββ sessions/ # User sessions
β βββ theme.css # Tenant theme
β
βββ client-1/ # Production tenant
β βββ ...
β
βββ client-2/ # Another tenant
βββ ...
Tenant Configuration
Each tenant has its own .env for runtime settings:
# sites/client-1/.env
ID=client-1
NAME=client-1
PORT=3001
DOMAIN=app1.com
NODE_ENV=production
The database connection lives separately in sites/client-1/config/db.config.json:
{
"dialect": "mysql",
"connection": {
"host": "localhost",
"port": "3306",
"user": "client1",
"password": "secret"
},
"database": "db_a1b2c3d4e5f6a7b8"
}
With no db config, the tenant runs on zero-config SQLite (sites/<tenant>/database/).
Use Cases
| Scenario | Setup |
|---|---|
| SaaS Product | One tenant per customer |
| Agency | One tenant per client project |
| Enterprise | Separate tenants for departments |
| Staging | Dev, Staging, Production tenants |
Key Benefits
| Benefit | Description |
|---|---|
| Data Isolation | Each client's data is completely separate |
| Custom Domains | Professional URLs for each client |
| Independent Scaling | Scale individual tenants as needed |
| Easy Deployment | Deploy updates to all tenants at once |
| Cost Efficient | Single server, multiple clients |
API & Routes
Every URL in Loopar follows one convention β the action comes from the path, not from the HTTP verb:
/{workspace}/{Entity}/{action}
| Workspace | Purpose |
|---|---|
(none) / web | Public website pages |
desk | Admin UI (login required) |
api | JSON responses (login required, except public actions) |
portal | Authenticated end-user app |
auth | Login / register / recovery |
Built-in Actions
For an entity named Customer, these inherited actions are available:
| URL | Action |
|---|---|
/api/Customer/list | Paginated rows (JSON) |
/api/Customer/view?name=X | Single record |
/api/Customer/create | Create (POST fields as body) |
/api/Customer/update?name=X | Update (POST fields as body) |
/api/Customer/delete?name=X | Delete |
/api/Customer/bulkDelete | Delete many (names in body) |
The same actions under /desk/... render the admin UI instead of JSON.
Custom Actions
Add custom endpoints in the entity's controller file (customer-controller.js):
import { BaseController, loopar } from "loopar";
export default class CustomerController extends BaseController {
// Creates: GET/POST /api/Customer/sendWelcomeEmail
async actionSendWelcomeEmail() {
const customer = await loopar.getDocument("Customer", this.data.name);
await loopar.mail.send({
to: customer.email,
subject: "Welcome!",
html: "<h1>Welcome!</h1>"
});
return this.success("Email sent successfully");
}
// Creates: GET/POST /api/Customer/stats
async actionStats() {
const total = await loopar.db.count("Customer");
const active = await loopar.db.count("Customer", { status: "Active" });
return { total, active, inactive: total - active };
}
}
The URL segment is the literal method name without the
actionprefix βactionSendWelcomeEmailβ/api/Customer/sendWelcomeEmail. There is no kebab-case conversion. UsepublicAction*for endpoints that must work without login.
Calling Actions from the Client
Use the client loopar object instead of raw fetch β it handles the CSRF header and cookies for you:
import { loopar } from "loopar";
// Invoke a controller action β always POST /{Document}/{action}
const stats = await loopar.call("Customer", "stats");
// With data (JSON body)
const r = await loopar.call("Customer", "sendWelcomeEmail", {
body: { name: "CUST-001" }
});
Page Routes
Pages get their own routes automatically:
| Page Name | Route |
|---|---|
homepage | /homepage |
about-us | /about-us |
contact | /contact |
Desk Routes
Internal desk routes follow the same Entity/action pattern:
| Route | View |
|---|---|
/desk/Customer/list | List view |
/desk/Customer/create | New record form |
/desk/Customer/update?name=X | Edit record form |
/desk/Customer/view?name=X | Read-only view |
Database
Loopar uses a Knex-powered data layer to support multiple database backends. The schema is generated automatically from your entity definitions.
Supported Databases
| Database | Best For |
|---|---|
| MySQL | Production, high performance |
| PostgreSQL | Advanced features, JSON support |
| MariaDB | MySQL alternative |
| SQLite | Development, small deployments |
| SQL Server | Microsoft-based infrastructure |
| Oracle | Enterprise Oracle environments |
Automatic Schema Management
When you create or modify an entity:
- Loopar reads the element definitions from the entity's
doc_structure - Compares with the current database schema
- Generates the DDL through Knex
- Applies changes automatically
Entity Definition β Schema Diff β Migration β Database
Format to Column Mapping
Column types come from the element (or an input's format):
| Format / Element | MySQL | PostgreSQL |
|---|---|---|
data (input) | VARCHAR(255) | VARCHAR(255) |
text / textarea | TEXT / LONGTEXT | TEXT |
int / integer | INT | INTEGER |
float | FLOAT | REAL |
currency | DECIMAL(18,4) | NUMERIC(18,4) |
date | DATE | DATE |
datetime / date_time | TIMESTAMP | TIMESTAMP |
checkbox / switch | TINYINT | INTEGER |
json / metadata | JSON | JSON |
jsonb | JSON | JSONB |
uuid | CHAR(36) | UUID |
Custom formats can be added at runtime with registerColumnFormat(name, def).
Database API
Access the database in server-side code:
import { loopar } from "loopar";
// Get a document
const customer = await loopar.getDocument("Customer", "CUST-001");
// Create a document
const newCustomer = await loopar.newDocument("Customer");
newCustomer.name = "John Doe";
newCustomer.email = "john@example.com";
await newCustomer.save();
// Query documents
const customers = await loopar.db.getAll(
"Customer",
["name", "email"],
{ status: "Active" }
);
// Count documents
const count = await loopar.db.count("Customer", { status: "Active" });
// Raw SQL (when needed)
const results = await loopar.db.raw(
"SELECT * FROM Customer WHERE created_at > ?",
["2024-01-01"]
);
Relationships
Select from another Entity (Many-to-One)
A select element whose options is an entity name becomes a reference to that entity:
{ "element": "select", "data": { "name": "customer", "options": "Customer" } }
Child Tables (One-to-Many)
A form_table element creates a child entity whose rows carry a parent reference:
{ "element": "form_table", "data": { "name": "items", "options": "Invoice Item" } }
Database Per Tenant
Each tenant has its own database, configured in sites/<tenant>/config/db.config.json:
βββββββββββββββ βββββββββββββββ βββββββββββββββ
β Tenant A β β Tenant B β β Tenant C β
β β β β β β
β βββββββββ β β βββββββββ β β βββββββββ β
β β MySQL β β β β MySQL β β β βSQLite β β
β β db_a β β β β db_b β β β β db_c β β
β βββββββββ β β βββββββββ β β βββββββββ β
βββββββββββββββ βββββββββββββββ βββββββββββββββ
Each tenant can even use a different database type. SQLite is the zero-config default (sites/<tenant>/database/<tenant>.sqlite).
History & Comments
Any entity can carry an audit trail and a comment thread β enable them with two switches on the entity definition:
| Entity flag | Effect |
|---|---|
enable_history | Every create/update/delete writes an immutable Document History row (who, when, per-field diff) |
enable_comments | The document gets a comment thread (desk sidebar; also embeddable on public web pages) |
require_login_to_comment | Guests can't comment β only authenticated users |
Audit trail (automatic)
History is captured at the ORM level, so it works no matter how the write happens. Each row records:
| Field | Content |
|---|---|
action | Created, Updated, Deleted or Restored |
user / event_at | Who and when |
diff | Per-field before/after (only for updates that changed something) |
Read it in the desk sidebar of the document, or via the permission-gated history action (/desk/Entity/history?documentName=X).
Comments & moderation
Comments are polymorphic (document + document_name) and threaded (parent). Moderation uses the document-status codes: authenticated comments enter as Approved, guest comments as Pending.
| Action | Access | Purpose |
|---|---|---|
addComment | Logged-in or guest (unless require_login_to_comment) | Post a comment (max 2000 chars) |
comments | Public | Approved-only thread for a document |
moderate | Desk only | Approve / reject a comment |
history | Permission-gated | Audit rows + comments interleaved |
Site-wide moderation lives in Comment Manage (a virtual entity over the Comment table): search, review and approve/reject everything from one list.
Comments on public pages
The collection_view element can render a public comment block on detail pages (e.g. project pages) β guests post as Pending and appear once approved.
Programmatic API
await loopar.comments.add({
document: "Project",
document_name: "my-project",
comment: "Nice work!",
status: loopar.comments.STATUS.APPROVED
});
Theming
Loopar uses Tailwind CSS with CSS variables for a flexible theming system. Themes can be changed dynamically without rebuilds.
Theme System
Loopar supports:
- Light/Dark modes β Automatic switching
- Custom color schemes β Define your brand colors
- Per-tenant themes β Each tenant can have its own theme
- Dynamic generation β Create themes programmatically
CSS Variables
Themes are defined using CSS custom properties:
:root {
--background: 0 0% 100%;
--foreground: 222 84% 5%;
--primary: 221 83% 53%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96%;
--muted: 210 40% 96%;
--accent: 210 40% 96%;
--destructive: 0 84% 60%;
--border: 214 32% 91%;
--radius: 0.5rem;
}
.dark {
--background: 222 84% 5%;
--foreground: 210 40% 98%;
/* ... dark mode colors */
}
Using Theme Colors
In your components, use Tailwind classes:
// Background colors
<div className="bg-background" />
<div className="bg-primary" />
<div className="bg-secondary" />
// Text colors
<p className="text-foreground" />
<p className="text-muted-foreground" />
<p className="text-primary" />
// Border colors
<div className="border border-border" />
Built-in Themes
Loopar includes several pre-built themes:
| Theme | Description |
|---|---|
| Default | Clean, professional blue |
| Slate | Neutral gray tones |
| Rose | Warm pink accent |
| Orange | Energetic orange |
| Green | Fresh green accent |
| Violet | Purple tones |
Creating Custom Themes
Define a new theme in your app:
// Custom theme definition
const myTheme = {
name: "corporate",
colors: {
primary: { h: 210, s: 100, l: 40 }, // Corporate blue
secondary: { h: 210, s: 20, l: 95 },
accent: { h: 45, s: 100, l: 50 }, // Gold accent
background: { h: 0, s: 0, l: 100 },
foreground: { h: 210, s: 50, l: 10 },
}
};
Dark Mode
Dark mode is automatic based on:
- User preference in settings
- System preference (
prefers-color-scheme) - Manual toggle
// Toggle dark mode
import { useWorkspace } from "@workspace/workspace-provider";
function ThemeToggle() {
const { theme, setTheme } = useWorkspace();
return (
<button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
Toggle Theme
</button>
);
}
Glossary
Quick reference for Loopar terminology.
| Term | Definition |
|---|---|
| Entity | The core model definition that generates database tables, APIs, and UI |
| Builder | Specialized entity type for creating specific model types |
| App | Self-contained application package with modules and entities |
| Module | Organizational unit that groups related entities |
| Document | Instance of an entity (a record in the database) |
| Single | Entity type that stores only one record (for settings/config) |
| Tenant | Independent site with own database and configuration |
| Desk | The admin interface for managing entities and data |
| Component | UI element used in drag-and-drop design |
| Field | Data attribute defined in an entity |
| Hook | Lifecycle callback (beforeSave, afterInsert, etc.) |
| Controller | Server-side class handling business logic |
| Action | Custom API endpoint defined in a controller |
| Link | Field type referencing another entity (foreign key) |
| Table | Field type for child records (one-to-many) |
| PM2 | Process manager running all tenant sites |
| Caddy | Reverse proxy for SSL and domain routing |
Common Patterns
| Pattern | Example |
|---|---|
| Entity name | Customer, Sales Invoice, Stock Entry |
| Field name | customer_name, total_amount, is_active |
| Module name | core, sales, inventory |
| App name | my-crm, loopar, hr-management |
| Route | /desk/Customer/list, /api/Customer |
File Extensions
| Extension | Purpose |
|---|---|
.json | Entity/component definitions |
.js | Server-side controller |
.jsx | Client-side React component |
.env | Environment configuration |
.mjs | ES Module JavaScript |