Loopar Concepts
loopar-webpage

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

ConceptDescription
EntityThe master builder β€” defines data models and generates everything
BuilderSpecialized entity types (Page, View, Form, Controller)
AppA self-contained application package
ModuleGroups related entities within an app
DocumentA data-centric entity (stored in database)
PageA presentation-centric entity (web pages)
ComponentUI 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:

  1. Entity defines the structure of all models
  2. Entity creates Builders (specialized entity types)
  3. Builders create specific model types (Pages, Forms, Views)
  4. 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:

GeneratedDescription
Database TableWith proper schema and automatic migrations
Model ClassBaseDocument subclass with hooks & validations
HTTP Actionslist, view, create, update, delete... under /api and /desk
React FormWith all field types rendered
List ViewWith search, filters, and pagination
ControllerBaseController 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

Typeis_singleDescription
Document0Multiple records (customers, products)
Single1One 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.

FeatureDescription
PurposeDesign full web pages visually
OutputStandalone page with route
ComponentsSections, rows, columns, text, images, etc.
Use CasesHomepage, 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.

FeatureDescription
PurposeVisualize data from an entity
OutputConnected view component
ComponentsTables, cards, charts, grids
Use CasesDashboards, 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.

FeatureDescription
PurposeCollect and submit data
OutputForm that submits to a controller
ComponentsInputs, selects, checkboxes, buttons
Use CasesLogin, 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.

FeatureDescription
PurposeHandle specific backend actions
OutputAPI routes and actions
ComponentsN/A (server-side only)
Use CasesAuth 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

NeedBuilder
Database model with CRUDEntity
Web page designPage Builder
Display existing dataView Builder
Standalone formForm Builder
Custom API endpointsController 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:

ActionDescription
CreateStart a new app
InstallInstall app on current tenant
UninstallRemove app from tenant
UpdateUpdate to newer version
ImportInstall from a git repository

Default App Creation

When you create an App, Loopar automatically:

  1. Creates the app directory structure
  2. Generates a module with the same name
  3. Sets up the installer manifest
  4. 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

  1. Go to Desk β†’ Module β†’ New
  2. Enter module details:
  • Name: Module identifier (e.g., sales)
  • Label: Display name (e.g., Sales)
  • App: Parent app
  • Icon: Module icon for the sidebar
  1. Save the module

Module vs App

AspectAppModule
ScopeComplete applicationFunctional group
ContainsModules, assets, configEntities only
InstallableYesNo (part of app)
ExampleCRM AppSales 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)

ElementDescriptionDefault Column
inputSingle line text (set format for email, int, decimal...)VARCHAR(255)
textareaMulti-line textLONGTEXT
integerInteger numberINTEGER
decimalDecimal numberDECIMAL (default 10,2)
currencyMoney valuesDECIMAL (default 18,4; precision/scale configurable)
dateDate onlyDATE
date_timeDate and timeTIMESTAMP
timeTime onlyTIME
checkbox / switchBoolean (yes/no)INTEGER (0/1)
selectDropdown β€” static options or another entityTEXT
form_tableChild table (one-to-many)β€” (child rows)
passwordHashed passwordTEXT
color_pickerColor pickerTEXT
image_input / file_input / file_uploaderUploads (managed by File Manager)LONGTEXT (ref)
markdown_inputMarkdown editorTEXT
text_editorRich text editorLONGTEXT
designerNested visual structureLONGTEXT
metadataJSON dataJSON
idAuto-increment primary keyINCREMENTS

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):

ElementDescription
sectionFull-width container
rowHorizontal container with columns
colColumn within a row
cardCard-like container
div / genericGeneric containers
tabs / tabTabbed content
panelStyled container
banner / banner_imageHero banners
contact_form / formEmbedded forms

Design Elements

ElementDescription
title / subtitle / paragraphText hierarchy
text_block / text_block_iconText compositions
image / gallery / carousel / sliderMedia
iconLucide icon
button / linkActions
markdown / html_blockRendered content
feature_cardFeature highlight card
reviewReviews block
collection / collection_viewData-driven collections
stripe / stripe_embebed / stripe_plansStripe payments
particlesAnimated particle background
seoPage SEO metadata

Element Properties

Common properties in an element's data:

PropertyDescription
nameField identifier (snake_case)
labelDisplay label
formatColumn/validation format (on input)
requiredMust have a value (1)
uniqueMust be unique in database (1)
default_valueDefault value
hiddenHide from UI (1)
readonlyCannot be edited (1)
optionsFor select: newline-separated values, or an entity name
descriptionHelp text
classCSS 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)
FileRuns OnPurpose
.jsonBothElement definitions, metadata
.jsServerBusiness logic, hooks, database
-controller.jsServerHTTP actions (action* / publicAction*)
client/*.jsxClientUI 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) ForUse Controller ForUse Client (.jsx) For
Data validationCustom endpointsUI customization
Business rulesAccess controlUser interactions
Computed fieldsResponse shapingForm behavior
Hooks on save/deleteWebhooks (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

ScenarioSetup
SaaS ProductOne tenant per customer
AgencyOne tenant per client project
EnterpriseSeparate tenants for departments
StagingDev, Staging, Production tenants

Key Benefits

BenefitDescription
Data IsolationEach client's data is completely separate
Custom DomainsProfessional URLs for each client
Independent ScalingScale individual tenants as needed
Easy DeploymentDeploy updates to all tenants at once
Cost EfficientSingle 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}
WorkspacePurpose
(none) / webPublic website pages
deskAdmin UI (login required)
apiJSON responses (login required, except public actions)
portalAuthenticated end-user app
authLogin / register / recovery

Built-in Actions

For an entity named Customer, these inherited actions are available:

URLAction
/api/Customer/listPaginated rows (JSON)
/api/Customer/view?name=XSingle record
/api/Customer/createCreate (POST fields as body)
/api/Customer/update?name=XUpdate (POST fields as body)
/api/Customer/delete?name=XDelete
/api/Customer/bulkDeleteDelete 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 action prefix β€” actionSendWelcomeEmail β†’ /api/Customer/sendWelcomeEmail. There is no kebab-case conversion. Use publicAction* 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 NameRoute
homepage/homepage
about-us/about-us
contact/contact

Desk Routes

Internal desk routes follow the same Entity/action pattern:

RouteView
/desk/Customer/listList view
/desk/Customer/createNew record form
/desk/Customer/update?name=XEdit record form
/desk/Customer/view?name=XRead-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

DatabaseBest For
MySQLProduction, high performance
PostgreSQLAdvanced features, JSON support
MariaDBMySQL alternative
SQLiteDevelopment, small deployments
SQL ServerMicrosoft-based infrastructure
OracleEnterprise Oracle environments

Automatic Schema Management

When you create or modify an entity:

  1. Loopar reads the element definitions from the entity's doc_structure
  2. Compares with the current database schema
  3. Generates the DDL through Knex
  4. 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 / ElementMySQLPostgreSQL
data (input)VARCHAR(255)VARCHAR(255)
text / textareaTEXT / LONGTEXTTEXT
int / integerINTINTEGER
floatFLOATREAL
currencyDECIMAL(18,4)NUMERIC(18,4)
dateDATEDATE
datetime / date_timeTIMESTAMPTIMESTAMP
checkbox / switchTINYINTINTEGER
json / metadataJSONJSON
jsonbJSONJSONB
uuidCHAR(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 flagEffect
enable_historyEvery create/update/delete writes an immutable Document History row (who, when, per-field diff)
enable_commentsThe document gets a comment thread (desk sidebar; also embeddable on public web pages)
require_login_to_commentGuests 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:

FieldContent
actionCreated, Updated, Deleted or Restored
user / event_atWho and when
diffPer-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.

ActionAccessPurpose
addCommentLogged-in or guest (unless require_login_to_comment)Post a comment (max 2000 chars)
commentsPublicApproved-only thread for a document
moderateDesk onlyApprove / reject a comment
historyPermission-gatedAudit 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:

ThemeDescription
DefaultClean, professional blue
SlateNeutral gray tones
RoseWarm pink accent
OrangeEnergetic orange
GreenFresh green accent
VioletPurple 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:

  1. User preference in settings
  2. System preference (prefers-color-scheme)
  3. 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.


TermDefinition
EntityThe core model definition that generates database tables, APIs, and UI
BuilderSpecialized entity type for creating specific model types
AppSelf-contained application package with modules and entities
ModuleOrganizational unit that groups related entities
DocumentInstance of an entity (a record in the database)
SingleEntity type that stores only one record (for settings/config)
TenantIndependent site with own database and configuration
DeskThe admin interface for managing entities and data
ComponentUI element used in drag-and-drop design
FieldData attribute defined in an entity
HookLifecycle callback (beforeSave, afterInsert, etc.)
ControllerServer-side class handling business logic
ActionCustom API endpoint defined in a controller
LinkField type referencing another entity (foreign key)
TableField type for child records (one-to-many)
PM2Process manager running all tenant sites
CaddyReverse proxy for SSL and domain routing

Common Patterns

PatternExample
Entity nameCustomer, Sales Invoice, Stock Entry
Field namecustomer_name, total_amount, is_active
Module namecore, sales, inventory
App namemy-crm, loopar, hr-management
Route/desk/Customer/list, /api/Customer

File Extensions

ExtensionPurpose
.jsonEntity/component definitions
.jsServer-side controller
.jsxClient-side React component
.envEnvironment configuration
.mjsES Module JavaScript