Configuration-Driven Application Framework for Data-Intensive Web Apps
Thatcher is a complete extraction of the moonlanding application framework. Build full-featured CRUD applications with workflows, permissions, and external integrations — all through YAML configuration, without writing code.
- Zero-Code CRUD — Define entities in YAML; automatic REST API, UI, and database schema
- Workflow Engine — State machine with transitions, locks, and permissions
- Role-Based Access Control — Fine-grained permission templates per entity
- Plugin System — Extend entities with hooks, validators, and custom fields
- External Integrations — Google Drive/Gmail, Email (SMTP), PDF generation
- Hot Reload — Config changes reflect instantly without restart
- Production-Ready — busybase (LanceDB) document store, vector search, metrics, audit logging
# 1. Install globally (or use bun x)
npm install -g thatcher
# 2. Generate starter config
thatcher example
# 3. Start the server
thatcher startOr use directly with Bun:
bun x thatcher startThatcher is driven by a single thatcher.config.yml file:
# thatcher.config.yml
roles:
admin:
hierarchy: 0
label: Admin
permissions_scope: global
user:
hierarchy: 1
label: User
permissions_scope: assigned
permission_templates:
basic:
admin: [list, view, create, edit, delete, manage_settings]
user: [list, view]
entities:
item:
label: Item
label_plural: Items
fields:
name:
type: text
required: true
description:
type: textarea
status:
type: enum
options: [active, archived]
default: active
workflows:
simple:
stages:
- draft
- active
- completed
system:
pagination:
default_page_size: 20
max_page_size: 100That's it. Start the server and you have:
- busybase
itemtable (schemaless; created on first insert) - REST endpoints:
GET/POST/PUT/DELETE /api/item - Role-based permissions enforced
- State transitions via
POST /api/item/:id/transition
import { createThatcher } from 'thatcher';
const thatcher = createThatcher({
config: './thatcher.config.yml',
databasePath: './data/app.db',
});
// Initialize (loads config, migrates DB, registers plugins)
await thatcher.init();
// Start HTTP server
await thatcher.startServer({ port: 3000 });
// Use APIs directly
const items = await thatcher.list('item');
const item = await thatcher.get('item', 'id123');
const created = await thatcher.create('item', { name: 'Test' }, { id: 'user1' });
// Workflow transitions
await thatcher.transition('item', item.id, 'simple', 'active', { id: 'user1', role: 'admin' });
// Permissions
const canEdit = await thatcher.can(user, thatcher.getEntitySpec('item'), 'edit');| Command | Description |
|---|---|
thatcher start |
Start server in production mode |
thatcher dev |
Start with hot reload enabled |
thatcher migrate |
Run database migrations only |
thatcher validate |
Validate configuration file |
thatcher console |
Open REPL with thatcher API |
thatcher example |
Generate example config |
thatcher/
├── src/
│ ├── index.js # Main entry: createThatcher()
│ ├── cli.js # CLI commands
│ ├── config/
│ │ ├── config-loader.js # YAML loading & validation
│ │ ├── spec-helpers.js # Entity spec utilities
│ │ ├── env.js # Environment config
│ │ └── constants.js # HTTP codes, statuses
│ ├── lib/
│ │ ├── busybase-store.js # busybase data layer (async CRUD)
│ │ ├── query-engine.js # Read operations (GET, search)
│ │ ├── query-engine-write.js # Write operations (CRUD)
│ │ ├── config-generator-engine.js # Spec builder
│ │ ├── hook-engine.js # Event system
│ │ ├── workflow-engine.js # State machines
│ │ ├── auth-middleware.js # Auth checks
│ │ ├── crud-factory.js # Handler factory
│ │ ├── crud-handlers.js # HTTP handlers
│ │ ├── validate.js # Validation
│ │ └── logger.js # Structured logging
│ ├── services/
│ │ └── permission.service.js # Authorization
│ ├── adapters/
│ │ ├── google-auth.js # Google OAuth
│ │ └── google-drive.js # Drive file operations
│ ├── plugins/
│ │ └── index.js # Plugin auto-discovery
│ └── server/
│ └── server.js # HTTP server
└── package.json
- Configuration Load —
master-config.ymlparsed into memory - Spec Generation — For each entity, ConfigEngine builds full spec from base + overrides + plugins
- Database Migration — Tables created/updated from spec fields (idempotent)
- Plugin Registration —
.plugin.jsfiles extend entity behavior - Request Handling — Generic CRUD handlers enforce permissions, validate, execute hooks
Each entity derives from config:
| Config Key | Purpose |
|---|---|
fields |
Column definitions (type, required, ref, enum, etc.) |
permission_template |
Maps roles → allowed actions |
workflow |
State machine name for lifecycle |
row_access |
Scoping: team, assigned, client |
list.defaultSort |
Default list ordering |
has_* flags |
UI features (PDF, collaboration, notifications) |
Permission templates define role capabilities per entity:
permission_templates:
standard:
admin: [list, view, create, edit, delete, export]
user: [list, view]Actions: list, view, create, edit, delete, archive, export, manage_settings, etc.
Row access controls which records a user can see:
team— Only records in user's teamassigned— Only records assigned to userclient— Only records for user's clientassigned_or_team— Either condition
State machines with transitions:
workflows:
engagement_lifecycle:
state_field: stage
stages:
- name: draft
label: Draft
forward: [review, submitted]
readonly: true
- name: review
label: In Review
forward: [approved, rejected]
backward: [draft]
requires_role: [manager, partner]
- name: closed
label: Closed
entry: partner_onlyTransitions validated automatically:
- Only allowed transitions
- Role requirements
- Lockout period (configurable)
- Readonly state blocks
Listen to lifecycle events:
// my-entity.plugin.js
export default {
entityName: 'item',
hooks: [
{
event: 'create:item:after',
handler: async ({ entity, id, data, user }) => {
console.log(`Item ${id} created by ${user.id}`);
// Send notification, sync external system, etc.
},
},
],
};Hook naming: <timing>:<entity>:<phase>
create:entity:before/afterupdate:entity:before/afterdelete:entity:before/aftertransition:entity:before/after- Custom:
upload_files:entity:after,resolve_highlight:review:after
Extend entities with custom fields and behavior:
// plugins/custom-item.plugin.js
export default {
entityName: 'item',
fields: {
custom_field: {
type: 'text',
label: 'Custom Field',
required: false,
},
},
validators: {
validateCustom: (data) => {
if (data.custom_field && !data.custom_field.startsWith('X')) {
return 'Custom field must start with X';
}
return null;
},
},
};All .plugin.js files in plugins/ directory auto-loaded on startup.
Set env vars:
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
GOOGLE_SERVICE_ACCOUNT_PATH=./service-account.json
GOOGLE_DRIVE_FOLDER_ID=...Drive operations via @/adapters/google-drive:
import { uploadFile, downloadFile, exportToPdf } from 'thatcher/adapters/google-drive';
await uploadFile('./local.pdf', 'Document.pdf', { folderId: '...' });
const pdf = await exportToPdf('docId');EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USER=...
EMAIL_PASSWORD=...
EMAIL_FROM=noreply@example.comUse via @/lib/email-sender (included).
All APIs accessible via thatcher instance:
thatcher.list(entity, where, opts)→ Arraythatcher.get(entity, id)→ objectthatcher.create(entity, data, user)→ objectthatcher.update(entity, id, data, user)→ objectthatcher.delete(entity, id)→ void
thatcher.search(entity, query, where, opts)→ Array (uses FTS)
thatcher.transition(entityType, entityId, workflowName, toState, user, reason)→ objectthatcher.getAvailableTransitions(workflowName, currentState, user, record)→ Array
thatcher.can(user, spec, action)→ booleanthatcher.requirePermission(user, spec, action)→ throws if denied
thatcher.getConfigEngine()→ ConfigGeneratorEnginethatcher.getEntitySpec(entityName)→ spec objectthatcher.getAllEntities()→ Array
thatcher.withTransaction(cb)→ Promise
| Variable | Purpose | Default |
|---|---|---|
PORT |
Server port | 3000 |
BUSYBASE_DIR |
busybase data directory | busybase_data |
NODE_ENV |
development | production |
development |
DEBUG |
Enable debug logging | false |
GOOGLE_CLIENT_ID |
OAuth client ID | — |
GOOGLE_CLIENT_SECRET |
OAuth client secret | — |
GOOGLE_DRIVE_FOLDER_ID |
Root Drive folder | — |
EMAIL_HOST |
SMTP host | smtp.gmail.com |
EMAIL_PORT |
SMTP port | 587 |
EMAIL_USER |
SMTP username | — |
EMAIL_PASSWORD |
SMTP password | — |
Thatcher is published to npm as thatcher. Every push to main triggers:
- Bump version (from git tags or conventional commits)
- Build & test
- Publish to npm registry
- Create GitHub release
CI/CD ready via included GitHub Actions workflow.
MIT — Extracted from moonlanding with all functionality preserved.