■ Headless CMS · .NET 10 · PostgreSQL
Content, set in modules.
ModularCMS composes pages from reusable blocks, proofs them through review and releases, and delivers them to any frontend through three layers of cache. Media, translations, personalization, consent, forms, and frontend users are built in, and it runs on any cloud or on a single box.
Kubernetes, Docker Compose, or Terraform on AWS · Migrates from Umbraco
- Draft
- Review
- Live
Autosaved · 3 blocks · 4 checks passed
Hero block
Alpenglow opens its winter season
Twelve lifts, two new runs, and a later last chair.
Lift status
Ski school
Webcams
Quote block
“Best opening snow in a decade.”
- status
- 200
- cache
- no-store
- time
- 41 ms
{ "contentType": "article",
"properties": { "sections": { "blockList" } },
"meta": { "preview": true } }Illustrative · timings from the verification run
- Cached response, p95
- 8.5 ms
- at 2,741 requests a second
- Publish to visible, p95
- 0.56 s
- every cache layer updated
- Pages per site
- 250k
- type changes stay instant
- Outbound calls
- 0
- no telemetry, no licence checks
Timings measured on a single-host Docker Compose verification run (September 2026); the platform’s targets are 150 ms for cached responses and 5 s from publish to visible.
§ 01 The composition model
Blocks, not pages. Reuse is the default.
Model content once as element types, then let editors set pages from them. The same block serves every site, language, and channel you deliver to.
- 01
Element types, composed
Block List and Block Grid fields assemble pages from element types with their own content and settings. Pickers, repeatable groups, links, embeds, tables, locations, and date ranges come as field types.
- 02
Change the model at any size
Values live as JSONB inside a relational shell, so adding or reshaping a field on a 250,000-page site saves immediately: no lock, no migration, no reindex.
- 03
One block, many audiences
A block can carry variants per audience segment and declares the consent category of anything third-party it loads, right in the Delivery API response.
§ 02 What's inside
Everything a content team needs, in one proof.
Not a starter kit and a list of integrations: the features large sites need are part of the platform, tested together.
Governance that ships
Multi-stage approvals, timezone-true scheduling, content locks, and releases that bundle many pages into one atomic publish you can preview, approve, and roll back.
Cloudinary-style media
Upload an original once. Sizes, focal-point crops, formats, and adaptive video are produced from the URL and served from the CDN, never from the app.
Translations as children
Each translation sits under its master item with its own status, review, and XLIFF round trip. Right-to-left and CJK work end to end.
Personalization that keeps the cache
Segments, block and field variants, and recommendation slots, chosen by a separate uncached decision so every content response stays cacheable.
Consent, built in
Jurisdiction-aware banners, script gating, Global Privacy Control, and cookie scanning, delivered by the same one-tag loader.
Many tenants, one deployment
Organization, instance, site. Every row, cache key, and storage path is scoped by instance and enforced by PostgreSQL row-level security.
Frontend users and forms
Member pools with single sign-on across domains, self-service accounts, gated content, and forms whose submissions are encrypted per instance.
Search, SEO, and monitoring
An SEO tab that fills itself, checks that can block a publish, sitemaps and redirects, tunable search, and a crawler watching for broken links and Core Web Vitals.
§ 03 Delivery
One publish. Three caches. Nothing stale.
Each cache layer has one job and one way to invalidate it, so a cache problem is always traceable to a single place and a publish never waits on a purge.
Layer 1
Edge cache
- Where
- Any CDN: CloudFront, Front Door, Cloudflare, Fastly
- Holds
- Full responses keyed on the site's publish version
- Invalidated
- A new version makes old entries unreachable. No purge storms.
Layer 2
Output cache
- Where
- In process, on every API node
- Holds
- Composed documents with a tag for every item they depend on
- Invalidated
- Evicted by tag the moment a picked author, image, or type changes
Layer 3
Distributed cache
- Where
- Redis or Valkey
- Holds
- Publish versions, routing tables, locks, rate limits, queues
- Invalidated
- Written by the publishing pipeline; degrades safely if Redis goes away
01An editor, a schedule, or a release publishes.
02The pipeline works out every affected item: ancestors, referrers, translations.
03The site's publish version moves on, and the tag set goes out over Redis.
04Every API node evicts matching entries; search, sitemap, and redirects update.
05Signed webhooks tell consumer sites to revalidate, carrying the new version.
06The next request is composed once from PostgreSQL and fills both caches again.
What personalization does not touch
Variants, recommendations, and member content never vary a cached response.
Decisions come from a separate uncached endpoint and are applied by the loader in the browser. Preview, member-only content, and anything personal is marked no-store and never reaches the edge.
§ 04 For developers
Your frontend. Your framework. Typed all the way down.
Public sites are your own apps. ModularCMS gives them a versioned Delivery API, a generated SDK, a Next.js adapter, one script tag for widgets, and a CLI for your pipeline.
- One OpenAPI document generates the SDK, the backoffice client, and the reference
- An API explorer in the backoffice with Try it and curl, fetch, and SDK snippets
- Structure as code: plan, diff, and apply content types from Git
- Two-way Git sync for structure, with GitHub pull requests for releases
- Signed plugin packages and backoffice extension points
app/[...slug]/page.tsx
import { Blocks, getDeliveryClient, seoToMetadata, cacheTags } from "@modularcms/next";
import { expand } from "@modularcms/delivery";
import { Hero, CardGrid, Quote } from "@/components/blocks";
export async function generateMetadata({ params }) {
const path = "/" + (await params).slug.join("/");
const page = await getDeliveryClient().content.item(path);
return page ? seoToMetadata(page.seo, page) : {};
}
export default async function Page({ params }) {
const path = "/" + (await params).slug.join("/");
const cms = getDeliveryClient({ tags: [cacheTags.path(path)] });
const page = await cms.content.item(path, { expand: expand({ blocks: true }) });
return (
<Blocks
items={page?.properties.sections?.items}
components={{ hero: Hero, cardGrid: CardGrid, quoteBlock: Quote }}
/>
);
}The adapter wires draft mode, cache tags, SEO metadata, images, rich text, menus, and personalization into the App Router. The same page code renders drafts and published content.
§ 05 Hosting
Runs on any cloud. Or on none.
Four portable services are all the core needs. Everything else is optional and has an in-platform fallback, so you choose the cloud, and can leave it.
Required
- PostgreSQL 16+
- Redis or Valkey
- Keycloak
- A storage backend
A local volume on a single host, or S3-compatible, Azure Blob, or Google Cloud Storage for many nodes.
Optional, with fallbacks
- OpenSearch / Elasticsearch
- Any CDN
- Email service
- Analytics providers
- AI providers
Search falls back to PostgreSQL full-text search; analytics to the first-party store.
Deploy with
Kubernetes
Helm chart with migrations as a hook, rolling restarts, probes
Docker Compose
The whole platform on one host, for small teams and local work
Terraform
Modules for managed dependencies, AWS first; nothing requires AWS
No telemetry. No licence checks. No calls home.
The only outbound connections are the update check and the store connection, and both are off until you turn them on.
§ 06 Coming from Umbraco
Bring the site. Keep the ideas you like.
ModularCMS borrows the good parts of Umbraco 17 — document types, Block List and Block Grid, dictionary items, public access, culture and hostnames — and imports real Umbraco sites, Umbraco Cloud included.
Migration · alpenglow-umbraco
rehearsed- Document types → content types
- 38
- Element types and blocks
- 61
- Content nodes and translations
- 12,406
- Media from the Azure Blob container
- 8,915
- Dictionary items
- 402
- Redirects kept from Umbraco
- 1,137
Illustrative · rehearse into staging, compare, then run for real
§ 07 Roadmap
Version 1.0 is the foundation. Here's what gets set next.
Phases 1 and 2 ship together as 1.0. AI, the widget builder, and the marketplace follow, built on hooks that are already in place.
Phase 1in 1.0
Foundation
The core CMS, media, consent, the Delivery API, tenancy, hosting, and the Umbraco import.
Phase 2in 1.0
Analytics, users, personalization
Analytics, full frontend users, forms, personalization, releases, developer tooling, and integrations.
Phase 3next
AI
AI with your own keys or metered platform keys, budgets, and every call accounted for.
Phase 4next
Widgets and catalog
A widget builder, the component catalog, visual editing on the live site, and co-editing.
Phase 5next
Marketplace
The ModularCMS Store for publishing, licensing, and selling packages.
■ Ready for press
See your content set in modules.
We’ll walk through the backoffice with your own site in mind, or import a copy of your Umbraco site and show you the result.