Building the Foundations: Libraries, Objects, and Requirements in Simple GRC

Over the past few weeks, I’ve been shaping the core structure behind Simple GRC — specifically how all the governance, risk, and compliance content connects together.
I wanted something clean, logical, and scalable. GRC data tends to get messy fast — one framework references another, controls overlap, requirements duplicate, and every “simple mapping” turns into a web of dependencies.

To handle that without losing my mind, I built a model centered around Libraries, Objects, and Requirements.
It sounds simple (and that’s the point), but there’s quite a bit going on under the hood.


📚 Libraries — the Source Layer

The libraries table defines the highest level — the source of content.

ColumnTypeNotes
idbigintPrimary key
namestringe.g., “NIST 800-53”, “CIS Controls”, “Internal Policy Framework”
slugstringUsed in URLs for cleaner routing
descriptiontextShort description shown under the library name
categoryenumDefines if it’s a Control, Risk, Policy, etc.
is_activebooleanUsed for toggling availability in dropdowns

Each library can have types — stored in a related table library_types, linked via library_id.
For example, the “Controls” library might have Preventative, Detective, and Corrective as its types.
This allows one library to handle variations without creating multiple separate libraries.


🧩 Objects — the Content Layer

Objects are the individual building blocks that live inside libraries.

ColumnTypeNotes
idbigintPrimary key
library_idbigintFK to libraries
type_idbigintFK to library_types
namestringObject title
descriptiontextOptional, shown under the title
parent_idbigint (nullable)Allows hierarchical grouping
orderintSort order
instance_idbigintMulti-instance separation

Objects belong to a library, and their type filters are context-aware — meaning when you switch tabs on the UI, only the types relevant to that library show up.

Page Logic:

Each Library page uses a Livewire component (LibraryObjects) that handles:

  • loading all objects under the selected library (with pagination)
  • applying the selected type_id filter (if any)
  • showing a pill next to each object with the count of its total requirements
  • lazy-loading counts via subqueries to avoid slow joins

When a tab is switched, the tab state persists in the URL (e.g. /libraries/objects?tab=controls&type=preventative), so if you navigate away and come back, you land exactly where you left off.

Each object card shows:

  • title and description
  • a small badge like 12 reqs if requirements exist
  • a dropdown to access deeper details or mappings

If no requirements exist, the badge simply doesn’t render — keeping the UI clean.


✅ Requirements — the Action Layer

Requirements are the actual actionable elements tied to objects.

ColumnTypeNotes
idbigintPrimary key
object_idbigintFK to library_objects
parent_idbigint (nullable)For nested or dependent requirements
descriptiontextCore requirement content
is_mandatorybooleanIndicates if it’s a must-have
weightintRelative importance or control weight
instance_idbigintKeeps multi-instance separation

The recursive relationship through parent_id allows the system to display a tree of requirements.
The total count of all descendant requirements for each object is pre-calculated using a recursive CTE or cached accessor.
That number is displayed as the pill indicator under each object, giving a quick overview of scope or implementation complexity.


⚙️ Livewire Logic and Filtering

Each tab and component relies on a lightweight Livewire setup:

  • A root component handles library switching and type filters.
  • Child components (LibraryObjects, ObjectRequirements) manage data specific to the selection.
  • The selected tab and filter are passed through the query string to keep navigation smooth and back-button friendly.
  • For large libraries, requirements counts are calculated asynchronously after the main list renders — this avoids slow initial loads.

Example:

public function getRequirementCountProperty()
{
    return Requirement::where('object_id', $this->id)
        ->with('children')
        ->get()
        ->flatMap(fn($r) => $r->allDescendants())
        ->count();
}

🎨 UI Behavior

The interface is built in Tailwind + Livewire:

  • Tabs across the top for libraries (sticky header)
  • Filter dropdown for type selection (dynamic per library)
  • Object cards in a grid layout with responsive wrapping
  • Requirement counts as rounded badges
  • Descriptions in small text under each object title

The layout is designed to adapt automatically — no fixed heights, full responsiveness, and clean transitions between libraries and filters.


🧠 Why This Structure Works

This model scales well because:

  • Libraries define context.
  • Objects define content units.
  • Requirements define actionable obligations.

Each layer is tightly scoped yet loosely coupled — meaning you can expand horizontally (add new libraries and types) or vertically (add new depth to requirements) without breaking existing relationships.

It also supports custom fields, which will be introduced next. Each library and library+type combination will be able to define its own field set — for example, a “Preventative Control” could have a “Detection Method” field that doesn’t apply to other types.


🧭 What’s Next

The next iterations in Simple GRC will focus on:

  • Implementing custom field definitions per library and type.
  • Adding mapping relationships between objects (Control ↔ Risk ↔ Policy).
  • Enabling requirement inheritance across linked objects.
  • And eventually, building an intuitive Mappings UI where users can connect anything to anything — visually and interactively.

The goal has always been simplicity — a system where GRC data feels structured but not rigid, relational but still intuitive.
The Library → Object → Requirement model is now doing that — turning a messy web of frameworks, controls, and mandates into something clean, traceable, and actually usable.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *