Skip to content

Structure / Feature Plugin System (Code Level)

C4 Level 4 detail of the PSI frontend's core architectural pattern: how Structures define content types and Features extend them via config slots.

Core Concepts

classDiagram
    class Structure {
        +key: string
        +name: string
        +screens: Screen[]
        +defaultConfig: Config
    }

    class Feature {
        +key: string
        +name: string
        +config: Partial~Config~
        +parentFeature: string?
    }

    class Config {
        +widgets: Widget[]
        +callbacks: Callback[]
        +data: DataConfig
    }

    class Screen {
        +key: string
        +component: ReactComponent
    }

    class Widget {
        +key: string
        +priority: number
        +component: ReactComponent
    }

    class Callback {
        +key: string
        +callback: Function
    }

    Structure "1" --> "*" Screen : defines
    Structure "1" --> "1" Config : defaultConfig
    Feature "*" --> "1" Structure : extends
    Feature "1" --> "1" Config : partial config overlay
    Config "1" --> "*" Widget : contains
    Config "1" --> "*" Callback : contains

Config Resolution Algorithm

When a structure instance is rendered, all enabled features are merged into the final configuration:

sequenceDiagram
    participant Instance as Structure Instance
    participant ConfigResolver as Config Resolver
    participant Structure as Structure Definition
    participant Feature1 as Feature: comment-slider
    participant Feature2 as Feature: reactions
    participant Feature3 as Feature: pin-comment

    Instance->>ConfigResolver: Resolve config for this instance

    ConfigResolver->>Structure: Get defaultConfig
    Structure-->>ConfigResolver: Base config with empty slots

    ConfigResolver->>Instance: Get enabled features list
    Instance-->>ConfigResolver: [comment-slider, reactions, pin-comment]

    ConfigResolver->>Feature1: Get config overlay
    Feature1-->>ConfigResolver: {pageTopWidgets: [SliderWidget]}

    ConfigResolver->>Feature2: Get config overlay
    Feature2-->>ConfigResolver: {commentActionWidgets: [ReactionBar]}

    ConfigResolver->>Feature3: Get config overlay
    Feature3-->>ConfigResolver: {commentHeaderWidgets: [PinButton]}

    Note over ConfigResolver: Merge rules:<br/>1. Arrays are APPENDED<br/>2. REPLACE() wrapper fully replaces<br/>3. Dedup by key, highest priority wins

    ConfigResolver-->>Instance: Final merged config

Config Slot Types

graph TD
    subgraph "Config Slot Categories"
        Widgets["Widget Slots<br/>(React components inserted into UI positions)"]
        Callbacks["Callback Slots<br/>(Event handlers and post-processors)"]
        Data["Data Slots<br/>(Labels, settings, rankers, data requirements)"]
    end

    subgraph "Widget Slot Examples"
        PTW["pageTopWidgets: [CommentSlider, ContextEntryPoints]"]
        CAW["commentActionWidgets: [ReactionBar, UpvoteButton]"]
        CHW["commentHeaderWidgets: [PinButton]"]
        CSW["composerSubWidgets: [PollCreator]"]
        ABW["aboutBannerWidgets: [CommunityGuidelines]"]
    end

    subgraph "Callback Slot Examples"
        CPC["commentPostCheckers: [checkModeration, checkLength]"]
        CPP["commentPostProcessors: [translateContent]"]
        OLA["onLoadActions: [loadRankings]"]
    end

    subgraph "Data Slot Examples"
        RNK["rankers: [qualityRanker, bridgingRanker]"]
        LBL["labels: {submitButton: 'Share', ...}"]
        FLT["filters: [hideBlocked, hideSpam]"]
    end

    Widgets --> PTW
    Widgets --> CAW
    Widgets --> CHW
    Widgets --> CSW
    Widgets --> ABW
    Callbacks --> CPC
    Callbacks --> CPP
    Callbacks --> OLA
    Data --> RNK
    Data --> LBL
    Data --> FLT

Merge Rules in Detail

graph TD
    subgraph "Rule 1: Array Append"
        A1["Structure default:<br/>pageTopWidgets: [A]"] --> M1["Feature adds:<br/>pageTopWidgets: [B]"]
        M1 --> R1["Result:<br/>pageTopWidgets: [A, B]"]
    end

    subgraph "Rule 2: REPLACE Wrapper"
        A2["Structure default:<br/>rankers: [defaultRanker]"] --> M2["Feature uses REPLACE:<br/>rankers: REPLACE([customRanker])"]
        M2 --> R2["Result:<br/>rankers: [customRanker]<br/>(default is replaced entirely)"]
    end

    subgraph "Rule 3: Key Deduplication"
        A3["Feature A adds:<br/>{key: 'submit', priority: 1, ...}"] --> M3["Feature B adds:<br/>{key: 'submit', priority: 10, ...}"]
        M3 --> R3["Result: Feature B wins<br/>(higher priority)"]
    end

Structure Registry (All 18)

graph TD
    subgraph "Multi-Instance Structures"
        Question["Question<br/><em>A question users answer</em>"]
        Profile["Profile<br/><em>User profile page</em>"]
        Article["Article<br/><em>Content linked to questions/topics</em>"]
        Topic["Topic<br/><em>Threaded discussion topic</em>"]
        SimpleComments["Simple Comments<br/><em>Basic comment thread</em>"]
    end

    subgraph "Singleton Structures"
        Admin["Admin<br/><em>Admin dashboard</em>"]
        EditorialDash["Editorial Dash<br/><em>Create/browse questions</em>"]
        ModerationDash["Moderation Dash<br/><em>Content moderation UI</em>"]
        ModerationDashLegacy["Moderation Dash (Legacy)<br/><em>Old moderation interface</em>"]
        Login["Login<br/><em>SSO provider selection</em>"]
        Account["Account<br/><em>User's own account</em>"]
        Settings["Settings<br/><em>User preferences</em>"]
        DevTools["Developer Tools<br/><em>Debug and inspect</em>"]
        ComponentDemo["Component Demo<br/><em>Design system showcase</em>"]
        TestStructure["Test Structure<br/><em>Automated testing</em>"]
    end

    subgraph "ZDF Partner Structures"
        ZDFModDash["ZDF Mod Dashboard<br/><em>ZDF-specific moderation</em>"]
        VideoVoting["Video Voting<br/><em>Video-based voting UI</em>"]
    end

Feature Extension Example: Question Structure

graph TD
    Q["Question Structure<br/>(defaultConfig)"]

    Q --> CS["comment-slider<br/>Fills: pageTopWidgets<br/><em>Swipeable comment cards</em>"]
    Q --> Reactions["reactions<br/>Fills: commentActionWidgets<br/><em>Emoji reactions</em>"]
    Q --> Mod["moderation<br/>Fills: commentPostCheckers<br/><em>AI moderation gate</em>"]
    Q --> Pin["pin-comment<br/>Fills: commentHeaderWidgets<br/><em>Pin to top</em>"]
    Q --> Poll["poll-question<br/>Fills: composerSubWidgets<br/><em>Add polls to questions</em>"]
    Q --> CEP["context-entry-points<br/>Fills: pageTopWidgets<br/><em>Links to related content</em>"]
    Q --> RP["representing-perspectives<br/>Fills: pageTopWidgets<br/><em>Perspective visualization</em>"]
    Q --> MP["most-popular<br/>Fills: data.rankers<br/><em>Popular comment sorting</em>"]
    Q --> MA["multi-answer<br/>Fills: composerSubWidgets<br/><em>Multiple answers per user</em>"]
    Q --> TP["thread-pagination<br/>Fills: data, callbacks<br/><em>Paginated comment loading</em>"]

    Mod --> AIMod["sub: ai-moderation<br/><em>GPT-based auto-moderation</em>"]
    Mod --> ManMod["sub: manual-moderation<br/><em>Human review queue</em>"]

Feature Enablement Per Instance

graph TD
    subgraph "Silo: ZDF"
        I1["Question Instance 1<br/>Features: comment-slider, reactions, moderation, pin-comment"]
        I2["Question Instance 2<br/>Features: poll-question, moderation, representing-perspectives"]
    end

    subgraph "Silo: CBC"
        I3["Question Instance 3<br/>Features: multi-answer, moderation, thread-pagination"]
    end

    Note1["Same structure, different<br/>features per instance"]

    subgraph "Feature Defaults"
        SD["Silo defaults (psi.config.ts)<br/>Define which features are on by default"]
        ID["Instance overrides<br/>global/features/{featureKey} = true/false"]
    end

    SD --> I1
    SD --> I2
    SD --> I3
    ID --> I1
    ID --> I2
    ID --> I3

Further Reading