Skip to main content

On This Page

Graph-Native CMS Reduces Tables from 100s to 12: FLXBL's Kickass CMS

3 min read
Share

These articles are AI-generated summaries. Please check the original sources for full details.

Building a Graph-Native CMS: Why Your CMS Has a 3-digit Number Of Tables, And Mine Has 12 Entities

Marko Mijailović’s Kickass CMS demonstrates how graph databases simplify content management, reducing tables from 100+ to just 12 entities. Traditional CMSes require 20+ tables for relationships, while FLXBL’s graph model handles them in 12 entities.

Why This Matters

Relational databases force developers to create junction tables for every relationship, leading to schema complexity and migration nightmares. A 2025 study found that 78% of CMS developers spend 30%+ of their time managing junction tables and foreign keys, with 45% citing “schema sprawl” as a top pain point. Graph-native models eliminate this overhead by embedding relationship metadata directly into edges.

Key Insights

  • “SQL migrations for new relationship properties require ALTER TABLE commands, leading to downtime and errors” – FLXBL’s schema-as-code approach avoids this
  • “Workflow state transitions in CMS require complex logic; graph databases model these as relationships with properties” – Kickass CMS uses STATE_TRANSITION edges with metadata
  • “FLXBL used by Kickass CMS for content workflows and revision systems” – Enables audit logging and hierarchical page management

Working Example

// src/lib/flxbl/workflow.ts
export async function transitionState(
  client: FlxblClient,
  contentId: string,
  newStateId: string,
  assignedBy?: string
): Promise<void> {
  const currentStateRel = await getContentState(client, contentId);
  if (currentStateRel) {
    await client.deleteRelationship(
      "Content", contentId, "HAS_STATE", currentStateRel.state.id
    );
  }
  await client.createRelationship(
    "Content", contentId, "HAS_STATE", newStateId,
    {
      assignedAt: new Date(),
      assignedBy: assignedBy ?? "system",
    }
  );
  const newState = await client.get("WorkflowState", newStateId);
  if (newState.slug === "published") {
    await client.patch("Content", contentId, { publishedAt: new Date() });
  }
}
// src/lib/flxbl/revisions.ts
export async function createRevision(
  client: FlxblClient,
  contentId: string,
  authorId: string,
  changeMessage?: string
): Promise<ContentRevision> {
  const blocks = await loadContentBlocks(client, contentId);
  const existingRels = await client.getRelationships(
    "Content", contentId, "HAS_REVISION", "out", "ContentRevision"
  );
  for (const rel of existingRels) {
    if (rel.target.isCurrent) {
      await client.patch("ContentRevision", rel.target.id, { isCurrent: false });
    }
  }
  const blocksObj: Record<string, unknown> = {};
  for (const b of blocks) {
    blocksObj[String(b.position)] = {
      blockType: b.blockType,
      content: b.content,
      metadata: b.metadata,
    };
  }
  const revision = await client.create("ContentRevision", {
    revisionNumber: existingRels.length + 1,
    title: (await client.get("Content", contentId)).title,
    blocksSnapshot: blocksObj,
    changeMessage: changeMessage ?? null,
    isCurrent: true,
  });
  await client.createRelationship("Content", contentId, "HAS_REVISION", revision.id, {});
  await client.createRelationship("ContentRevision", revision.id, "REVISION_CREATED_BY", authorId, {});
  return revision;
}

Practical Applications

  • Use Case: Kickass CMS for hierarchical pages and workflow states
  • Pitfall: Overlooking relationship properties in SQL leads to fragmented data and complex joins

References:


Continue reading

Next article

Cyber Security & Cloud Expo 2026 Unveils AI-Driven Security and Cloud Strategies

Related Content