Collections & Schemas
Understand Moul collections, schema modeling, collection types, and the 11 supported dynamic field types.
In Moul, a Collection (internally called a Moul) represents a dynamic SQLite database table together with its schema, field validation constraints, and access control rules.
Collections can be created, updated, inspected, or deleted dynamically at runtime via the HTTP API, the Web Admin Console (/_moul_/), the TUI (moul), or AI assistants via MCP without restarting the server.
Collection Types
Moul supports four distinct collection types tailored for different application architectures:
| Type | Description | Built-in Capabilities |
|---|---|---|
base | Standard dynamic relational database table. | Dynamic CRUD, custom fields, HCL access rules, webhooks, SSE. |
auth | User account & authentication collection. | Automatic password hashing, Email OTP, Passkey WebAuthn, OAuth2 social login, session refresh/logout. |
worker | Asynchronous background job queue. | Oban-style job dispatch, state transitions, automatic retries with exponential backoff, priority sorting. |
analytic | First-party visitor and event tracking table. | Automatic client header parsing (IP, User-Agent, Referrer, UTM), visitor session deduplication, optional GeoIP resolution. |
Supported Dynamic Field Types
Moul supports 11 dynamic field types with automatic SQLite column mapping, runtime validation, and OpenAPI 3.0 type generation:
| Field Type | Description | SQLite Storage | Validation Constraints | OpenAPI Type / Format |
|---|---|---|---|---|
text | Character string data | TEXT | min, max length, pattern (regex) | type: string, minLength, maxLength |
number | Numeric values (integer/float) | NUMERIC | min, max numeric bounds | type: number, minimum, maximum |
bool | Boolean flag (true/false) | INTEGER (1/0) | Validates boolean or 1/0 | type: boolean |
date | Calendar date | TEXT | Enforces YYYY-MM-DD ISO format | type: string, format: date |
datetime | Timestamp with timezone | TEXT | Enforces ISO 8601 / RFC 3339 format | type: string, format: date-time |
json | Arbitrary JSON object/array | TEXT | Enforces valid JSON syntax | type: object |
url | Web URL string | TEXT | Enforces valid HTTP/HTTPS URI | type: string, format: uri |
file | Uploaded file or S3 key | TEXT | File size bounds, MIME types | type: string |
select | Constrained enum string | TEXT | Must match one of options array | type: string, enum: [...] |
relation | Foreign key association | TEXT | Validates target record ID exists | type: string or type: array |
cloak | Authenticated encrypted binary with optional blind index | BLOB | Validates secret key; AES-256-GCM | type: string |
Encrypted Fields (cloak) & Blind Indexing
The cloak field type provides application-layer authenticated encryption at rest for sensitive data (PII, SSNs, access tokens, credit cards):
- Encryption Standard: AES-256-GCM with a 12-byte random nonce and 16-byte authentication tag (
[0x01][nonce][ciphertext + tag]). Keys are derived via HKDF-SHA256 from theMOUL_ENCRYPTION_KEYenvironment variable. - Fail-Safe Startup: If any collection in the database defines a
cloakfield andMOUL_ENCRYPTION_KEYis not provided, the engine refuses to boot to prevent unrecoverable data loss or unauthorized reads. - Masking by Default: Record reads mask
cloakvalues automatically (••••+ last 4 characters, or••••if 4 or fewer characters). Authenticated administrators always see decrypted values. Other authenticated consumers can pass?reveal=fieldName(or?reveal=all) to request unmasked plaintext. - Searchable Blind Indexing: Because ciphertext is encrypted nondeterministically with unique nonces, direct search on encrypted binary is impossible. Setting
"searchable": trueinstructs Moul to maintain an automatic, indexed companion column (<fieldName>Hash TEXT) populated with an HMAC-SHA256 blind index.- Query Restrictions: Exact equality queries (
field = 'value'andfield != 'value') are seamlessly translated into companion hash lookups. Pattern matches (~,!~) and range comparisons (>,<) are strictly rejected with HTTP 400 Bad Request to prevent security misconfigurations. - Security Assurance: Companion hash columns are internal SQLite storage details and are completely stripped from all API outputs.
- Query Restrictions: Exact equality queries (
Field Naming & System Timestamps
Strict camelCase Naming
All custom field names in a collection must strictly follow camelCase (matching regex ^[a-z][a-zA-Z0-9]*$, e.g. authorId, viewsCount, isFeatured).
Field names containing underscores (e.g. author_id), dashes, spaces, uppercase initial letters, or reserved keywords are rejected with HTTP 400 Bad Request.
Universal Timestamps
Every collection table automatically creates and maintains createdAt and updatedAt ISO 8601 UTC timestamp columns. These system fields are automatically stamped on record creation and update:
createdAt— RFC 3339 UTC timestamp of initial record creation.updatedAt— RFC 3339 UTC timestamp of most recent update.
Reserved field names (id, createdAt, updatedAt, createdat, updatedat) cannot be used as custom field names.
Schema Management API
Create a Collection (POST /api/moul)
Schema creation and management endpoints require administrative authorization via either:
_rootUsersAdmin JWT Bearer Token:Authorization: Bearer <root_user_token>(obtained viaPOST /api/moul/_rootUsers/auth-with-password).- Administrative Master Key:
X-Admin-Key: <MOUL_ADMIN_KEY>(orAuthorization: Bearer <MOUL_ADMIN_KEY>).
curl -X POST "http://localhost:8090/api/moul" \
-H "Authorization: Bearer <root_user_token>" \
-H "Content-Type: application/json" \
-d '{
"name": "articles",
"type": "base",
"fields": [
{ "name": "title", "type": "text", "required": true, "options": { "min": 3, "max": 200 } },
{ "name": "slug", "type": "text", "required": true, "unique": true },
{ "name": "content", "type": "text", "required": false },
{ "name": "views", "type": "number", "options": { "min": 0 } },
{ "name": "published", "type": "bool" }
],
"rules": {
"listRule": "published = true || @request.auth.id != \"\"",
"viewRule": "published = true || @request.auth.id != \"\"",
"createRule": "@request.auth.id != \"\"",
"updateRule": "@request.auth.id != \"\"",
"deleteRule": "@request.auth.role = \"admin\""
}
}'curl -X POST "http://localhost:8090/api/moul" \
-H "X-Admin-Key: test-admin-key-1234" \
-H "Content-Type: application/json" \
-d '{
"name": "articles",
"type": "base",
"fields": [
{ "name": "title", "type": "text", "required": true, "options": { "min": 3, "max": 200 } },
{ "name": "slug", "type": "text", "required": true, "unique": true },
{ "name": "content", "type": "text", "required": false },
{ "name": "views", "type": "number", "options": { "min": 0 } },
{ "name": "published", "type": "bool" }
]
}'const response = await fetch('http://localhost:8090/api/moul', {
method: 'POST',
headers: {
'X-Admin-Key': 'test-admin-key-1234',
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'articles',
type: 'base',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'slug', type: 'text', required: true, unique: true },
{ name: 'content', type: 'text' },
{ name: 'published', type: 'bool' },
],
rules: {
listRule: 'published = true',
},
}),
});
const collection = await response.json();
console.log('Collection created:', collection);package main
import (
"bytes"
"encoding/json"
"net/http"
)
func main() {
payload := map[string]any{
"name": "articles",
"type": "base",
"fields": []map[string]any{
{"name": "title", "type": "text", "required": true},
{"name": "published", "type": "bool"},
},
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "http://localhost:8090/api/moul", bytes.NewReader(body))
req.Header.Set("X-Admin-Key", "test-admin-key-1234")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
}Collection Management Endpoints
GET /api/moul- List all dynamic collections.GET /api/moul/:name- Get schema definition, field types, and rules for a collection.PATCH /api/moul/:name- Update fields, schema, or access rules.DELETE /api/moul/:name- Delete a collection and drop its SQLite table.