Skip to content

Core Relationship Map

Capell Core Page-centred relationship map

This page maps the core Capell model relationships without listing every schema column. Use it when you need the shape of the system before reading migrations or model classes.

blueprints is the registry for pages, sites, themes, and other blueprint-backed records. There is no separate core types table.

The small Page-centred map above is generated from the canonical Mermaid source. It keeps the README vocabulary focused on the identity records; the detailed tables and operational maps below retain the wider relationship notes.

Start at Site. A site has one default language and one active theme. It owns domains, pages, site-specific layouts, and localized URLs.

Page is the main content record. It belongs to a site, layout, and blueprint, and it sits inside a nested tree. Its public path lives in PageUrl; its translated title, content, and metadata live in Translation.

One Page identity branching to language-scoped Translation and PageUrl records with SiteDomain host context

How does one article have two localised URLs? The English title and /en/coast path, and French title and /fr/cote path, are illustrative records for the same Page, not automatically generated defaults. Translations and Page URLs carry language scope. PageUrl::siteDomain() joins on site_id and language_id; it does not use a standalone domain foreign key. Both language contexts belong to the same Site, and separate hosts per language are not required.

Canonical Mermaid source · Public page URL lookup contract

flowchart LR
Page["Page"]
Site["Site"]
Layout["Layout"]
Blueprint["Blueprint"]
Translation["Translation"]
Media["Media"]
AssetAttachment["Asset attachment"]
RoleRestriction["Role restriction"]
Role["Role"]
GraphEdge["Content graph edge"]
Audit["Audit"]
Page -.->|"media owner"| Media
Site -.->|"media owner"| Media
Layout -.->|"media owner"| Media
Blueprint -.->|"media owner"| Media
Translation -.->|"media owner"| Media
Page -.->|"related record"| AssetAttachment
Media -.->|"attached asset"| AssetAttachment
Blueprint -.->|"restrictable page behaviour"| RoleRestriction
Role -->|"allowed role"| RoleRestriction
Page -.->|"source or target"| GraphEdge
Media -.->|"source or target"| GraphEdge
Site -->|"optional graph scope"| GraphEdge
Translation -.->|"auditable record"| Audit
Page -.->|"auditable record"| Audit
Site -.->|"auditable record"| Audit

Dashed lines are polymorphic or logical relationships. They are real application relationships, but they are not always hard foreign keys because packages can add more content models later.

media is owned by Spatie Media Library and extended by Capell’s Media model. asset_attachments lets content records point at reusable assets without hard-coding one asset table. content_graph_edges stores derived references for impact analysis.

flowchart LR
User["User"]
UpgradeRun["Upgrade run"]
UpgradeEvent["Upgrade run event"]
UpgradeLog["Upgrade log"]
Extension["Extension state"]
MarketplaceInstall["Marketplace install"]
HealthAlert["Extension health alert"]
DeletionBatch["Deletion batch"]
DeletionRecord["Deleted model record"]
ContentLock["Content lock"]
Notification["Admin notification subscription"]
User -->|"starts"| UpgradeRun
UpgradeRun -->|"records"| UpgradeEvent
UpgradeLog -->|"tracks package step"| Extension
Extension -.->|"composer name"| HealthAlert
MarketplaceInstall -.->|"install id"| HealthAlert
DeletionBatch -->|"contains"| DeletionRecord
User -->|"holds"| ContentLock
User -.->|"subscribes to"| Notification

The extension and marketplace tables intentionally use stable string keys such as composer_name, install_id, extension_slug, and affected_install_id instead of foreign keys. This lets Capell record package health and marketplace state even when an extension is missing, disabled, or no longer installed.

TableSourceHow core uses it
usersHost Laravel appUserstamps, content locks, upgrade runs, admin notification subscriptions, and audit users.
rolesSpatie PermissionPage-type access restrictions through page_role_restrictions.role_id.
mediaSpatie Media LibraryCapell media collections and media metadata.
auditsOwenIt Laravel AuditingActivity/audit trail for auditable core models.
RelationshipTypeNotes
pages.parent_id -> pages.idSelf-referenceBacked by nested set columns for hierarchy.
page_urls.pageable_*PolymorphicRoutes page-like records; core primarily uses Page.
translations.translatable_*PolymorphicLocalizes records; core primarily uses Page, Site, Media, and related content models.
media.model_*PolymorphicSpatie Media Library ownership for Capell media collections.
asset_attachments.related_* / asset_*PolymorphicJoins an owning record to an attached asset record.
content_graph_edges.source_* / target_*Logical polymorphicDerived graph edges for impact analysis, scoped optionally by site/language.
content_locks.model_*Logical polymorphicOne active lock per model record.
deletion_batches.root_* / deletion_batch_records.model_*Logical polymorphicTracks restore groups without coupling to package tables.

Page soft deletion records the live subtree in these batch tables before deleting descendants. Restoration and its permission checks use the same recorded membership, so crossing a clock second cannot lose a child or include independently deleted trash. Deletion re-reads current page state and nested-set bounds under row locks; a stale instance of an already-trashed page cannot record a second cascade. Membership is consumed per restored or permanently purged member, and empty Page batches are pruned. A moved subtree retains its own membership until it is restored or purged; a later genuine Page deletion supersedes earlier Page-batch membership for that member only. It preserves Site-owned membership recorded before the Site cascade. Site restoration consumes Page-owned membership for the pages it restores while retaining its own Site history.

Restoration checks every member’s old URLs before writes using the router’s enabled, site-and-language scope and the database’s collation. It restores standard URL and translation relations in two statements. Members are written parent-first; ordinary Eloquent model events and database auditing run inside the transaction. Only Core’s restore notification, cache invalidation and content-graph work wait until the outermost transaction commits. Refusal or rollback discards that deferred work. The vendor timestamp hook performs no second restore. See page restoration for the authorisation boundary, extension restrictions and Site restoration.

Historical trash without a recorded page batch must be restored explicitly; its trashed ancestors are restored too, while descendant membership is never guessed from deleted_at. Admin recovery names only descendants allowed by the page view policy and explains separate recovery. Inaccessible descendants are neither named nor counted. Recently Deleted applies the same view policy before rendering names or totals and displays the recovery guidance before any action.