{"id":39173111,"url":"https://github.com/hegner123/modulacms","last_synced_at":"2026-04-14T13:01:28.522Z","repository":{"id":308034877,"uuid":"879792224","full_name":"hegner123/modulacms","owner":"hegner123","description":"Headless CMS built in Go. Single binary, tri-database, developer-first. Built on three  values: Performance, Flexibility, and Transparency.","archived":false,"fork":false,"pushed_at":"2026-04-13T10:57:22.000Z","size":157805,"stargazers_count":0,"open_issues_count":42,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-04-13T12:23:43.148Z","etag":null,"topics":["cms","go","headless-cms","mcp","tui"],"latest_commit_sha":null,"homepage":"https://modulacms.com","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hegner123.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-10-28T14:57:11.000Z","updated_at":"2026-04-13T10:57:28.000Z","dependencies_parsed_at":"2025-08-03T19:32:49.227Z","dependency_job_id":"13ad13e6-3808-4589-9d53-0b153d23008d","html_url":"https://github.com/hegner123/modulacms","commit_stats":null,"previous_names":["hegner123/modulacms"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/hegner123/modulacms","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hegner123%2Fmodulacms","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hegner123%2Fmodulacms/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hegner123%2Fmodulacms/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hegner123%2Fmodulacms/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hegner123","download_url":"https://codeload.github.com/hegner123/modulacms/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hegner123%2Fmodulacms/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31797376,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-14T11:13:53.975Z","status":"ssl_error","status_checked_at":"2026-04-14T11:13:53.299Z","response_time":153,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cms","go","headless-cms","mcp","tui"],"created_at":"2026-01-17T22:25:04.521Z","updated_at":"2026-04-14T13:01:28.506Z","avatar_url":"https://github.com/hegner123.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Modula\n\n[![CI/CD](https://github.com/hegner123/modulacms/actions/workflows/go.yml/badge.svg)](https://github.com/hegner123/modulacms/actions/workflows/go.yml)\n[![Windows](https://github.com/hegner123/modulacms/actions/workflows/windows.yml/badge.svg)](https://github.com/hegner123/modulacms/actions/workflows/windows.yml)\n\nA headless CMS written in Go, built on three core values: **performance**, **flexibility**, and **transparency**.\n\n\n## Requirements\n\n| Requirement | Details |\n|-------------|---------|\n| Go | 1.24+ |\n| CGO | Must be enabled (`CGO_ENABLED=1`) |\n| C compiler | GCC or Clang (for the SQLite driver) |\n| OS | Linux or macOS |\n| Build runner | [just](https://github.com/casey/just#installation) |\n\nCGO is required because the SQLite driver (`mattn/go-sqlite3`) is a C library. Even if you use MySQL or PostgreSQL, the binary still compiles with the SQLite driver.\n\n## Quick Start\n\n### 1. Build and install\n\n```bash\ngit clone https://github.com/hegner123/modulacms.git\ncd modulacms\njust build\ncp out/bin/modula /usr/local/bin/modula\n```\n\nVerify: `modula version`\n\n### 2. Create a project\n\n```bash\nmkdir ~/mysite \u0026\u0026 cd ~/mysite\nmodula init\n```\n\n`modula init` runs an interactive setup wizard that prompts for database driver, connection details, ports, and admin credentials. It creates `modula.config.json`, initializes the database schema, seeds three bootstrap roles (admin, editor, viewer) with 72 permissions, and registers the project in `~/.modula/configs.json`.\n\nFor non-interactive setup with defaults:\n\n```bash\nmodula init --yes --admin-password your-password\n```\n\nDefaults: SQLite database (`modula.db` in working directory), HTTP on `:8080`, HTTPS on `:4000`, SSH on `:2233`, development environment. The admin credentials are printed to the startup log.\n\n### 3. Start the server\n\n```bash\nmodula serve\n```\n\nThree servers start concurrently:\n\n| Server | Default Address | Purpose |\n|--------|-----------------|---------|\n| HTTP | `localhost:8080` | REST API + admin panel |\n| HTTPS | `localhost:4000` | TLS-secured API (autocert in production) |\n| SSH | `localhost:2233` | Terminal UI |\n\nGraceful shutdown: first SIGINT/SIGTERM triggers a 30-second shutdown; second signal forces exit.\n\n### 4. Connect\n\n**Web admin panel:** [http://localhost:8080/admin/](http://localhost:8080/admin/)\n\nLog in with `system@modulacms.local` and the password from init.\n\n**Terminal UI:** `ssh localhost -p 2233`\n\n**REST API:**\n\n```bash\ncurl -X POST http://localhost:8080/api/v1/auth/login \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"email\": \"system@modulacms.local\", \"password\": \"YOUR_PASSWORD\"}' \\\n  -c cookies.txt\n\ncurl http://localhost:8080/api/v1/datatype -b cookies.txt\n```\n\nOnce registered, manage a project from any directory:\n\n```bash\nmodula connect              # default project, default env\nmodula connect mysite       # specific project\nmodula connect mysite prod  # specific project + env\n```\n\n## Build \u0026 Development\n\n```bash\njust dev              # Build local binary with version info via ldflags\njust run              # Build and run\njust build            # Production binary to out/bin/\njust build-target darwin arm64  # Cross-compile for specific OS/arch\njust build-all        # Build all release targets (darwin/linux, amd64/arm64)\njust check            # Compile-check without producing artifacts\njust clean            # Remove build artifacts\njust vendor           # Update vendor directory\n```\n\n## Testing\n\n```bash\njust test             # Run all tests (creates/cleans testdb/ and backups/)\njust coverage         # Tests with coverage report\njust lint             # Run all linters (Go, Dockerfile, YAML)\n\n# Single package or test\ngo test -v ./internal/db\ngo test -v ./internal/db -run TestSpecificName\n\n# S3 integration tests (requires MinIO)\njust test-minio       # Start MinIO container\njust test-integration # Run integration tests\njust test-minio-down  # Stop MinIO\n\n# Cross-backend DB integration tests\njust docker-infra         # Start Postgres, MySQL, MinIO\njust test-integration-db  # Run cross-backend tests\n```\n\n## Docker\n\nUnified via `just dc \u003cbackend\u003e \u003caction\u003e`:\n\n```bash\n# Backends: full, sqlite, mysql, postgres, prod\n# Actions: up, down, reset, dev, fresh, logs, destroy (full only), minio-reset (postgres only)\n\njust dc full up       # Full stack (CMS + all databases + MinIO)\njust dc full down     # Stop containers, keep volumes\njust dc full reset    # Stop containers and delete volumes\njust dc full dev      # Rebuild and restart CMS container only\njust dc full fresh    # Reset volumes then rebuild everything\njust dc full logs     # Tail CMS logs\njust dc full destroy  # Remove containers, volumes, and images\n\njust dc sqlite up     # SQLite stack\njust dc mysql up      # MySQL stack\njust dc postgres up   # PostgreSQL stack\n```\n\nOther Docker commands:\n\n```bash\njust docker-infra     # Infrastructure only (Postgres, MySQL, MinIO)\njust docker-build     # Build standalone CMS image (for CI)\njust docker-release   # Tag and push image with version tags\n```\n\nThe Docker image exposes ports 8080 (HTTP), 4000 (HTTPS), and 2233 (SSH), with volumes for `/app/data`, `/app/certs`, `/app/.ssh`, `/app/backups`, and `/app/plugins`.\n\n## Architecture\n\n### Runtime\n\nThe `serve` command starts three concurrent servers sharing a single `DbDriver` instance:\n\n```\nHTTP  (default :8080)  ─┐\nHTTPS (default :8443)  ─┤── stdlib ServeMux (Go 1.22+) ── Middleware Chain ── Handlers ── DbDriver\nSSH   (default :2222)  ─┘   Charmbracelet Wish ── Bubbletea TUI ─────────────────────────── DbDriver\n```\n\nGraceful shutdown: first SIGINT/SIGTERM triggers a 30-second shutdown; second signal forces exit. Shutdown order: HTTP servers, plugin system, database connections.\n\n### Request Flow\n\n```\nClient Request\n  -\u003e Request ID\n  -\u003e Logging\n  -\u003e CORS\n  -\u003e Authentication (cookie session or Bearer API key)\n  -\u003e Rate Limiting (auth endpoints: 10 req/min per IP)\n  -\u003e Permission Injection (RBAC)\n  -\u003e Route Handler\n  -\u003e DbDriver Interface\n  -\u003e Database-specific wrapper (SQLite / MySQL / PostgreSQL)\n  -\u003e sqlc-generated queries\n```\n\n### Tri-Database Pattern\n\nOne codebase supports three databases through a layered abstraction:\n\n1. **SQL schemas** in `sql/schema/` define tables and queries per dialect (SQLite, MySQL, PostgreSQL)\n2. **sqlc** generates type-safe Go code into `internal/db-sqlite/`, `internal/db-mysql/`, `internal/db-psql/`\n3. **`DbDriver` interface** (~150 methods in `internal/db/db.go`) provides the contract\n4. **Wrapper structs** (`Database`, `MysqlDatabase`, `PsqlDatabase`) implement the interface, converting between sqlc types and application types\n\nSwitch databases by setting `db_driver` in `modula.config.json` to `\"sqlite\"`, `\"mysql\"`, or `\"postgres\"`.\n\n### Content Model\n\nContent uses a tree structure with sibling pointers for O(1) navigation and reordering:\n\n- `parent_id`: parent node\n- `first_child_id`: leftmost child\n- `next_sibling_id` / `prev_sibling_id`: doubly-linked sibling list\n\nContent items have a status lifecycle: **draft** -\u003e **pending** -\u003e **published** -\u003e **archived**.\n\n### Data Model\n\n27 schema directories define the full entity model:\n\n| Entity Group | Tables |\n|-------------|--------|\n| **Content** | content_data, content_fields, content_relations, admin variants |\n| **Schema** | datatypes, fields, datatype_fields, admin variants |\n| **Media** | media, media_dimensions |\n| **Routing** | routes, admin_routes |\n| **Users \u0026 Auth** | users, roles, permissions, role_permissions, tokens, user_oauth, sessions, user_ssh_keys |\n| **i18n** | locales |\n| **Webhooks** | webhooks, webhook_deliveries |\n| **System** | backups, change_events, tables |\n\nAll entity IDs are 26-character ULIDs wrapped in distinct Go types (`ContentID`, `UserID`, `FieldID`, etc.) that provide compile-time type safety.\n\n### RBAC Authorization\n\nRole-based access control with `resource:operation` granular permissions:\n\n| Role | Permissions | Description |\n|------|-------------|-------------|\n| **admin** | 47 (all) | Bypasses all permission checks |\n| **editor** | 28 | CRUD on content, media, routes, datatypes, fields |\n| **viewer** | 3 | Read-only: content, media, routes |\n\nThe `PermissionCache` maintains an in-memory role-to-permissions map with lock-free reads and 60-second periodic refresh. System-protected roles and permissions cannot be deleted or renamed.\n\n### Audited Commands\n\nAll database mutations are wrapped in transactions that atomically record `change_events` rows capturing:\n- Operation type (INSERT, UPDATE, DELETE)\n- Old and new JSON values\n- User ID, request ID, IP address\n- Hybrid Logical Clock timestamps for distributed ordering\n\n## Admin Panel\n\nServer-rendered HTMX + templ web interface. No SPA: all pages are server-rendered with HTMX for interactivity.\n\n- **Content**: tree navigation, block editor with drag-and-drop, inline field editing\n- **Schema**: datatypes, fields, and field-datatype associations\n- **Media**: upload, browse, image preview with dimension presets\n- **Users \u0026 Roles**: user management, role assignment, permission configuration\n- **Routes**: URL slug management\n- **Plugins**: browse, enable, disable, view details\n- **Webhooks**: create, test, view delivery history\n- **Locales**: i18n configuration and locale management\n- **Settings**: server configuration\n- **Audit Log**: change event browser\n- **Import**: bulk import from external CMS platforms\n- **Sessions \u0026 Tokens**: active session and API token management\n\nLight DOM web components (`mcms-*`) provide dialog, data-table, field-renderer, media-picker, tree-nav, toast, confirm, and search widgets.\n\n```bash\njust admin generate      # Regenerate templ Go code\njust admin watch         # Watch .templ files for changes\njust admin bundle        # Bundle block editor JS via esbuild\n```\n\n## API\n\nAll endpoints are prefixed with `/api/v1/` and follow standard REST conventions. Content delivery uses slug-based routing at `/api/v1/content/{slug}`.\n\n### Authentication\n\n```\nPOST   /api/v1/auth/login          # Session login\nPOST   /api/v1/auth/logout         # Session logout\nGET    /api/v1/auth/me             # Current user profile\nPOST   /api/v1/auth/register       # Registration\nPOST   /api/v1/auth/reset          # Password reset\nGET    /api/v1/auth/oauth/login    # OAuth flow initiation\nGET    /api/v1/auth/oauth/callback # OAuth callback\n```\n\n### Content Management\n\n```\nGET|POST          /api/v1/contentdata            # List / Create\nGET|PUT|DELETE    /api/v1/contentdata/{id}        # Get / Update / Delete\nPOST              /api/v1/content/create          # Create content with fields (cascade)\nPOST              /api/v1/content/batch           # Batch operations\nPOST              /api/v1/contentdata/move        # Move node in tree\nPOST              /api/v1/contentdata/reorder     # Reorder siblings\n\nGET|POST          /api/v1/contentfields           # Content field values\nGET|POST          /api/v1/contentrelations        # Content relationships\n```\n\n### Publishing \u0026 Versioning\n\n```\nPOST              /api/v1/content/publish         # Publish content (creates snapshot)\nPOST              /api/v1/content/unpublish       # Unpublish content\nPOST              /api/v1/content/schedule        # Schedule future publish\nGET               /api/v1/content/versions        # List versions\nPOST              /api/v1/content/versions        # Create manual version\nDELETE            /api/v1/content/versions/{id}   # Delete version\nPOST              /api/v1/content/restore         # Restore from version\n```\n\nAdmin content mirrors exist at `/api/v1/admin/content/` for draft management.\n\n### Content Delivery\n\n```\nGET               /api/v1/content/{slug}          # Published content by slug\nGET               /api/v1/content/{slug}?preview=true  # Live draft (requires auth)\nGET               /api/v1/content/{slug}?locale=en     # Locale-specific delivery\nGET               /api/v1/content/{slug}?format=clean  # Format override\nGET               /api/v1/globals                 # All global content trees\nGET               /api/v1/query/{datatype}        # Query by datatype\n```\n\nThe `format` query parameter controls response structure: `contentful`, `sanity`, `strapi`, `wordpress`, `clean`, or `raw`.\n\n### Schema\n\n```\nGET|POST          /api/v1/datatype               # Datatypes\nGET|POST          /api/v1/fields                 # Field definitions\nGET|POST          /api/v1/datatypefields         # Datatype-field associations\nGET|POST          /api/v1/tables                 # Custom tables\n```\n\n### Media\n\n```\nGET               /api/v1/media                  # List (paginated)\nPOST              /api/v1/media                  # Upload (multipart/form-data)\nDELETE            /api/v1/media/{id}             # Delete\nGET               /api/v1/media/health           # S3 connectivity check\nDELETE            /api/v1/media/cleanup          # Remove orphaned S3 objects\nGET|POST          /api/v1/mediadimensions        # Dimension presets\n```\n\n### Routes \u0026 Locales\n\n```\nGET|POST          /api/v1/routes                 # Route management\nGET               /api/v1/locales                # Public locale list\nCRUD              /api/v1/admin/locales          # Admin locale management\n```\n\n### Users \u0026 Access Control\n\n```\nGET|POST          /api/v1/users                  # User management\nPOST              /api/v1/users/reassign-delete  # Reassign content and delete user\nGET|POST          /api/v1/roles                  # Roles\nGET|POST          /api/v1/permissions            # Permissions\nGET|POST          /api/v1/role-permissions       # Role-permission mappings\nGET|POST|DELETE   /api/v1/ssh-keys               # SSH key management\nGET|POST|DELETE   /api/v1/sessions               # Session management\nGET|POST|DELETE   /api/v1/tokens                 # API token management\n```\n\n### Webhooks\n\n```\nCRUD              /api/v1/admin/webhooks         # Webhook management\nPOST              /api/v1/admin/webhooks/{id}/test       # Test delivery\nGET               /api/v1/admin/webhooks/{id}/deliveries # Delivery history\nPOST              /api/v1/admin/webhooks/deliveries/{id}/retry # Retry delivery\n```\n\n### Import \u0026 Configuration\n\n```\nPOST   /api/v1/import/contentful   # Import from Contentful\nPOST   /api/v1/import/sanity       # Import from Sanity\nPOST   /api/v1/import/strapi       # Import from Strapi\nPOST   /api/v1/import/wordpress    # Import from WordPress\nPOST   /api/v1/import/clean        # Import Modula format\nPOST   /api/v1/import              # Bulk import\n\nGET               /api/v1/admin/config           # Get config (redacted)\nPATCH             /api/v1/admin/config           # Update config\nGET               /api/v1/admin/config/meta      # Config field metadata\nGET               /api/v1/admin/plugins          # List plugins\nGET               /api/v1/admin/plugins/routes   # Plugin route approval\n```\n\n## Terminal UI\n\nThe SSH-accessible TUI is built with Charmbracelet Bubbletea following the Elm Architecture (Model-Update-View). Each screen implements a `Screen` interface with its own state, update, and view methods.\n\n- **Content**: browse, create, edit content with tree navigation (regular and admin views)\n- **Datatypes \u0026 Fields**: define and manage content schemas\n- **Media**: upload and manage media assets with file picker\n- **Users**: user management with role assignment\n- **Routes**: URL slug configuration\n- **Plugins**: browse, enable, disable, reload Lua plugins\n- **Webhooks**: webhook management and delivery monitoring\n- **Pipelines**: pipeline entry management\n- **Deploy**: content sync between environments\n- **Configuration**: edit server configuration\n- **Database**: database info and table browser\n- **Quick Start**: guided setup wizard\n\nUI features: responsive panel layouts with three screen modes (normal/wide/full), accordion focus, panel tabs, scroll indicators, adaptive statusbar, compact/full header with breadcrumbs.\n\n## Connect System\n\nThe `connect` command provides a project registry for managing multiple CMS instances and environments from a single CLI. Each project can have multiple environments (local, dev, staging, prod), each pointing to a different `modula.config.json`. The registry lives at `~/.modula/configs.json`.\n\n### Local vs Remote\n\nWhen a config has `db_driver` set, the TUI connects directly to the local database. When a config has `remote_url` and `remote_api_key` set instead, the TUI connects to a remote CMS server over HTTPS using the Go SDK as a `DbDriver` implementation. This means the same TUI works identically whether managing a local database or a remote server: the `RemoteDriver` in `internal/remote/` implements the full `DbDriver` interface by delegating to SDK calls.\n\nRemote connections include:\n- **Health check** on startup (fails fast if server unreachable)\n- **Connection tracking** with atomic status (connected/disconnected/unknown), reflected in the TUI status bar\n- **Retry logic** for transient failures (502/503/504, timeouts) with a single retry after 1s delay\n- **Graceful degradation**: DDL and infrastructure methods return `ErrNotSupported` or `ErrRemoteMode`\n\n### Registry Management\n\n```bash\n# Register a project environment\nmodula connect set mysite local ./modula.config.json\nmodula connect set mysite prod /srv/mysite/modula.config.json\n\n# Connect to a project\nmodula connect                      # default project, default env\nmodula connect mysite               # mysite, default env\nmodula connect mysite prod          # mysite, prod env\n\n# Manage defaults\nmodula connect default mysite       # set default project\nmodula connect default mysite prod  # set default env for project\n\n# List and remove\nmodula connect list                 # show all projects and environments\nmodula connect remove mysite        # remove entire project\nmodula connect remove mysite --env dev  # remove single environment\n```\n\n### Resolution Order\n\n1. Both name and env given: use that exact project + environment\n2. Only name given: use that project's default environment\n3. Neither given: use the default project's default environment\n4. Registry empty: look for `modula.config.json` in the current directory\n\n### Config Fields\n\n| Field | Purpose |\n|-------|---------|\n| `remote_url` | Base URL of the remote CMS server (e.g. `https://cms.example.com`) |\n| `remote_api_key` | API key for authenticating SDK calls to the remote server |\n| `db_driver` | Local database driver (`sqlite`, `mysql`, `postgres`): mutually exclusive with `remote_url` |\n\nA config must have either `remote_url` or `db_driver` set (not both).\n\n## Lua Plugin System\n\nPlugins extend Modula with sandboxed Lua scripts via gopher-lua.\n\n### Plugin Structure\n\n```\nplugins/my-plugin/\n  init.lua          # Entry point with plugin_info table\n```\n\n### Capabilities\n\n- **Database**: query builder with safe identifier validation, isolated per-plugin tables\n- **Content Hooks**: before/after hooks on create, update, delete, publish, archive\n- **HTTP Routes**: register custom endpoints with approval workflow\n- **Logging**: structured logging integrated with CMS slog\n- **Schema**: define custom tables with drift detection\n\n### Safety\n\n- Operation counting per VM checkout (default 1000 ops)\n- Per-hook timeout (default 2000ms) and per-event timeout (default 5000ms)\n- Circuit breaker: max consecutive failures trips the plugin\n- Connection pool limits and request/response size limits\n- Plugins cannot access core CMS tables or other plugins' tables\n- Hot-reload with file watcher (opt-in for production)\n\n### CLI\n\n```bash\n./modula plugin list               # List plugins\n./modula plugin init my-plugin     # Create scaffold\n./modula plugin validate ./path    # Validate structure\n./modula plugin reload my-plugin   # Hot-reload (requires running server)\n./modula plugin enable my-plugin   # Enable\n./modula plugin disable my-plugin  # Disable\n```\n\n## Deploy\n\nThe `deploy` command syncs content between environments via JSON exports and the Go SDK.\n\n```bash\nmodula deploy export -o export.json           # Export content to JSON\nmodula deploy import -f export.json           # Import from JSON\nmodula deploy pull --env prod                 # Download from remote\nmodula deploy push --env staging              # Upload to remote\nmodula deploy snapshot list                   # List import snapshots\nmodula deploy snapshot show \u003cid\u003e              # Show snapshot details\nmodula deploy snapshot restore \u003cid\u003e           # Restore from snapshot\nmodula deploy env list                        # List configured environments\nmodula deploy env test                        # Test environment connectivity\n```\n\n## MCP Server\n\nModula includes a built-in Model Context Protocol server with 40+ tools for AI-assisted content management. The MCP server connects to a running Modula instance via the Go SDK: no separate binary to build or install.\n\n```bash\n# Start the MCP server over stdio\nMODULA_URL=http://localhost:8080 MODULA_API_KEY=your-key modula mcp\n\n# Or use flags\nmodula mcp --url http://localhost:8080 --api-key your-key\n```\n\nTools cover content CRUD, content fields, batch operations, schema management, media, routes, users, roles, permissions, configuration, and import.\n\n## Configuration\n\nConfiguration lives in `modula.config.json` at the project root. Environment variables can be referenced as `${VAR}` or `${VAR:-default}`.\n\nKey configuration categories:\n\n| Category | Fields |\n|----------|--------|\n| **Database** | `db_driver`, `db_url`, `db_name`, `db_user`, `db_password` |\n| **Server** | `port`, `ssl_port`, `ssh_port`, `environment`, `cert_dir` |\n| **Auth** | `auth_salt`, `cookie_*`, `oauth_*` |\n| **S3 Storage** | `bucket_endpoint`, `bucket_media`, `bucket_backup`, `bucket_access_key`, `bucket_secret_key` |\n| **Email** | `email_enabled`, `email_provider` (smtp/sendgrid/ses/postmark), `email_from_*` |\n| **CORS** | `cors_origins`, `cors_methods`, `cors_headers`, `cors_credentials` |\n| **i18n** | `i18n_enabled`, `i18n_default_locale`, `i18n_fallback_chain` |\n| **Output** | `output_format` (contentful/sanity/strapi/wordpress/clean/raw) |\n| **Plugins** | `plugin_enabled`, `plugin_directory`, `plugin_max_vms`, `plugin_timeout`, `plugin_hot_reload` |\n| **Observability** | `observability_enabled`, `observability_provider` (sentry/datadog/newrelic), `observability_dsn` |\n\nRuntime configuration can be updated via the REST API (`PATCH /api/v1/admin/config`) with hot-reload support for applicable fields.\n\n## SQL Code Generation\n\n```bash\njust sqlc    # Regenerate Go code from SQL queries\n```\n\nThis runs `sqlc generate` against `sql/sqlc.yml`, producing type-safe Go code in three packages:\n- `internal/db-sqlite/` (package `mdb`)\n- `internal/db-mysql/` (package `mdbm`)\n- `internal/db-psql/` (package `mdbp`)\n\nNever edit files in these directories by hand: they are overwritten by sqlc. After modifying schema or queries, run `just sqlc`, then update the `DbDriver` interface and implement methods on all three wrapper structs.\n\n## SDKs\n\nModula provides official SDKs for TypeScript, Go, and Swift. All SDKs have zero external dependencies beyond their respective standard libraries.\n\n### TypeScript\n\nThe `sdks/typescript/` directory is a pnpm workspace monorepo with three packages:\n\n| Package | npm | Purpose |\n|---------|-----|---------|\n| `types/` | `@modulacms/types` | Shared entity types, 30 branded IDs, enums |\n| `modulacms-sdk/` | `@modulacms/sdk` | Read-only content delivery client |\n| `modulacms-admin-sdk/` | `@modulacms/admin-sdk` | Full admin CRUD client |\n\n**Requirements:** TypeScript 5.7+, Node 18+ (Fetch API), pnpm 9+\n\n**Install:**\n```bash\nnpm install @modulacms/sdk             # Content delivery\nnpm install @modulacms/admin-sdk       # Admin operations\n```\n\n**Content Delivery:**\n```typescript\nimport { ModulaClient } from \"@modulacms/sdk\"\n\nconst cms = new ModulaClient({\n  baseUrl: \"https://cms.example.com\",\n  defaultFormat: \"clean\",\n})\n\nconst page = await cms.getPage(\"blog/hello-world\")\nconst media = await cms.listMedia()\n```\n\n**Admin SDK:**\n```typescript\nimport { createAdminClient } from \"@modulacms/admin-sdk\"\n\nconst client = createAdminClient({\n  baseUrl: \"https://cms.example.com\",\n  apiKey: \"your-api-key\",\n})\n\nawait client.auth.login({ email: \"admin@example.com\", password: \"pass\" })\nconst users = await client.users.list()\nconst content = await client.contentData.create({ status: \"draft\", ... })\nawait client.mediaUpload.upload(file)\n```\n\nBoth SDKs ship as dual ESM + CommonJS builds via tsup with full type declarations.\n\n```bash\njust sdk ts install      # pnpm install (workspace root)\njust sdk ts build        # Build all packages\njust sdk ts test         # Run all SDK tests (Vitest)\njust sdk ts typecheck    # Typecheck all packages\n```\n\n### Go\n\nThe Go SDK provides a type-safe client with a generic `Resource[Entity, CreateParams, UpdateParams, ID]` pattern for CRUD operations.\n\n**Import:** `github.com/hegner123/modulacms/sdks/go`\n\n```go\nimport modula \"github.com/hegner123/modulacms/sdks/go\"\n\nclient, err := modula.NewClient(modula.ClientConfig{\n    BaseURL: \"https://cms.example.com\",\n    APIKey:  \"your-api-key\",\n})\n\n// Authentication\nme, err := client.Auth.Me(ctx)\n\n// CRUD with typed IDs and pagination\nusers, err := client.Users.ListPaginated(ctx, modula.PaginationParams{Limit: 20, Offset: 0})\ncontent, err := client.ContentData.Create(ctx, modula.CreateContentDataParams{...})\n\n// Media upload\nmedia, err := client.MediaUpload.Upload(ctx, file, \"photo.jpg\", \u0026modula.MediaUploadOptions{\n    Path: \"blog/headers\",\n})\n\n// Content delivery\npage, err := client.Content.GetPage(ctx, \"blog/hello-world\", \"clean\")\n\n// Error classification\nif modula.IsNotFound(err) { ... }\nif modula.IsUnauthorized(err) { ... }\n```\n\nThe SDK exposes 23+ typed resource endpoints including content, schema, media, users, roles, permissions, plugins, configuration, sessions, SSH keys, and bulk import.\n\n```bash\njust sdk go test      # Run Go SDK tests\njust sdk go vet       # Vet Go SDK\n```\n\n### Swift\n\nThe Swift SDK is a zero-dependency Swift Package Manager package supporting Apple platforms.\n\n**Platforms:** iOS 16+, macOS 13+, tvOS 16+, watchOS 9+\n**Swift:** 5.9+\n\n```swift\nimport Modula\n\nlet client = try ModulaClient(config: ClientConfig(\n    baseURL: \"https://cms.example.com\",\n    apiKey: \"your-api-key\"\n))\n\n// Authentication\nlet login = try await client.auth.login(params: LoginParams(\n    email: \"admin@example.com\",\n    password: \"pass\"\n))\n\n// CRUD with branded IDs\nlet users = try await client.users.listPaginated(params: PaginationParams(limit: 20, offset: 0))\nlet content = try await client.contentData.create(params: CreateContentDataParams(...))\n\n// Media upload\nlet media = try await client.mediaUpload.upload(\n    data: imageData,\n    filename: \"photo.jpg\",\n    options: MediaUploadResource.UploadOptions(path: \"photos\")\n)\n\n// Content delivery\nlet page = try await client.content.getPage(slug: \"blog/hello-world\", format: \"clean\")\n\n// Error handling\ndo {\n    let user = try await client.users.get(id: userID)\n} catch let error as APIError where isNotFound(error) {\n    print(\"User not found\")\n}\n```\n\nThe SDK uses async/await throughout, marks all types as `Sendable` for actor isolation, and provides 30 branded ID types via a `ResourceID` protocol. It covers 26 CRUD resources plus 13 specialized endpoints (auth, media upload, admin tree, plugins, config, etc.).\n\n```bash\njust sdk swift build  # Build Swift SDK\njust sdk swift test   # Run Swift SDK tests\njust sdk swift clean  # Clean build artifacts\n```\n\n## Backup \u0026 Restore\n\n```bash\n./modula backup create              # Create full backup (ZIP with SQL dump + metadata)\n./modula backup restore backup.zip  # Restore from backup\n./modula backup list                # List backup history\n./modula backup delete {id}         # Delete backup record\n```\n\nBackups are ZIP archives containing a database-specific SQL dump and a JSON manifest with driver, timestamp, version, and node ID. Storage is local (`backups/` directory) or S3.\n\n## CLI Commands\n\n```\nmodula [--config=path] [--verbose] \u003ccommand\u003e\n\n  serve              Start HTTP/HTTPS/SSH servers\n  serve --wizard     Interactive setup before starting\n  install            Interactive installation wizard\n  install --yes      Non-interactive with defaults\n  tui                Launch terminal UI standalone\n  db init            Initialize database and bootstrap data\n  db wipe            Drop all tables\n  backup create      Create full backup\n  backup restore     Restore from backup\n  backup list        List backup history\n  config show        Print config as JSON\n  config validate    Validate modula.config.json\n  config set         Update config field\n  config fields      List all config fields with metadata\n  cert generate      Generate self-signed certificates\n  cert check         Verify certificate validity\n  update check       Check for updates\n  update install     Install new version\n  connect            Launch TUI for a registered project (default project + env)\n  connect \u003cname\u003e     Launch TUI for named project (default env)\n  connect \u003cname\u003e \u003cenv\u003e  Launch TUI for named project + specific env\n  connect set        Register a project environment\n  connect list       List registered projects and environments\n  connect remove     Remove a project or environment\n  connect default    Set the default project or environment\n  deploy export      Export content data to JSON file\n  deploy import      Import content from JSON export\n  deploy pull        Download data from remote environment\n  deploy push        Upload local data to remote environment\n  deploy snapshot    List, show, or restore import snapshots\n  deploy env         List and test configured deploy environments\n  mcp                Start the MCP server over stdio (requires MODULA_URL + MODULA_API_KEY)\n  pipeline list      Show all pipeline entries\n  pipeline show      Show pipelines for a specific table\n  plugin list        List plugins\n  plugin init        Create plugin scaffold\n  plugin validate    Validate plugin structure\n  plugin reload      Hot-reload plugin\n  plugin enable      Enable plugin\n  plugin disable     Disable plugin\n  version            Show version info\n```\n\n## CI/CD\n\nTwo GitHub Actions workflows:\n\n- **Go** (`.github/workflows/go.yml`): runs on Go source changes (excludes `sdks/**`), tests with libwebp-dev on Ubuntu\n- **SDKs** (`.github/workflows/sdks.yml`): runs on SDK changes, tests TypeScript (pnpm + Vitest), Go SDK, and Swift SDK (SPM build + test)\n\n## Project Structure\n\n```\ncmd/                          Cobra CLI commands (serve, install, tui, connect, deploy, pipeline, db, backup, config, cert, plugin, version)\ninternal/\n  admin/                      HTMX admin panel: CSRF, auth middleware, static file embed, web components\n  admin/handlers/             Admin page handlers (render, auth, CRUD for all resources)\n  admin/pages/                templ full-page components (~28 pages)\n  admin/static/               CSS, JS, block editor, web components (go:embed)\n  auth/                       Authentication (bcrypt, OAuth, sessions)\n  backup/                     Backup/restore (SQL dump + ZIP)\n  config/                     Configuration loading, validation, hot-reload\n  db/                         DbDriver interface, wrapper structs, application types\n  db/types/                   ULID-based typed IDs, enums, nullable wrappers\n  db/audited/                 Audited command pattern for change events\n  db-sqlite/, db-mysql/, db-psql/  sqlc-generated code (do not edit)\n  definitions/                CMS format definitions (Contentful, Sanity, Strapi, WordPress)\n  deploy/                     Content sync: export, import, push, pull, snapshots\n  email/                      Transactional email (SMTP, SendGrid, SES, Postmark)\n  install/                    Setup wizard and bootstrap\n  media/                      Image optimization, S3 upload\n  middleware/                  CORS, rate limiting, sessions, RBAC authorization\n  model/                      Domain structs (Node, Datatype, Field)\n  plugin/                     Lua plugin system (gopher-lua)\n  publishing/                 Publish, snapshot, version management\n  registry/                   Project registry (~/.modula/configs.json)\n  remote/                     RemoteDriver: DbDriver over Go SDK (HTTPS)\n  router/                     HTTP route registration, slug handling, globals, pagination\n  service/                    Service layer (SchemaService with composable store interfaces)\n  transform/                  Content format transformers\n  tui/                        Bubbletea TUI (Screen interface, PanelScreen base, 12+ screens)\n  utility/                    Logging (slog), version info, helpers\n  validation/                 Composable field validation rules\nmcp/                          MCP server (40+ tools for AI-assisted CMS management)\nsql/\n  schema/                     27 numbered schema directories (DDL + queries per dialect)\n  sqlc.yml                    sqlc configuration\nsdks/\n  typescript/\n    types/                    @modulacms/types (shared types, branded IDs, enums)\n    modulacms-sdk/            @modulacms/sdk (read-only content delivery)\n    modulacms-admin-sdk/      @modulacms/admin-sdk (full admin CRUD)\n  go/                         Go SDK (generic Resource[E,C,U,ID] pattern)\n  swift/                      Swift SDK (SPM, zero dependencies, async/await)\ndeploy/\n  docker/                     Docker Compose files per database\n```\n## Performance\n\nModula is built in Go and ships as a single compiled binary. There is no runtime to install, no interpreter overhead, and no dependency tree to manage in production. One artifact runs everything: API server, admin panel, SSH terminal UI, and background services.\n\n- **Single binary**: one compiled artifact, zero runtime dependencies\n- **Three concurrent servers**: HTTP, HTTPS (Let's Encrypt autocert), and SSH start together and share a single database connection pool with graceful shutdown on SIGINT/SIGTERM\n- **Indexed SQL databases**: SQLite, MySQL, and PostgreSQL via sqlc-generated type-safe queries with prepared statements: no ORM, no reflection\n- **REST API**: stdlib `net/http` ServeMux (Go 1.22+) with an eight-layer middleware chain; no framework overhead\n- **SSH TUI**: full content management over SSH using Charmbracelet Bubbletea with responsive panel layouts, eliminating round-trips to a web browser\n- **Lock-free permission cache**: RBAC permissions are held in memory with build-then-swap updates and `RWMutex` for reader-heavy workloads; permission checks are O(1) map lookups\n- **O(1) content tree navigation**: content uses doubly-linked sibling pointers (`next_sibling_id`, `prev_sibling_id`, `first_child_id`) for constant-time reordering and traversal without recursive queries\n- **Compiled templates**: the admin panel uses templ, which compiles templates to Go bytecode with buffer pooling; no template parsing at runtime\n- **Embedded static assets**: CSS, JS, and web components are compiled into the binary via `go:embed`; zero file I/O in production\n- **Image optimization pipeline**: uploads are converted to WebP with responsive dimension presets and focal-point cropping; variants are precomputed at upload time, not on demand\n- **Per-IP rate limiting**: token-bucket rate limiter with background cleanup of stale entries; applied to auth endpoints to prevent brute-force attacks\n- **Connection pooling**: database connections are pooled (25 max open, 10 idle, 5-minute lifetime) across all three backends\n- **Plugin VM pooling**: Lua plugin VMs use lock-free buffered channels with backpressure; reserved channels guarantee hook execution under load\n\n## Flexibility\n\nModula uses a dual content schema: public tables for the site's actual content and admin tables that power the admin panel UI itself. The schema is defined at runtime: datatypes, fields, content data, content fields, and routes combine to let you create any content structure you need without code changes or redeployment.\n\n- **Runtime schema**: define datatypes, attach fields, create content, and configure routes entirely through the API, admin panel, or TUI; no migrations, no redeploys\n- **Dual content schema**: parallel admin and client table sets (`admin_content_data` / `content_data`, `admin_content_fields` / `content_fields`, etc.) give you a second full content layer with no prescribed purpose. Agencies use it to build custom admin panels where features toggle per client and upgrades distribute universally, but it is just data, so use it for whatever you need\n- **Tri-database support**: switch between SQLite, MySQL, and PostgreSQL by changing one field in `modula.config.json`; the `DbDriver` interface (~150 methods) abstracts all differences\n- **Six output formats**: content responses can mimic Contentful, Sanity, Strapi, WordPress, or use Modula clean/raw format; set a default or override per request with `?format=`\n- **Multi-CMS import**: bulk import content from Contentful, Sanity, Strapi, WordPress, or Modula clean format with automatic datatype and field creation\n- **Built-in backup and deploy**: ZIP backups (SQL dump + media) stored locally or in S3; content sync between environments with export, import, push, pull, and snapshot restore with dry-run validation\n- **Full spec compliance**: OAuth works with any provider that implements the standard (Google, GitHub, Azure, Okta, Auth0) via configurable endpoints; S3 storage works with AWS, MinIO, DigitalOcean Spaces, Wasabi, or any S3-compatible service; email works with SMTP, SendGrid, SES, or Postmark. You are never locked into a vendor\n- **Plugin system**: Lua plugins can define custom database tables, register HTTP routes, hook into content lifecycle events, and access core CMS data through a gated API\n- **Connect system**: manage multiple CMS instances and environments from a single CLI; the same TUI works identically whether connected to a local database or a remote server over HTTPS\n- **Self-consuming SDK**: remote operations from a local binary use Modula's own Go SDK as the transport layer. The `RemoteDriver` implements the full `DbDriver` interface by delegating to SDK calls over HTTPS, so local and remote modes share the same code paths. The MCP server and deploy system use the same SDK\n- **Content versioning and i18n**: publish, unpublish, schedule, version, and restore content with locale-aware delivery and fallback chains\n- **Webhooks**: event-driven HTTP notifications with HMAC-SHA256 signing, delivery tracking, retry, and test endpoints\n- **Three SDKs**: official TypeScript (ESM + CJS), Go, and Swift SDKs with zero external dependencies\n- **Multi-format delivery**: slug-based content delivery with preview mode, locale selection, format override, datatype queries, and global content trees\n- **MCP server**: 40+ tool Model Context Protocol server for AI-assisted content management\n\n## Transparency\n\nPlugins always tell you what they need. Every plugin declares its capabilities and core table access requirements in a `plugin_info` manifest: there are no hidden lifecycles. Access is enforced through three sequential gates: a hardcoded table whitelist, per-table read/write policies with column-level filtering, and per-plugin approved access stored in the database.\n\n- **Plugin manifest**: plugins declare capabilities (hooks, routes, core table access) in `plugin_info`; the system enforces exactly what was declared and nothing more\n- **Plugin safety**: operation counting (default 1000 ops per checkout), per-hook timeouts (2000ms), per-event timeouts (5000ms), circuit breaker on consecutive failures, sandboxed Lua VMs with stripped globals, and table namespace isolation between plugins\n- **Single config file**: `modula.config.json` contains every setting: database, server ports, auth, OAuth, S3 storage, CORS, email, plugins, observability, i18n, and update preferences. Environment variables are referenced as `${VAR}` or `${VAR:-default}`, not scattered across the system\n- **Config introspection**: `modula config fields` lists all 190+ config fields with metadata, `modula config validate` checks required fields, and `PATCH /api/v1/admin/config` with `GET /api/v1/admin/config/meta` expose field metadata programmatically\n- **Audit trail**: every database mutation atomically records a `change_event` with operation type, old and new JSON values, user ID, request ID, IP address, hybrid logical clock timestamp, and optional metadata\n- **Request traceability**: every request gets a UUID v4 `X-Request-ID` that flows through middleware, handlers, audit records, and logs for end-to-end tracing\n- **Explicit RBAC**: permissions use `resource:operation` labels (e.g., `content:read`, `media:create`) with O(1) set lookups; three bootstrap roles (admin, editor, viewer) with 47 granular permissions; system-protected roles cannot be deleted or renamed\n- **Webhook delivery tracking**: every webhook delivery is recorded with status (pending/success/failed), attempt count, and payload; failed deliveries are retried on a 60-second interval with full history available via API\n- **Structured logging**: severity-based structured logging via slog with request context; never logs credentials, tokens, or PII\n- **Observability**: metrics collection (HTTP requests/duration, DB queries, SSH connections, cache hits, plugin health, goroutine count) with export to Sentry, Datadog, or console\n- **Version and update checks**: `modula version` prints semantic version, git commit, and build timestamp; `modula update check` compares against GitHub releases with configurable channels\n- **Zero regular expressions**: ModulaCMS does not use regular expressions anywhere in its codebase. It is our opinion that regular expressions have no practical use case. We strongly encourage plugin authors to avoid them as well. No plugin that uses regular expressions will be officially endorsed by ModulaCMS\n\n## License\n\nSee [LICENSE](LICENSE) for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhegner123%2Fmodulacms","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhegner123%2Fmodulacms","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhegner123%2Fmodulacms/lists"}