Files
James MaesandClaude Sonnet 5.5 3fb67aac99
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
Shared Java Maven CI / secrets (push) Successful in 33s
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow f1d46718955078331817c6206b822a94bc7d769f] (pull_request) Successful in 12s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 9s
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / secrets (pull_request) Successful in 30s
Shared Java Maven CI / verify (push) Successful in 1m31s
Shared Java Maven CI / gate (push) Successful in 20s
qrun-release / release (push) Successful in 56m59s
qrun-release / postpublish (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / verify (pull_request) Successful in 1m33s
QRun-IO/ci-policy: Shared Java Maven CI / gate (pull_request) Successful in 26s
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Successful in 58m32s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 10s
Release candidate / release-rc (pull_request) Skipped
feat(cms): implement the CMS CORE feature on QQQ 4.1
Implement the CORE feature area as standard QQQ tables: site, domain,
redirect, redirect type, page, page type, page section, page section type,
snippet, menu and menu item. Add the CMS app with Sites, Pages and
Navigation homes, CmsFeature flags with dependency validation, per-table
permissions with CmsRole bundles and openReads, a startup schema check
(FAIL_FAST, DEGRADED, SKIP), a prefix-parameterized Liquibase changelog and
the Postgres DDL.

Add unit tests, Postgres-backed Testcontainers install and runtime-identity
tests, and a local demo server used to produce the docs screenshots.

Build against the published QQQ 4.1 BOM (4.1.0-RC.21), add the test-scope
Postgres, Javalin, frontend-next and Testcontainers dependencies, and
enforce checkstyle at validate.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 05:28:20 -06:00

6.5 KiB

QBit CMS

License Java

QQQ QBit: a headless CMS (sites, pages, menus, blog, media, projects, photos and publishing) as standard QQQ tables.

Status: design approved in QRun-IO/qqq#1083 (see docs/DESIGN.md). The CORE feature is implemented; MEDIA, BLOG, PROJECTS, PHOTOS, PUBLISHING and STREAM are next. Nothing is published yet.

The CMS is extracted from the kof22 website, which runs it in production. Its data lives in ordinary QQQ tables in your own database, secured by QQQ permissions. It brings an admin app with one home per area, and a schema you run with your own Liquibase.

Requirements

  • Java 21.
  • QQQ 4.1 (built against 4.1.0-RC.4) with an RDBMS backend: PostgreSQL 14+ in production, H2 for tests.
  • QQQ Next UI 1.0 (1.0.0-RC.14 or later). Material is not supported.
  • Liquibase in your migration pipeline (or the reference DDL in docs/ddl/postgres.sql).

Install

  1. Add the dependency com.kingsrook.qbits:qbit-cms (once released).

  2. Run the schema. Put the qbit jar on your Liquibase classpath and include its changelog. Set its properties before the include (the first definition wins):

    databaseChangeLog:
      - property: { name: cms.tablePrefix, value: cms_ }      # must equal CmsQBitConfig.tableNamePrefix
      - property: { name: cms.features, value: CORE }          # the features you install
      - include:  { file: qbit-cms/db/changelog/qbit-cms.changelog.yaml }
    

    The changelog never touches a table it did not create. If a table with one of its names already exists, it halts with a message telling you to choose another prefix. It seeds only generic reference data (redirect, page and section types); set cms.seedReferenceData to false to skip that.

  3. Produce the qbit after your backends (and the parent app, if any) are on the instance:

    new CmsQBitProducer()
       .withQBitConfig(new CmsQBitConfig()
          .withBackendName("rdbms")
          .withRole("cms-admins", CmsRole.ADMIN)
          .withRole("cms-editors", CmsRole.EDITOR))
       .produce(qInstance);
    
  4. Grant permissions from your authentication customizer: CmsSecurity.applySessionPermissions(session, userGroups, config).

At startup the producer checks that the changelog ran for every enabled feature and that the tables exist. startupMode FAIL_FAST (the default) refuses to start and lists what is missing. DEGRADED starts and shows the problem on the CMS home. SKIP does not check (unit tests without a database).

Configuration reference (CmsQBitConfig)

Setting Default Meaning
backendName required The RDBMS backend for every CMS table
tableNamePrefix cms_ Table name prefix (QQQ and SQL names are equal); must equal the changelog's cms.tablePrefix
namePrefix cms Prefix of app and widget names (cmsPages, cmsPagesHome)
permissionBaseName cms Base of qbit-wide permissions (cms.admin, cms.publish)
features every available feature CmsFeature set; CORE is required, and a feature this version does not implement yet is rejected
appName, appLabel, parentAppName cms, CMS, none The top app and where it nests in your app tree
openReads false When true, reading CMS tables needs no permission (writes always do)
startupMode FAIL_FAST FAIL_FAST, DEGRADED or SKIP (see above)
roleMapping empty Identity-provider group name to CmsRole

A namespace (withNamespace("siteTwo")) fills in all three prefixes for a second install in the same instance (site_two_page, siteTwoPages, site_two.admin).

Features

Feature Tables Status
CORE site, domain, redirect, redirect type, page, page type, page section, page section type, snippet, menu, menu item available
MEDIA media, media type, storage SPI (qbit-object-storage for S3) next
BLOG blog, blog type, post, tags, authors, post media, versions next
PROJECTS, PHOTOS, PUBLISHING, STREAM, DATA_HEALTH_RULES see the design planned

The admin

The qbit adds one app (CMS) with a home, and one sub-app per area, each with its own home:

  • Sites: sites (domains are a list on each site) and redirects; Settings has redirect types.
  • Pages: pages (sections and child pages are lists on each page) and snippets; Settings has page types and page section types.
  • Navigation: menus (items are a list on each menu).

The nav shows only main tables and reference data; child tables are edited on their parents' records. Each home shows only real data read as the current user: counts, drafts, inactive rows and recently changed records, readable at any width in light and dark mode. The qbit never sets the instance's home app.

Permissions

Every CMS table has QQQ permission rules. The table permissions are <table>.read, .insert, .edit and .delete (for example cms_page.edit). The qbit-wide permissions are cms.admin and cms.publish.

Role Grants
ADMIN everything, including reference data, cms.admin and cms.publish
EDITOR read everything; write content tables
PUBLISHER EDITOR plus cms.publish
DRAFTER read everything; insert and edit content (no deletes, no publishing)
VIEWER read everything

The qbit is built for QQQ core permission enforcement (qInstance.setEnforcePermissionsInCoreActions(true), the default from QQQ 4.2).

Operational tables

All CMS data is in normal, visible QQQ tables. Nothing is hidden and there is no side store. The schema is in docs/ddl/postgres.sql (generated from the changelog).

Upgrading

Read the CHANGELOG's "Schema changes" before deploying. Run your migrations before the new app version. Released changesets never change; new versions only append changesets.

Adopting in another app

Follow the QQQ adoption checklist once the first release is published.

Build

./mvnw verify

Always use the Maven wrapper (Maven 3.9.11); Maven 3.10 cannot read the parent POM. The PostgreSQL tests use Testcontainers and are skipped when Docker is not available.

License

Apache License 2.0. See LICENSE and NOTICE.