Moul

Records & Dynamic CRUD

Create, read, update, filter, paginate, sort, and delete dynamic collection records via REST API.

Moul provides instant, fully typed REST API endpoints for every dynamic collection. Record operations are validated against the schema and checked against the collection's access rules.


Query Parameters

List queries (GET /api/moul/:name/records) accept the following standard query parameters:

ParameterTypeDefaultDescriptionExample
pageinteger11-indexed page number.page=2
perPageinteger30Number of records per page (max 500).perPage=50
sortstring-createdComma-separated sort fields. Prefix with - for descending.sort=-views,title
filterstring""Filter expression evaluated against SQLite records.filter=published=true && views>100
expandstring""Comma-separated relation field names to expand inline.expand=authorId,categories
fieldsstring*Comma-separated fields to return in the payload.fields=id,title,createdAt
revealstring""Comma-separated cloak field names (or all / *) to unmask decrypted plaintext for authenticated users.reveal=ssn,apiKey

Standard List Response Format

{
  "page": 1,
  "perPage": 30,
  "totalItems": 142,
  "totalPages": 5,
  "items": [
    {
      "id": "rec_01J6XYZ123",
      "title": "Getting Started with Moul",
      "slug": "getting-started-with-moul",
      "published": true,
      "views": 420,
      "createdAt": "2026-08-01T12:00:00Z",
      "updatedAt": "2026-08-02T15:30:00Z"
    }
  ]
}

CRUD Operations

1. List Records (GET /api/moul/:name/records)

curl -s "http://localhost:8090/api/moul/posts/records?page=1&perPage=20&sort=-created&filter=published=true"
const params = new URLSearchParams({
  page: '1',
  perPage: '20',
  sort: '-created',
  filter: 'published = true',
});

const res = await fetch(`http://localhost:8090/api/moul/posts/records?${params}`);
const data = await res.json();
console.log(`Found ${data.totalItems} posts:`, data.items);
req, _ := http.NewRequest("GET", "http://localhost:8090/api/moul/posts/records?page=1&perPage=20", nil)
resp, err := http.DefaultClient.Do(req)
// parse JSON...

2. View Single Record (GET /api/moul/:name/records/:id)

curl -s "http://localhost:8090/api/moul/posts/records/rec_01J6XYZ123"

3. Create Record (POST /api/moul/:name/records)

curl -X POST "http://localhost:8090/api/moul/posts/records" \
  -H "Authorization: Bearer <user_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Deploying on Bare Metal",
    "slug": "deploying-on-bare-metal",
    "content": "A complete walk-through...",
    "published": true
  }'
const res = await fetch('http://localhost:8090/api/moul/posts/records', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${userToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    title: 'Deploying on Bare Metal',
    slug: 'deploying-on-bare-metal',
    content: 'A complete walk-through...',
    published: true,
  }),
});
const record = await res.json();
payload := map[string]any{
	"title":     "Deploying on Bare Metal",
	"slug":      "deploying-on-bare-metal",
	"published": true,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "http://localhost:8090/api/moul/posts/records", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+userToken)
req.Header.Set("Content-Type", "application/json")

4. Update Record (PATCH /api/moul/:name/records/:id)

curl -X PATCH "http://localhost:8090/api/moul/posts/records/rec_01J6XYZ123" \
  -H "Authorization: Bearer <user_token>" \
  -H "Content-Type: application/json" \
  -d '{"views": 500}'

5. Delete Record (DELETE /api/moul/:name/records/:id)

curl -X DELETE "http://localhost:8090/api/moul/posts/records/rec_01J6XYZ123" \
  -H "Authorization: Bearer <user_token>"
# Returns HTTP 204 No Content on success

Encrypted Cloak Fields in Record CRUD

Collections with cloak fields allow storing sensitive data with end-to-end server encryption:

Creating & Updating Records

Submit plaintext strings in your JSON payload as normal. Moul automatically derives AES-256-GCM and HMAC keys, encrypts the value, and stores companion blind index hashes transparently:

curl -X POST "http://localhost:8090/api/moul/customers/records" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice Smith", "ssn": "123-45-6789"}'

Reading & Revealing Plaintext

By default, record reads return masked strings (•••• + last 4 characters) to prevent accidental exposure in UI tables, logs, and client bundles:

{
  "id": "rec_123",
  "name": "Alice Smith",
  "ssn": "••••6789"
}

To unmask specific fields, append ?reveal=<fieldName> or ?reveal=all:

curl "http://localhost:8090/api/moul/customers/records/rec_123?reveal=ssn" \
  -H "Authorization: Bearer <token>"

Administrators (authenticated via master X-Admin-Key or _rootUsers token) always receive decrypted unmasked values automatically.

Exact Match Filtering via Blind Indexes

If searchable: true was configured on the schema, filter expressions can perform exact equality searches:

curl "http://localhost:8090/api/moul/customers/records?filter=ssn = '123-45-6789'" \
  -H "Authorization: Bearer <token>"

Substring searches (~, !~) and range operators (>, <, >=, <=) are strictly forbidden on cloak fields and will return HTTP 400 Bad Request. Companion hash columns (ssnHash) are internal database details and are never exposed in API payloads.

On this page