Skip to content

Architecture Differences

This document catalogues every significant difference between the Open Social implementation (pl-opensocial) and the standard Drupal implementation (pl-drupalorg) for groups functionality.


AspectOpen Social (pl-opensocial)Standard Drupal (pl-drupalorg)
BaseOpen Social 13.0.0 distribution (~100 bundled modules)Vanilla Drupal 10 + individually installed contrib
Install profilesocial (custom installer with private file path requirement)standard or minimal
Themesocialblue / social_base (requires ~30 frontend JS libraries)bluecheese (standard Drupal admin/frontend theme)
Frontend libraries30+ manually-copied JS libs (Bootstrap, FontAwesome, select2, photoswipe, etc.)Managed by Drupal core + Composer
PHP8.38.3
DDEVpl-opensocial-rework.ddev.site:8493drupalorg.ddev.site

Note Open Social is a distribution — a pre-assembled package of Drupal core + contrib modules + custom themes + custom modules. Moving to standard Drupal means every dependency must be explicitly installed and configured. The upside is full control over what’s included; the downside is more setup work.


2. User Profiles — The Biggest Difference

Section titled “2. User Profiles — The Biggest Difference”

Open Social uses the Profile module to create a separate profile entity type that stores all personal information:

// Open Social — loading profile data
$profiles = \Drupal::entityTypeManager()
->getStorage("profile")
->loadByProperties(["uid" => $user->id(), "type" => "profile"]);
$profile = reset($profiles);
$first_name = $profile->get("field_profile_first_name")->value;
$org = $profile->get("field_profile_organization")->value;
$bio = $profile->get("field_profile_self_introduction")->value;

pl-drupalorg: Fields directly on the user entity

Section titled “pl-drupalorg: Fields directly on the user entity”

Standard Drupal stores profile data directly on the user entity:

// pl-drupalorg — loading profile data
$user = \Drupal\user\Entity\User::load($uid);
$first_name = $user->get("field_first_name")->value;
$bio = $user->get("field_bio")->value;

Why does Open Social use a profile entity?

Section titled “Why does Open Social use a profile entity?”
  1. Multiple profile types — you can have different profile sets (e.g., “personal profile” vs. “organization profile”) without cluttering the user entity
  2. Access control separation — profile data can have different visibility rules than the user account (e.g., public bio vs. private email)
  3. Fieldable without touching user entity — avoids conflicts with other modules that also add fields to the user entity
  4. Revisionable — profile changes can be tracked independently of account changes
  • Only one profile type is needed
  • Putting fields directly on the user entity is simpler and avoids the extra entity load
  • Fewer moving parts = less to maintain
  • The tradeoff is you lose multi-profile-type capability, but you gain simplicity
Open Social (field_profile_*)pl-drupalorg (field_*)
field_profile_first_namefield_first_name
field_profile_last_namefield_last_name
field_profile_self_introductionfield_bio
field_profile_organizationfield_organization (if added)
field_profile_functionNot available (job title field)
field_profile_imageuser_picture (core field)
field_profile_summaryNot available
field_profile_expertiseNot available
field_profile_interestsNot available

Every module that touches user profile data must be rewritten:

ModuleWhat changes
do_profile_statsProfileCompletenessBlock — must check user entity fields instead of loading profile entity. Complete rewrite.
do_notificationsfollow_user flag targets user entity, not profile entity
Demo data scriptsAll profile population uses $user->set() instead of Profile::create()

AspectOpen Socialpl-drupalorg
Group module versionBundled with OS (Group 2.x API internally)drupal/group ^3.0 (Group 3.x)
Group typeflexible_group (pre-configured with 6+ fields)community_group (created from scratch)
Group admin roleflexible_group-group_managercommunity_group-admin
Group member roleflexible_group-membercommunity_group-member
Group outsider roleflexible_group-outsidercommunity_group-outsider
Relationship entitygroup_content (Group 2.x naming)group_relationship (Group 3.x naming)
Relationship type patternflexible_group-group_node-topiccommunity_group-group_node-forum

Fields on flexible_group that don’t exist on community_group

Section titled “Fields on flexible_group that don’t exist on community_group”

These OS-specific fields must be either recreated or replaced with Group module access plugins:

OS FieldPurposepl-drupalorg equivalent
field_flexible_group_visibilitypublic / community / secretGroup access control plugins
field_group_allowed_join_methoddirect / request / inviteGroup access control plugins
field_group_allowed_visibilityControls which content visibility is allowedNot needed (no content visibility field)
field_group_typeReference to group_type taxonomyMust be added manually (Step 220)
field_group_languageGroup’s preferred languageMust be added manually (Step 630d)

4. Access Control — No Content Visibility Field

Section titled “4. Access Control — No Content Visibility Field”

This is the second biggest difference after the profile entity.

Every node has a field_content_visibility field with three options:

  • public — visible to everyone
  • community — visible only to logged-in users
  • group — visible only to group members

Open Social’s access system reads this field and grants/denies access accordingly.

This field does not exist. Access control is handled entirely by the Group module’s access control plugins, which are configured per group type. Content posted to a group inherits the group’s access rules.

  • Remove all references to field_content_visibility from code and demo data
  • Group access (public/private/secret) is configured at the group type level, not per-node
  • Simpler model but less granular (you can’t have public content in a private group)

Open Socialpl-drupalorgNotes
topicforumRenamed; functionally equivalent
eventeventSame name, different fields
pagepage, documentation, postOS has 1 type; pl-drupalorg splits into 3
documentationNew type for working group docs
postBlog-style posts

Total: 3 group-postable types → 5 group-postable types

Section titled “Total: 3 group-postable types → 5 group-postable types”

The do_multigroup, do_discovery, and do_notifications modules all hard-code content type lists that must be expanded:

// Open Social
$bundles = ['topic', 'event', 'page'];
// pl-drupalorg
$bundles = ['forum', 'documentation', 'event', 'post', 'page'];

AspectOpen Socialpl-drupalorg
Event enrollmentCustom EventEnrollment entity typersvp_event Flag
Date fieldsfield_event_date + field_event_date_endfield_date_of_event (check if daterange)
Enrollment togglefield_event_enroll (boolean)Not available (remove)
Anonymous enrollmentfield_event_an_enroll (boolean)Not available (remove)
Max enrollmentfield_event_max_enroll + field_event_max_enroll_numNot available (add if needed)
Event type taxonomyevent_types (plural, OS default)event_types (must be created)
Sub-modulessocial_event_type, social_event_managers, social_event_an_enroll, social_event_max_enrollNone — standard Drupal fields

Open Social has a rich enrollment entity with fields:

// Open Social
$enrollment = \Drupal\social_event\Entity\EventEnrollment::create([
"field_event" => $node->id(),
"field_enrollment_status" => 1,
"field_account" => $user->id(),
"user_id" => $user->id(),
"field_request_or_invite_status" => NULL,
]);

pl-drupalorg uses a simple flag:

// pl-drupalorg
$flag = \Drupal::service("flag")->getFlagById("rsvp_event");
\Drupal::service("flag")->flag($flag, $event_node, $user);

Tradeoff: Simpler, but you lose per-enrollment metadata (request/invite status, enrollment notes).


AspectOpen Socialpl-drupalorg
Tag vocabularysocial_taggingtags
Tag field on nodessocial_tagging (entity reference)field_group_tags (references group_tags vocabulary)
Group type vocabularygroup_type (OS default)group_type (must be created)
Event type vocabularyevent_types (plural)event_types (must be created)
Tagging modulesocial_taggingNot needed (standard taxonomy)

Open Social rolepl-drupalorg role
contentmanagercontent_administrator
sitemanagersite_moderator
authenticatedauthenticated (same)
administratoradministrator (same)
Permission areaOpen Socialpl-drupalorg
Flag permissionsGranted to contentmanager / sitemanagerGrant to content_administrator / site_moderator
Taxonomy accesstaxonomy_access_fix module overrides selection handler; requires select terms in {vocab} permissionStandard Drupal entity reference — no extra permission needed
Translationsocial_language grants permissions automaticallyMust grant translate any entity, create content translations, etc. manually

9. Notification System — External Delivery

Section titled “9. Notification System — External Delivery”
Node/Comment created
→ hook_entity_insert() creates Activity entity
→ activity_send_email module checks user preferences
→ ActivityDigestWorker batches into daily/weekly digests
→ social_follow_content / social_follow_user / social_follow_tag
determine who receives notifications

Open Social has a complete notification infrastructure: Activity entity, ActivitySendEmail plugin, digest worker, and social_follow_* modules.

pl-drupalorg approach: event recording only

Section titled “pl-drupalorg approach: event recording only”

Drupal does NOT send email. Drupal only records “what happened” as a lightweight queue item. An external system handles everything else.

Node/Comment created
→ hook_node_insert() records event to `do_notifications` queue
{event, entity_type, entity_id, bundle, author_uid, group_ids, timestamp}
→ External system reads queue
→ External system reads flagging table (follow_content, follow_user, follow_term)
→ External system checks suppression (State API), mute flags, frequency preferences
→ External system renders and delivers email

One queue item per event, not per recipient. The external system resolves recipients.

What Drupal provides (for the external system to query)

Section titled “What Drupal provides (for the external system to query)”
DataWhere storedPurpose
Notification eventsqueue table (do_notifications queue)What happened
Content followsflagging table (follow_content flag)Who follows what content
User followsflagging table (follow_user flag)Who follows which users
Term followsflagging table (follow_term flag)Who follows which tags
Group mutingflagging table (mute_group_notifications flag)Who has muted which groups
Per-post opt-outState API (do_notifications_suppress_{nid})Author chose not to notify
User disable-allState API (do_notifications_disabled_{uid})User paused all notifications
Frequency preferencefield_notification_frequency on user entityimmediate / daily / weekly
ComponentPorts?Notes
follow_content flag✅ YesSame Flag module API
follow_user flag⚠️ PartiallyTarget changes from profile entity → user entity
follow_term flag✅ YesSame Flag module API
mute_group_notifications flag✅ YesSame Flag module API
Per-post opt-out checkbox✅ Yeshook_form_node_form_alter() + State API (external system checks the flag)
Notification settings page⚠️ PartiallyURL route and controller work; subscription queries need minor changes
Email sending❌ NoEntirely external — Drupal never sends email

AspectOpen Socialpl-drupalorg
Setupsocial_language module enables 4 translation modules + grants permissions in one stepMust manually enable language, locale, interface_translation, config_translation, content_translation
Activity streamTranslation-aware (activities render in user’s preferred language)Must configure content translation manually for each content type
Language negotiationPre-configured by social_languageMust set via language.types config (user → group → URL → selected)
Translation permissionsAuto-granted to Site ManagerMust explicitly grant translate any entity, create content translations, etc.

AspectOpen Social (socialblue)pl-drupalorg (bluecheese)
Sidebar regioncomplementary_bottomsidebar_first
Content regioncontentcontent (same)
Theme machine namesocialbluebluecheese
Frontend libraries~30 manually-copied JS libsStandard Drupal core JS
CSS strategysocial_base +.libraries.yml per moduleStandard .libraries.yml per module

Every block placement command must change the theme and region values:

// Open Social
"region" => "complementary_bottom", "theme" => "socialblue"
// pl-drupalorg
"region" => "sidebar_first", "theme" => "bluecheese"

ViewOpen Socialpl-drupalorg
Group topics streamgroup_topics (queries group_content)group_content_stream (queries group_relationship)
Group events streamgroup_eventsNeeds new view or adapt existing
Group directorynewest_groupsall_groups
Tags aggregationtags_aggregationtags_aggregation (same, but vocabulary changes)
Hot contenthot_contenthot_content (same structure)
Promoted contentpromoted_contentpromoted_content (same structure)
RSS feedgroup_rss_feedgroup_rss_feed (same structure)
Pending groupspending_groupspending_groups (same structure)
  • Table/entity name: group_content_field_datagroup_relationship_field_data
  • Relationship plugin: Verify group_relationship Views plugin exists in Group 3.x
  • Node table alias: In group_topics view, the node table alias is node_field_data_group_relationship_field_data (not node_field_data) — affects do_group_pin’s SQL join

13. Contributed Modules — Availability Gotchas

Section titled “13. Contributed Modules — Availability Gotchas”
ModuleStatusGotcha
flag✅ Availableflag_count is a sub-module (not a separate Composer package)
statistics⚠️ DeprecatedDeprecated in Drupal 10.3.0, removed in 11.0.0; available as contrib
social_tagging❌ OS-onlyUse standard taxonomy module
social_event_type❌ OS-onlyCreate event_types vocabulary + terms manually
social_event_managers❌ OS-onlyAdd event manager field manually if needed
social_event_an_enroll❌ OS-onlyNot needed (no enrollment entity)
social_language❌ OS-onlyEnable 5 core modules individually
social_follow_content❌ OS-onlyUse flag module with follow_content flag
social_follow_tag❌ OS-onlyUse flag module with follow_term flag
social_follow_user❌ OS-onlyUse flag module with follow_user flag

ModuleDifficultyKey changes
do_group_extras🟡 MediumForm IDs, role names (sitemanagersite_moderator), theme regions
do_multigroup🟡 MediumBundle list (topicforum), group_contentgroup_relationship, theme selectors
do_discovery🟢 LowContent type in hot score query (topicforum)
do_notifications🟢 LowRemove activity_send_email dependency; event recording only (external system delivers email)
do_profile_stats🔴 HighComplete rewrite: profile entity → user entity for completeness check; topicforum for stats
do_group_pin🟡 MediumTarget View ID (group_topicsgroup_content_stream), flag creation, node table alias
do_group_mission🟢 LowBlock region change only (complementary_bottomsidebar_first)
do_group_language🟢 LowBundle name in field load (flexible_groupcommunity_group)
do_wiki🟢 LowNo code changes needed — 100% Drupal-generic

AspectOpen Social DEMO_DATA_PLANpl-drupalorg
Profile storageprofile entity + field_profile_*user entity + field_*
Rolescontentmanager, sitemanagercontent_administrator, site_moderator
Group typeflexible_groupcommunity_group
Group roleflexible_group-group_managercommunity_group-admin
Content typetopicforum
Tag vocabularysocial_taggingtags
Tag fieldsocial_taggingfield_group_tags
Content visibilityfield_content_visibilityNot available (remove)
Event datesfield_event_date + field_event_date_endfield_date_of_event
Event enrollmentEventEnrollment entityrsvp_event flag
Group relationshipgroup_content + group_node:topicgroup_relationship + group_node:forum
Follow user targetprofile entityuser entity