A headless CMS written in Go, built on three core values: performance, flexibility, and transparency.
Modula 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.
- Single binary -- one compiled artifact, zero runtime dependencies
- 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
- Indexed SQL databases -- SQLite, MySQL, and PostgreSQL via sqlc-generated type-safe queries with prepared statements -- no ORM, no reflection
- REST API -- stdlib
net/httpServeMux (Go 1.22+) with an eight-layer middleware chain; no framework overhead - SSH TUI -- full content management over SSH using Charmbracelet Bubbletea with responsive panel layouts, eliminating round-trips to a web browser
- Lock-free permission cache -- RBAC permissions are held in memory with build-then-swap updates and
RWMutexfor reader-heavy workloads; permission checks are O(1) map lookups - 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 - Compiled templates -- the admin panel uses templ, which compiles templates to Go bytecode with buffer pooling; no template parsing at runtime
- Embedded static assets -- CSS, JS, and web components are compiled into the binary via
go:embed; zero file I/O in production - 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
- Per-IP rate limiting -- token-bucket rate limiter with background cleanup of stale entries; applied to auth endpoints to prevent brute-force attacks
- Connection pooling -- database connections are pooled (25 max open, 10 idle, 5-minute lifetime) across all three backends
- Plugin VM pooling -- Lua plugin VMs use lock-free buffered channels with backpressure; reserved channels guarantee hook execution under load
Modula uses a dual content schema -- admin tables for editorial work and public tables for delivery -- so editors work on drafts without affecting live content. The schema itself 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.
- Runtime schema -- define datatypes, attach fields, create content, and configure routes entirely through the API, admin panel, or TUI; no migrations, no redeploys
- 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 - Tri-database support -- switch between SQLite, MySQL, and PostgreSQL by changing one field in
modula.config.json; theDbDriverinterface (~150 methods) abstracts all differences - 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= - Multi-CMS import -- bulk import content from Contentful, Sanity, Strapi, WordPress, or Modula clean format with automatic datatype and field creation
- 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
- 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
- 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
- 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
- Self-consuming SDK -- remote operations from a local binary use Modula's own Go SDK as the transport layer. The
RemoteDriverimplements the fullDbDriverinterface 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 - Content versioning and i18n -- publish, unpublish, schedule, version, and restore content with locale-aware delivery and fallback chains
- Webhooks -- event-driven HTTP notifications with HMAC-SHA256 signing, delivery tracking, retry, and test endpoints
- Three SDKs -- official TypeScript (ESM + CJS), Go, and Swift SDKs with zero external dependencies
- Multi-format delivery -- slug-based content delivery with preview mode, locale selection, format override, datatype queries, and global content trees
- MCP server -- 40+ tool Model Context Protocol server for AI-assisted content management
Plugins 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.
- Plugin manifest -- plugins declare capabilities (hooks, routes, core table access) in
plugin_info; the system enforces exactly what was declared and nothing more - 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
- Single config file --
modula.config.jsoncontains 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 - Config introspection --
modula config fieldslists all 190+ config fields with metadata,modula config validatechecks required fields, andPATCH /api/v1/admin/configwithGET /api/v1/admin/config/metaexpose field metadata programmatically - Audit trail -- every database mutation atomically records a
change_eventwith operation type, old and new JSON values, user ID, request ID, IP address, hybrid logical clock timestamp, and optional metadata - Request traceability -- every request gets a UUID v4
X-Request-IDthat flows through middleware, handlers, audit records, and logs for end-to-end tracing - Explicit RBAC -- permissions use
resource:operationlabels (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 - 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
- Structured logging -- severity-based structured logging via slog with request context; never logs credentials, tokens, or PII
- Observability -- metrics collection (HTTP requests/duration, DB queries, SSH connections, cache hits, plugin health, goroutine count) with export to Sentry, Datadog, or console
- Version and update checks --
modula versionprints semantic version, git commit, and build timestamp;modula update checkcompares against GitHub releases with configurable channels
- Go 1.24+ with CGO enabled (for SQLite via
mattn/go-sqlite3) - Linux or macOS
- just as the build runner
# Build and run (auto-creates modula.config.json with defaults on first run)
just run
# Or build a local binary
just dev
./modula-x86 serve
# Interactive setup wizard
./modula serve --wizardOn first run without a modula.config.json, Modula generates one with defaults, creates the database schema, bootstraps RBAC roles (admin, editor, viewer), and logs a random admin password. The SSH server starts immediately; HTTP/HTTPS start once the database is ready.
Default Ports:
| Server | Port |
|---|---|
| HTTP | 8080 |
| HTTPS | 8443 |
| SSH | 2222 |
Connect to the TUI: ssh localhost -p 2222
Access the admin panel: http://localhost:8080/admin/
just dev # Build local binary with version info via ldflags
just run # Build and run
just build # Production binary to out/bin/
just check # Compile-check without producing artifacts
just clean # Remove build artifacts
just vendor # Update vendor directoryjust test # Run all tests (creates/cleans testdb/ and backups/)
just coverage # Tests with coverage report
just lint # Run all linters (Go, Dockerfile, YAML)
# Single package or test
go test -v ./internal/db
go test -v ./internal/db -run TestSpecificName
# S3 integration tests (requires MinIO)
just test-minio # Start MinIO container
just test-integration # Run integration tests
just test-minio-down # Stop MinIO
# Cross-backend DB integration tests
just docker-infra # Start Postgres, MySQL, MinIO
just test-integration-db # Run cross-backend testsUnified via just dc <backend> <action>:
# Backends: full, sqlite, mysql, postgres, prod
# Actions: up, down, reset, dev, fresh, logs, destroy (full only), minio-reset (postgres only)
just dc full up # Full stack (CMS + all databases + MinIO)
just dc full down # Stop containers, keep volumes
just dc full reset # Stop containers and delete volumes
just dc full dev # Rebuild and restart CMS container only
just dc full fresh # Reset volumes then rebuild everything
just dc full logs # Tail CMS logs
just dc full destroy # Remove containers, volumes, and images
just dc sqlite up # SQLite stack
just dc mysql up # MySQL stack
just dc postgres up # PostgreSQL stackOther Docker commands:
just docker-infra # Infrastructure only (Postgres, MySQL, MinIO)
just docker-build # Build standalone CMS image (for CI)
just docker-release # Tag and push image with version tagsThe 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.
The serve command starts three concurrent servers sharing a single DbDriver instance:
HTTP (default :8080) ─┐
HTTPS (default :8443) ─┤── stdlib ServeMux (Go 1.22+) ── Middleware Chain ── Handlers ── DbDriver
SSH (default :2222) ─┘ Charmbracelet Wish ── Bubbletea TUI ─────────────────────────── DbDriver
Graceful shutdown: first SIGINT/SIGTERM triggers a 30-second shutdown; second signal forces exit. Shutdown order: HTTP servers, plugin system, database connections.
Client Request
-> Request ID
-> Logging
-> CORS
-> Authentication (cookie session or Bearer API key)
-> Rate Limiting (auth endpoints: 10 req/min per IP)
-> Permission Injection (RBAC)
-> Route Handler
-> DbDriver Interface
-> Database-specific wrapper (SQLite / MySQL / PostgreSQL)
-> sqlc-generated queries
One codebase supports three databases through a layered abstraction:
- SQL schemas in
sql/schema/define tables and queries per dialect (SQLite, MySQL, PostgreSQL) - sqlc generates type-safe Go code into
internal/db-sqlite/,internal/db-mysql/,internal/db-psql/ DbDriverinterface (~150 methods ininternal/db/db.go) provides the contract- Wrapper structs (
Database,MysqlDatabase,PsqlDatabase) implement the interface, converting between sqlc types and application types
Switch databases by setting db_driver in modula.config.json to "sqlite", "mysql", or "postgres".
Content uses a tree structure with sibling pointers for O(1) navigation and reordering:
parent_id-- parent nodefirst_child_id-- leftmost childnext_sibling_id/prev_sibling_id-- doubly-linked sibling list
Content items have a status lifecycle: draft -> pending -> published -> archived.
27 schema directories define the full entity model:
| Entity Group | Tables |
|---|---|
| Content | content_data, content_fields, content_relations, admin variants |
| Schema | datatypes, fields, datatype_fields, admin variants |
| Media | media, media_dimensions |
| Routing | routes, admin_routes |
| Users & Auth | users, roles, permissions, role_permissions, tokens, user_oauth, sessions, user_ssh_keys |
| i18n | locales |
| Webhooks | webhooks, webhook_deliveries |
| System | backups, change_events, tables |
All entity IDs are 26-character ULIDs wrapped in distinct Go types (ContentID, UserID, FieldID, etc.) that provide compile-time type safety.
Role-based access control with resource:operation granular permissions:
| Role | Permissions | Description |
|---|---|---|
| admin | 47 (all) | Bypasses all permission checks |
| editor | 28 | CRUD on content, media, routes, datatypes, fields |
| viewer | 3 | Read-only: content, media, routes |
The 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.
All database mutations are wrapped in transactions that atomically record change_events rows capturing:
- Operation type (INSERT, UPDATE, DELETE)
- Old and new JSON values
- User ID, request ID, IP address
- Hybrid Logical Clock timestamps for distributed ordering
Server-rendered HTMX + templ web interface. No SPA -- all pages are server-rendered with HTMX for interactivity.
- Content -- tree navigation, block editor with drag-and-drop, inline field editing
- Schema -- datatypes, fields, and field-datatype associations
- Media -- upload, browse, image preview with dimension presets
- Users & Roles -- user management, role assignment, permission configuration
- Routes -- URL slug management
- Plugins -- browse, enable, disable, view details
- Webhooks -- create, test, view delivery history
- Locales -- i18n configuration and locale management
- Settings -- server configuration
- Audit Log -- change event browser
- Import -- bulk import from external CMS platforms
- Sessions & Tokens -- active session and API token management
Light DOM web components (mcms-*) provide dialog, data-table, field-renderer, media-picker, tree-nav, toast, confirm, and search widgets.
just admin generate # Regenerate templ Go code
just admin watch # Watch .templ files for changes
just admin bundle # Bundle block editor JS via esbuildAll endpoints are prefixed with /api/v1/ and follow standard REST conventions. Content delivery uses slug-based routing at /api/v1/content/{slug}.
POST /api/v1/auth/login # Session login
POST /api/v1/auth/logout # Session logout
GET /api/v1/auth/me # Current user profile
POST /api/v1/auth/register # Registration
POST /api/v1/auth/reset # Password reset
GET /api/v1/auth/oauth/login # OAuth flow initiation
GET /api/v1/auth/oauth/callback # OAuth callback
GET|POST /api/v1/contentdata # List / Create
GET|PUT|DELETE /api/v1/contentdata/{id} # Get / Update / Delete
POST /api/v1/content/create # Create content with fields (cascade)
POST /api/v1/content/batch # Batch operations
POST /api/v1/contentdata/move # Move node in tree
POST /api/v1/contentdata/reorder # Reorder siblings
GET|POST /api/v1/contentfields # Content field values
GET|POST /api/v1/contentrelations # Content relationships
POST /api/v1/content/publish # Publish content (creates snapshot)
POST /api/v1/content/unpublish # Unpublish content
POST /api/v1/content/schedule # Schedule future publish
GET /api/v1/content/versions # List versions
POST /api/v1/content/versions # Create manual version
DELETE /api/v1/content/versions/{id} # Delete version
POST /api/v1/content/restore # Restore from version
Admin content mirrors exist at /api/v1/admin/content/ for draft management.
GET /api/v1/content/{slug} # Published content by slug
GET /api/v1/content/{slug}?preview=true # Live draft (requires auth)
GET /api/v1/content/{slug}?locale=en # Locale-specific delivery
GET /api/v1/content/{slug}?format=clean # Format override
GET /api/v1/globals # All global content trees
GET /api/v1/query/{datatype} # Query by datatype
The format query parameter controls response structure: contentful, sanity, strapi, wordpress, clean, or raw.
GET|POST /api/v1/datatype # Datatypes
GET|POST /api/v1/fields # Field definitions
GET|POST /api/v1/datatypefields # Datatype-field associations
GET|POST /api/v1/tables # Custom tables
GET /api/v1/media # List (paginated)
POST /api/v1/media # Upload (multipart/form-data)
DELETE /api/v1/media/{id} # Delete
GET /api/v1/media/health # S3 connectivity check
DELETE /api/v1/media/cleanup # Remove orphaned S3 objects
GET|POST /api/v1/mediadimensions # Dimension presets
GET|POST /api/v1/routes # Route management
GET /api/v1/locales # Public locale list
CRUD /api/v1/admin/locales # Admin locale management
GET|POST /api/v1/users # User management
POST /api/v1/users/reassign-delete # Reassign content and delete user
GET|POST /api/v1/roles # Roles
GET|POST /api/v1/permissions # Permissions
GET|POST /api/v1/role-permissions # Role-permission mappings
GET|POST|DELETE /api/v1/ssh-keys # SSH key management
GET|POST|DELETE /api/v1/sessions # Session management
GET|POST|DELETE /api/v1/tokens # API token management
CRUD /api/v1/admin/webhooks # Webhook management
POST /api/v1/admin/webhooks/{id}/test # Test delivery
GET /api/v1/admin/webhooks/{id}/deliveries # Delivery history
POST /api/v1/admin/webhooks/deliveries/{id}/retry # Retry delivery
POST /api/v1/import/contentful # Import from Contentful
POST /api/v1/import/sanity # Import from Sanity
POST /api/v1/import/strapi # Import from Strapi
POST /api/v1/import/wordpress # Import from WordPress
POST /api/v1/import/clean # Import Modula format
POST /api/v1/import # Bulk import
GET /api/v1/admin/config # Get config (redacted)
PATCH /api/v1/admin/config # Update config
GET /api/v1/admin/config/meta # Config field metadata
GET /api/v1/admin/plugins # List plugins
GET /api/v1/admin/plugins/routes # Plugin route approval
The 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.
- Content -- browse, create, edit content with tree navigation (regular and admin views)
- Datatypes & Fields -- define and manage content schemas
- Media -- upload and manage media assets with file picker
- Users -- user management with role assignment
- Routes -- URL slug configuration
- Plugins -- browse, enable, disable, reload Lua plugins
- Webhooks -- webhook management and delivery monitoring
- Pipelines -- pipeline entry management
- Deploy -- content sync between environments
- Configuration -- edit server configuration
- Database -- database info and table browser
- Quick Start -- guided setup wizard
UI features: responsive panel layouts with three screen modes (normal/wide/full), accordion focus, panel tabs, scroll indicators, adaptive statusbar, compact/full header with breadcrumbs.
The 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.
When 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.
Remote connections include:
- Health check on startup (fails fast if server unreachable)
- Connection tracking with atomic status (connected/disconnected/unknown), reflected in the TUI status bar
- Retry logic for transient failures (502/503/504, timeouts) with a single retry after 1s delay
- Graceful degradation -- DDL and infrastructure methods return
ErrNotSupportedorErrRemoteMode
# Register a project environment
modula connect set mysite local ./modula.config.json
modula connect set mysite prod /srv/mysite/modula.config.json
# Connect to a project
modula connect # default project, default env
modula connect mysite # mysite, default env
modula connect mysite prod # mysite, prod env
# Manage defaults
modula connect default mysite # set default project
modula connect default mysite prod # set default env for project
# List and remove
modula connect list # show all projects and environments
modula connect remove mysite # remove entire project
modula connect remove mysite --env dev # remove single environment- Both name and env given: use that exact project + environment
- Only name given: use that project's default environment
- Neither given: use the default project's default environment
- Registry empty: look for
modula.config.jsonin the current directory
| Field | Purpose |
|---|---|
remote_url |
Base URL of the remote CMS server (e.g. https://cms.example.com) |
remote_api_key |
API key for authenticating SDK calls to the remote server |
db_driver |
Local database driver (sqlite, mysql, postgres) -- mutually exclusive with remote_url |
A config must have either remote_url or db_driver set (not both).
Plugins extend Modula with sandboxed Lua scripts via gopher-lua.
plugins/my-plugin/
init.lua # Entry point with plugin_info table
- Database -- query builder with safe identifier validation, isolated per-plugin tables
- Content Hooks -- before/after hooks on create, update, delete, publish, archive
- HTTP Routes -- register custom endpoints with approval workflow
- Logging -- structured logging integrated with CMS slog
- Schema -- define custom tables with drift detection
- Operation counting per VM checkout (default 1000 ops)
- Per-hook timeout (default 2000ms) and per-event timeout (default 5000ms)
- Circuit breaker: max consecutive failures trips the plugin
- Connection pool limits and request/response size limits
- Plugins cannot access core CMS tables or other plugins' tables
- Hot-reload with file watcher (opt-in for production)
./modula plugin list # List plugins
./modula plugin init my-plugin # Create scaffold
./modula plugin validate ./path # Validate structure
./modula plugin reload my-plugin # Hot-reload (requires running server)
./modula plugin enable my-plugin # Enable
./modula plugin disable my-plugin # DisableThe deploy command syncs content between environments via JSON exports and the Go SDK.
modula deploy export -o export.json # Export content to JSON
modula deploy import -f export.json # Import from JSON
modula deploy pull --env prod # Download from remote
modula deploy push --env staging # Upload to remote
modula deploy snapshot list # List import snapshots
modula deploy snapshot show <id> # Show snapshot details
modula deploy snapshot restore <id> # Restore from snapshot
modula deploy env list # List configured environments
modula deploy env test # Test environment connectivityModula 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.
# Start the MCP server over stdio
MODULA_URL=http://localhost:8080 MODULA_API_KEY=your-key modula mcp
# Or use flags
modula mcp --url http://localhost:8080 --api-key your-keyTools cover content CRUD, content fields, batch operations, schema management, media, routes, users, roles, permissions, configuration, and import.
Configuration lives in modula.config.json at the project root. Environment variables can be referenced as ${VAR} or ${VAR:-default}.
Key configuration categories:
| Category | Fields |
|---|---|
| Database | db_driver, db_url, db_name, db_user, db_password |
| Server | port, ssl_port, ssh_port, environment, cert_dir |
| Auth | auth_salt, cookie_*, oauth_* |
| S3 Storage | bucket_endpoint, bucket_media, bucket_backup, bucket_access_key, bucket_secret_key |
email_enabled, email_provider (smtp/sendgrid/ses/postmark), email_from_* |
|
| CORS | cors_origins, cors_methods, cors_headers, cors_credentials |
| i18n | i18n_enabled, i18n_default_locale, i18n_fallback_chain |
| Output | output_format (contentful/sanity/strapi/wordpress/clean/raw) |
| Plugins | plugin_enabled, plugin_directory, plugin_max_vms, plugin_timeout, plugin_hot_reload |
| Observability | observability_enabled, observability_provider (sentry/datadog/newrelic), observability_dsn |
Runtime configuration can be updated via the REST API (PATCH /api/v1/admin/config) with hot-reload support for applicable fields.
just sqlc # Regenerate Go code from SQL queriesThis runs sqlc generate against sql/sqlc.yml, producing type-safe Go code in three packages:
internal/db-sqlite/(packagemdb)internal/db-mysql/(packagemdbm)internal/db-psql/(packagemdbp)
Never 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.
Modula provides official SDKs for TypeScript, Go, and Swift. All SDKs have zero external dependencies beyond their respective standard libraries.
The sdks/typescript/ directory is a pnpm workspace monorepo with three packages:
| Package | npm | Purpose |
|---|---|---|
types/ |
@modulacms/types |
Shared entity types, 30 branded IDs, enums |
modulacms-sdk/ |
@modulacms/sdk |
Read-only content delivery client |
modulacms-admin-sdk/ |
@modulacms/admin-sdk |
Full admin CRUD client |
Requirements: TypeScript 5.7+, Node 18+ (Fetch API), pnpm 9+
Install:
npm install @modulacms/sdk # Content delivery
npm install @modulacms/admin-sdk # Admin operationsContent Delivery:
import { ModulaClient } from "@modulacms/sdk"
const cms = new ModulaClient({
baseUrl: "https://cms.example.com",
defaultFormat: "clean",
})
const page = await cms.getPage("blog/hello-world")
const media = await cms.listMedia()Admin SDK:
import { createAdminClient } from "@modulacms/admin-sdk"
const client = createAdminClient({
baseUrl: "https://cms.example.com",
apiKey: "your-api-key",
})
await client.auth.login({ email: "admin@example.com", password: "pass" })
const users = await client.users.list()
const content = await client.contentData.create({ status: "draft", ... })
await client.mediaUpload.upload(file)Both SDKs ship as dual ESM + CommonJS builds via tsup with full type declarations.
just sdk ts install # pnpm install (workspace root)
just sdk ts build # Build all packages
just sdk ts test # Run all SDK tests (Vitest)
just sdk ts typecheck # Typecheck all packagesThe Go SDK provides a type-safe client with a generic Resource[Entity, CreateParams, UpdateParams, ID] pattern for CRUD operations.
Import: github.com/hegner123/modulacms/sdks/go
import modula "github.com/hegner123/modulacms/sdks/go"
client, err := modula.NewClient(modula.ClientConfig{
BaseURL: "https://cms.example.com",
APIKey: "your-api-key",
})
// Authentication
me, err := client.Auth.Me(ctx)
// CRUD with typed IDs and pagination
users, err := client.Users.ListPaginated(ctx, modula.PaginationParams{Limit: 20, Offset: 0})
content, err := client.ContentData.Create(ctx, modula.CreateContentDataParams{...})
// Media upload
media, err := client.MediaUpload.Upload(ctx, file, "photo.jpg", &modula.MediaUploadOptions{
Path: "blog/headers",
})
// Content delivery
page, err := client.Content.GetPage(ctx, "blog/hello-world", "clean")
// Error classification
if modula.IsNotFound(err) { ... }
if modula.IsUnauthorized(err) { ... }The SDK exposes 23+ typed resource endpoints including content, schema, media, users, roles, permissions, plugins, configuration, sessions, SSH keys, and bulk import.
just sdk go test # Run Go SDK tests
just sdk go vet # Vet Go SDKThe Swift SDK is a zero-dependency Swift Package Manager package supporting Apple platforms.
Platforms: iOS 16+, macOS 13+, tvOS 16+, watchOS 9+ Swift: 5.9+
import Modula
let client = try ModulaClient(config: ClientConfig(
baseURL: "https://cms.example.com",
apiKey: "your-api-key"
))
// Authentication
let login = try await client.auth.login(params: LoginParams(
email: "admin@example.com",
password: "pass"
))
// CRUD with branded IDs
let users = try await client.users.listPaginated(params: PaginationParams(limit: 20, offset: 0))
let content = try await client.contentData.create(params: CreateContentDataParams(...))
// Media upload
let media = try await client.mediaUpload.upload(
data: imageData,
filename: "photo.jpg",
options: MediaUploadResource.UploadOptions(path: "photos")
)
// Content delivery
let page = try await client.content.getPage(slug: "blog/hello-world", format: "clean")
// Error handling
do {
let user = try await client.users.get(id: userID)
} catch let error as APIError where isNotFound(error) {
print("User not found")
}The 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.).
just sdk swift build # Build Swift SDK
just sdk swift test # Run Swift SDK tests
just sdk swift clean # Clean build artifacts./modula backup create # Create full backup (ZIP with SQL dump + metadata)
./modula backup restore backup.zip # Restore from backup
./modula backup list # List backup history
./modula backup delete {id} # Delete backup recordBackups 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.
modula [--config=path] [--verbose] <command>
serve Start HTTP/HTTPS/SSH servers
serve --wizard Interactive setup before starting
install Interactive installation wizard
install --yes Non-interactive with defaults
tui Launch terminal UI standalone
db init Initialize database and bootstrap data
db wipe Drop all tables
backup create Create full backup
backup restore Restore from backup
backup list List backup history
config show Print config as JSON
config validate Validate modula.config.json
config set Update config field
config fields List all config fields with metadata
cert generate Generate self-signed certificates
cert check Verify certificate validity
update check Check for updates
update install Install new version
connect Launch TUI for a registered project (default project + env)
connect <name> Launch TUI for named project (default env)
connect <name> <env> Launch TUI for named project + specific env
connect set Register a project environment
connect list List registered projects and environments
connect remove Remove a project or environment
connect default Set the default project or environment
deploy export Export content data to JSON file
deploy import Import content from JSON export
deploy pull Download data from remote environment
deploy push Upload local data to remote environment
deploy snapshot List, show, or restore import snapshots
deploy env List and test configured deploy environments
mcp Start the MCP server over stdio (requires MODULA_URL + MODULA_API_KEY)
pipeline list Show all pipeline entries
pipeline show Show pipelines for a specific table
plugin list List plugins
plugin init Create plugin scaffold
plugin validate Validate plugin structure
plugin reload Hot-reload plugin
plugin enable Enable plugin
plugin disable Disable plugin
version Show version info
Two GitHub Actions workflows:
- Go (
.github/workflows/go.yml) -- runs on Go source changes (excludessdks/**), tests with libwebp-dev on Ubuntu - SDKs (
.github/workflows/sdks.yml) -- runs on SDK changes, tests TypeScript (pnpm + Vitest), Go SDK, and Swift SDK (SPM build + test)
cmd/ Cobra CLI commands (serve, install, tui, connect, deploy, pipeline, db, backup, config, cert, plugin, version)
internal/
admin/ HTMX admin panel: CSRF, auth middleware, static file embed, web components
admin/handlers/ Admin page handlers (render, auth, CRUD for all resources)
admin/pages/ templ full-page components (~28 pages)
admin/static/ CSS, JS, block editor, web components (go:embed)
auth/ Authentication (bcrypt, OAuth, sessions)
backup/ Backup/restore (SQL dump + ZIP)
config/ Configuration loading, validation, hot-reload
db/ DbDriver interface, wrapper structs, application types
db/types/ ULID-based typed IDs, enums, nullable wrappers
db/audited/ Audited command pattern for change events
db-sqlite/, db-mysql/, db-psql/ sqlc-generated code (do not edit)
definitions/ CMS format definitions (Contentful, Sanity, Strapi, WordPress)
deploy/ Content sync: export, import, push, pull, snapshots
email/ Transactional email (SMTP, SendGrid, SES, Postmark)
install/ Setup wizard and bootstrap
media/ Image optimization, S3 upload
middleware/ CORS, rate limiting, sessions, RBAC authorization
model/ Domain structs (Node, Datatype, Field)
plugin/ Lua plugin system (gopher-lua)
publishing/ Publish, snapshot, version management
registry/ Project registry (~/.modula/configs.json)
remote/ RemoteDriver -- DbDriver over Go SDK (HTTPS)
router/ HTTP route registration, slug handling, globals, pagination
service/ Service layer (SchemaService with composable store interfaces)
transform/ Content format transformers
tui/ Bubbletea TUI (Screen interface, PanelScreen base, 12+ screens)
utility/ Logging (slog), version info, helpers
validation/ Composable field validation rules
mcp/ MCP server (40+ tools for AI-assisted CMS management)
sql/
schema/ 27 numbered schema directories (DDL + queries per dialect)
sqlc.yml sqlc configuration
sdks/
typescript/
types/ @modulacms/types (shared types, branded IDs, enums)
modulacms-sdk/ @modulacms/sdk (read-only content delivery)
modulacms-admin-sdk/ @modulacms/admin-sdk (full admin CRUD)
go/ Go SDK (generic Resource[E,C,U,ID] pattern)
swift/ Swift SDK (SPM, zero dependencies, async/await)
deploy/
docker/ Docker Compose files per database
See LICENSE for details.