Skip to content

How We Build in Drupal: The Upstream-First Operating Manual


1. The Drupal Upstream Supply Chain

Bluefly does not treat Packagist as the canonical distribution channel for Drupal contrib. Drupal contrib packages are resolved through Drupal.org's Composer repository:

https://packages.drupal.org/8

Drupal.org documentation explicitly notes that most contributed Drupal modules are not published on Packagist and should instead be resolved through the Drupal Composer repository.

graph LR
    C["Drupal Composer Repository<br/>packages.drupal.org/8"] --> |"Modules, Themes, Recipes"| D["Drupal Runtime<br/>ContextControl.ai / Drupal Sites"]
    G["DrupalCode GitLab<br/>git.drupalcode.org/project/*"] --> |"Canonical Upstream Source"| D
    N["NPM<br/>@drupal-canvas/* and Drupal JS packages"] --> |"Canvas / Frontend Tooling"| D
Channel Canonical Location Role Key Artifacts
Drupal Composer Repository https://packages.drupal.org/8 Primary Drupal PHP dependency distribution drupal/* modules, themes, recipes, Drupal core packages
DrupalCode https://git.drupalcode.org/project/<project> Canonical upstream source and development history source, branches, merge requests, CI, release tags
Drupal.org Project Pages https://www.drupal.org/project/<project> Release, security, compatibility, ecosystem and documentation discovery supported versions, security coverage, ecosystem links, issue queues
NPM npmjs.com JS/frontend/developer tooling @drupal-canvas/cli, @drupal-canvas/create, @drupal-canvas/eslint-config, Drupal JS packages

Composer Rule

Drupal dependencies must be installed from the site's Composer authority.

Normal form:

composer require drupal/<project>

Examples:

composer require drupal/ai_context
composer require drupal/tool
composer require drupal/tool_belt
composer require drupal/eca
composer require drupal/ai_integration_eca
composer require drupal/api_orchestrator
composer require drupal/key
composer require drupal/canvas

Drupal.org recommends Composer for contributed modules and themes and uses the drupal/<machine_name> namespace.

Do not manually download contrib archives into the codebase.

Do not edit:

web/modules/contrib/*
web/themes/contrib/*
vendor/*
Fix the package, dependency, Recipe, configuration, or upstream owner instead.


2. Upstream-First Is a Decision Tree

Before Bluefly writes Drupal code:

CORE
  ↓
STABLE CONTRIB
  ↓
CONTRIB CONFIGURATION
  ↓
DRUPAL CONFIGURATION
  ↓
RECIPE
  ↓
CONFIG ACTION
  ↓
CANVAS / SDC
  ↓
ECA / MODELER / FLOWDROP
  ↓
DRUPAL AI
  ↓
AI AGENTS
  ↓
TOOL API
  ↓
TOOL BELT
  ↓
MCP / EXISTING INTEGRATION
  ↓
EXISTING BLUEFLY MODULE
  ↓
CUSTOM PHP ONLY AFTER PROVEN GAP

Permanent rule:

Reuse the owner. Fix the owner. Extend the owner. Do not duplicate the owner.

Before introducing custom code:

UPSTREAM_PROJECT_SEARCHED=YES
ECOSYSTEM_SEARCHED=YES
EXISTING_CONTRIB_OWNER_SEARCHED=YES
EXISTING_BLUEFLY_OWNER_SEARCHED=YES
CAPABILITY_GAP_PROVEN=YES
Otherwise:
CUSTOM_CODE=NO


3. Mandatory Upstream Discovery Protocol

Before changing architecture or adding a dependency:

3.1 Find the project

https://www.drupal.org/project/<machine_name>

Check:

CURRENT_RELEASE=
SECURITY_COVERAGE=
SUPPORTED_DRUPAL_VERSIONS=
MAINTENANCE_STATUS=
RECOMMENDED_BRANCH=

3.2 Check the ecosystem

https://www.drupal.org/project/<machine_name>/ecosystem

Look for: - companion modules; - bridges; - submodules; - replacement projects; - integrations; - maintained successors.

Do not create an integration until the ecosystem has been checked.

3.3 Inspect canonical source

https://git.drupalcode.org/project/<machine_name>

Use upstream source to determine: - services; - plugin types; - extension points; - config entities; - events; - hooks; - APIs; - tests; - actual implementation behavior.

3.4 Check the installed version

Source documentation must match the version actually installed.

Use:

composer show drupal/<project>
and inspect: composer.lock

Do not implement against HEAD, 1.x-dev, old documentation, blog posts, or issue comments when the running site is pinned to another release.


4. Drupal Recipes Are the Default Site-Assembly Mechanism

Drupal Recipes are applied configuration packages.

Drupal's documentation recommends installing recipes with Composer, then applying them using Drush 13+ or Drupal core's recipe command.

Example:

composer require drupal/<recipe>
Then:
drush recipe ../recipes/<recipe>
# or:
php core/scripts/drupal recipe ../recipes/<recipe> -v

A Recipe is not a custom module. The Recipe itself should remain declarative.

Typical package:

recipe_contextual_memory/
├── composer.json
├── recipe.yml
└── config/

The Composer package may declare:

{ "type": "drupal-recipe" }

But business logic does not belong in PHP classes inside a Recipe.

Use:

INSTALL MODULES
IMPORT OTHER RECIPES
CREATE CONFIG
APPLY CONFIG ACTIONS
GRANT PERMISSIONS
UPDATE CONFIGURATION
instead.


5. Recipe Composition Law

Bluefly Recipes should be small and composable.

Correct:

administrator_role
  ↓
contextual_memory_base
  ↓
contextual_memory_ai
  ↓
contextcontrol_site

Incorrect:

ONE GIANT RECIPE WITH EVERY POSSIBLE FEATURE

A higher-level Recipe should compose lower-level capabilities.

Example:

name: Contextual Memory
description: Configures governed AI context and contextual memory.
type: Bluefly
recipes:
  - core/recipes/administrator_role
install:
  - ai
  - ai_context
  - tool
  - tool_belt
config:
  actions:
    user.role.administrator:
      grantPermissions:
        - 'administer ai context'

Rules:

RECIPE_OWNS_SITE_ASSEMBLY=YES
RECIPE_OWNS_RUNTIME_BUSINESS_LOGIC=NO
RECIPE_OWNS_GENERIC_INFRASTRUCTURE=NO


6. Configuration Before Code

Do not write PHP merely to establish site configuration.

Preferred sequence:

EXISTING CONFIGURATION
  ↓
CONFIG ENTITY
  ↓
RECIPE CONFIG
  ↓
CONFIG ACTION
  ↓
ECA / TOOL CONFIGURATION
  ↓
CUSTOM CODE

After configuring a site:

drush config:export -y
Then inspect the resulting configuration and move the authoritative reusable portion to the correct Recipe or configuration owner.

  • Do not blindly hand-author large config trees.
  • Do not blindly export the entire site into a Recipe.
  • Curate only what the Recipe owns.

7. Drush Is an Execution Interface, Not an Architecture

Use Drush for deterministic site operations.

Examples:

drush status
drush pm:list
drush config:status
drush config:export -y
drush cache:rebuild
drush recipe ../recipes/<recipe>

For code generation, Drush provides generators including:

drush generate module
drush generate drush:command

But:

GENERATE MODULE does not mean A MODULE SHOULD EXIST

Generate custom code only after upstream discovery proves the capability gap. Do not invent undocumented Drush commands and treat them as standard Drupal workflow.


8. Tool API Before Custom Service APIs

Drupal Tool API is the standard typed executable-unit abstraction.

Tool API defines reusable executable logic with typed inputs and outputs and is intended for reuse by systems including AI agents and workflow engines.

Use Tool API when a Drupal capability should be callable by:

AI AGENT
ECA
MCP
DRUSH ADAPTER
OTHER AUTOMATION

Prefer:

DOMAIN CAPABILITY → TOOL API PLUGIN
over:
DOMAIN CAPABILITY → CUSTOM CONTROLLER → CUSTOM REST ROUTE → CUSTOM AUTH → CUSTOM CLIENT
unless HTTP exposure is actually required.


9. Tool Belt Before Reimplementing Drupal Operations

Tool Belt is the contrib collection of reusable Tool API plugins for common Drupal operations.

Current upstream provides tools across: - content; - entity definitions; - fields; - users; - system operations; - translation; - workspaces.

Tool Belt exists specifically so Drupal installations do not repeatedly recreate common Drupal operations.

Therefore:

NEED ENTITY CRUD → CHECK TOOL BELT
NEED FIELD/BUNDLE MANAGEMENT → CHECK TOOL BELT
NEED WORKSPACE OPERATIONS → CHECK TOOL BELT
NEED USER OPERATIONS → CHECK TOOL BELT
before custom PHP.

Do not freeze tool counts such as "43 tools" into architecture doctrine. Tool inventories change. Treat upstream capability as dynamic.


10. Context Control Center Is the Drupal Context Authority

drupal/ai_context is upstream Drupal's Context Control Center.

Current upstream provides governed context items with capabilities including revisions, drafts, moderation, scheduling, scopes, and agent-context integration.

Bluefly architecture:

AI CONTEXT / CONTEXT CONTROL CENTER = Drupal-native governed context model
ContextControl.ai = product experience + governance around that upstream model

Do not duplicate: - context entity model - revision system - moderation - scope system - context selection

inside custom Bluefly modules when upstream owns them.

Bluefly extensions should focus on capabilities upstream does not own, such as:

PROVENANCE
TENANCY
CONFLICT CONTROL
EXTERNAL AGENT CORRELATION
POLICY ENFORCEMENT
GOVERNED MEMORY SEMANTICS


11. Memory Architecture

The memory stack is:

HUMAN GOVERNED CONTEXT
  ↓
drupal/ai_context
  ↓
Bluefly governance extensions
  ↓
Drupal AI / AI Search / vector-provider layer
  ↓
vector storage

kb_cache must not recreate a generic vector client already owned by Drupal AI/vector provider infrastructure.

Bluefly-specific ownership is limited to:

PROVENANCE
GAID / IDENTITY SEMANTICS
TENANCY
IDEMPOTENCY
CONFLICT DETECTION
WRITE GUARDS
CEDAR AUTHORIZATION
MEMORY GOVERNANCE
Generic Qdrant connection ownership should remain with the established vector-provider layer.


12. ECA Is the Event / Workflow Layer

Prefer ECA when behavior can be expressed as:

EVENT → CONDITION → ACTION

Use ECA before building: - custom event subscribers - custom orchestration services - custom queue glue - custom callback pipelines

when the workflow is configuration-expressible.

For AI workflows, use: drupal/ai_integration_eca not obsolete predecessor integrations.

Architecture:

Drupal Event → ECA → Tool / AI / API action → governed result


13. External APIs: Use API Orchestrator Before Custom HTTP Clients

For ordinary external REST/HTTP integrations:

CHECK drupal/api_orchestrator FIRST

API Orchestrator provides upstream configurable: - services; - endpoints; - authentication headers; - retries; - queues; - logging; - event dispatch; - ECA integration.

Therefore:

EXTERNAL HTTP API → API ORCHESTRATOR CONFIG → ECA / TOOL API
before:
CUSTOM GUZZLE SERVICE
CUSTOM CURL
CUSTOM RETRY LOOP
CUSTOM REQUEST LOG
Custom transport code requires a proven gap.


14. Webhooks: Use Existing Webhook Owners

Drupal's Webhooks contrib module supports both dispatching and receiving webhook calls and can expose received events to systems such as ECA.

Before creating a Bluefly webhook receiver:

CHECK PLATFORM NATIVE WEBHOOK
CHECK drupal/webhooks
CHECK API ORCHESTRATOR
CHECK EXISTING BLUEFLY INGRESS OWNER
Only create new ingress code after proving none can satisfy the contract. Webhook logic must not become a second orchestration plane.


15. Secrets: Key + External Secret Authority

Drupal Key is the Drupal abstraction for consuming sensitive values. Its own guidance ranks external secret management as the strongest storage approach.

Bluefly model:

1PASSWORD = secret authority
Drupal Key = Drupal-facing secret reference/consumption abstraction

Correct:

Drupal → Key → external provider/reference → secret consumed at runtime

Incorrect:

secret in config sync
secret in recipe.yml
secret in settings committed to Git
secret in Bead
secret in prompt

Permanent rule:

SECRET_VALUE_IN_SOURCE=NO
SECRET_VALUE_IN_CONFIG_EXPORT=NO
SECRET_VALUE_IN_RECIPE=NO
SECRET_VALUE_IN_TRANSCRIPT=NO


16. Canvas Is the Experience Composition Layer

Current stable Drupal Canvas is 1.11.x, not 1.9.x. Drupal.org lists Canvas 1.11.0 as a stable release for Drupal ^11.3.

Do not hardcode historical versions into architecture doctrine unless intentionally pinned by the product.

Architecture:

DRUPAL CONTENT / CONFIG / CONTEXT
  ↓
CANVAS
  ↓
CODE COMPONENTS / SDC / PAGES / TEMPLATES / REGIONS
  ↓
EXPERIENCE

Use Canvas for: - page composition; - Code Components; - global regions; - content templates; - shared frontend assets; - component-driven authoring.

Do not recreate a separate page-builder framework.


17. Canvas CLI Is the Local Code-Component Interface

Canonical package: @drupal-canvas/cli

The local Canvas workflow uses:

npx canvas pull
npx canvas validate
npx canvas push

The CLI can manage: - Code Components; - JavaScript/TypeScript; - component metadata; - shared local code; - global CSS; - packages; - pages; - content templates; - global regions; - Brand Kit assets.

For new Canvas codebases:

npx @drupal-canvas/create@latest

Normal cycle:

npx canvas pull
npx canvas validate
npx canvas push --yes

Current push behavior includes pages, content templates and regions by default unless disabled by configuration or --no-* flags.

Do not maintain obsolete --include-pages or --include-regions assumptions when current CLI behavior has changed.


18. Canvas Agent Context

Canvas includes agent-context commands for retrieving authoritative metadata needed by coding agents.

Example:

npx canvas agents-context cer-expressions
npx canvas agents-context cer-preview <component>

These commands provide authoritative Canvas entity-reference metadata rather than requiring an agent to infer component shapes.

Rule:

CANVAS_METADATA_AVAILABLE → QUERY CANVAS → DO NOT GUESS


19. Canvas Validation Is Mandatory Before Push

Run:

npx canvas validate
before:
npx canvas push

Canvas uses its own ESLint rules and component validation infrastructure. The official @drupal-canvas/eslint-config is automatically used by the CLI for component validation/build/push paths.

Do not create a competing Canvas validator.


20. Canvas Source Control Law

The local Canvas codebase belongs in Git.

Typical governed assets:

src/components/
canvas.config.json
canvas.brand-kit.json
package.json
package-lock.json
pages/
content templates
shared utilities

Secrets belong in environment/runtime secret authority.

Do not commit: - OAuth token files - client secrets - local credentials

Canvas documentation distinguishes repository configuration from .env/credential material.


21. Bluefly Drupal Architecture Overview

┌───────────────────────────────────────────────────────────────────────────┐
│ EXPERIENCE                                                                │
│                                                                           │
│   Drupal Canvas                                                           │
│   SDC / Code Components                                                   │
│   @drupal-canvas/cli                                                      │
│   dashboards / charts where appropriate                                   │
├───────────────────────────────────────────────────────────────────────────┤
│ GOVERNED CONTEXT                                                          │
│                                                                           │
│   drupal/ai_context                                                       │
│   ContextControl.ai                                                       │
│   recipe_contextual_memory                                                │
│   kb_cache governance extensions                                          │
├───────────────────────────────────────────────────────────────────────────┤
│ EXECUTABLE DRUPAL CAPABILITY                                              │
│                                                                           │
│   drupal/tool                                                             │
│   drupal/tool_belt                                                        │
│   Drupal AI / AI Agents                                                   │
│   Drupal MCP where appropriate                                            │
├───────────────────────────────────────────────────────────────────────────┤
│ WORKFLOW & INTEGRATION                                                    │
│                                                                           │
│   drupal/eca                                                              │
│   drupal/ai_integration_eca                                               │
│   drupal/api_orchestrator                                                 │
│   drupal/webhooks                                                         │
│   drupal/key                                                              │
├───────────────────────────────────────────────────────────────────────────┤
│ GOVERNANCE / AUTHORIZATION                                                │
│                                                                           │
│   ContextControl = human review / curation                                │
│   Cedar = mutation authorization                                          │
├───────────────────────────────────────────────────────────────────────────┤
│ FACTORY ORCHESTRATION                                                     │
│                                                                           │
│   Gas City                                                                │
│   Beads                                                                   │
│   authoritative Dolt work graph                                           │
│   Packs / Formulas / Orders / Events                                      │
├───────────────────────────────────────────────────────────────────────────┤
│ DELIVERY                                                                  │
│                                                                           │
│   GitLab                                                                  │
│   gitlab_components                                                       │
│   DDEV                                                                    │
│   governed worktrees                                                      │
│   release/v0.1.x → main                                                   │
└───────────────────────────────────────────────────────────────────────────┘

22. Drupal Does Not Become the Factory Control Plane

Permanent boundary:

DRUPAL = CONTENT = CONTEXT = GOVERNANCE UI = WORKFLOW = TOOL EXECUTION = HUMAN REVIEW
GAS CITY = AGENT ORCHESTRATION
BEADS = DURABLE WORK
CEDAR = AUTHORIZATION
GITLAB = SOURCE / CI / RELEASE
ORACLE = EXECUTION RUNTIME

Drupal must not recreate: - Gas City orchestration - Beads work graph - GitLab pipeline logic - host provisioning - Tailscale management - container lifecycle control - generic secret management - generic model routing


23. Gas City Integration Boundary

Gas City Beads are a pluggable persistence boundary. The default bd provider uses the Dolt-backed Beads implementation, while Gas City can also delegate the Bead store to an exec provider.

The exec-provider contract supports:

create get update close reopen delete list children ready metadata dependencies

That means Drupal should integrate through governed Gas City/Beads interfaces rather than implementing its own competing durable work graph.

Correct:

Drupal event → governance → Gas City Order/API → Bead → agent execution → event/result → Drupal receipt

Incorrect:

Drupal tables → recreate Beads → recreate agent scheduler → recreate task dependencies


24. ContextControl ↔ Gas City Contract

Target:

DRUPAL EVENT
  ↓
ECA / Tool API
  ↓
ContextControl governance
  ↓
Cedar authorization
  ↓
Gas City Order
  ↓
Bead
  ↓
Agent
  ↓
Result / Event
  ↓
ContextControl receipt

Correlation must survive the entire lifecycle:

correlation_id
request_id
bead_id
rig
agent
source_entity
source_revision
result
provenance

ContextControl stores governance and receipt information. It does not become the execution scheduler.


25. Upstream Contribution Before Permanent Fork

If upstream almost provides what Bluefly needs:

OPEN ISSUE → PROPOSE FIX → CONTRIBUTE MR → TEMPORARY THIN ADAPTER IF NECESSARY → REMOVE ADAPTER WHEN UPSTREAM LANDS

Do not immediately create bluefly_<upstream_project> and maintain a permanent parallel implementation.

A fork requires:

UPSTREAM_GAP_PROVEN=YES
CONTRIBUTION_PATH_EXHAUSTED=YES
BUSINESS_NEED=YES
EXIT_PLAN=YES
FOUNDER_EXCEPTION=YES


26. Version Law

Do not hardcode dependency versions in architecture prose unless the version itself is part of the architecture.

Use:

CURRENT_SUPPORTED_STABLE
and verify:
composer show drupal/<project> --all
before changing constraints.

Examples as of September 2026: - Canvas: 1.11.x stable - AI Context / CCC: 1.0 beta line - AI Integration - ECA: 1.0 stable - API Orchestrator: 1.1 stable - Key: 1.22 stable - Tool Belt: 1.0 alpha line

These are observations, not eternal architecture. Treat upstream release status as dynamic truth.


27. Dependency Selection Rule

A module being available does not mean Bluefly should install it.

Classify:

CAPABILITY_REQUIRED=YES|NO
UPSTREAM_MAINTENANCE=
SECURITY_COVERAGE=
STABLE_RELEASE=
OVERLAPPING_OWNER=
ACTIVE_CONSUMER=

Prefer stable/security-covered releases. Alpha/beta dependencies require an explicit reason.


28. DDEV Is the Standard Local Drupal Runtime

Bluefly Drupal development happens in DDEV.

Normal flow:

BEAD → GOVERNED WORKTREE → DDEV → COMPOSER INSTALL → DRUSH → TESTS → CONFIG/RECIPE VALIDATION → PUSH → GITLAB MR

Host-local assumptions do not become project requirements. No developer should need Thomas's machine-specific filesystem state to build the site.


29. Composer Projection Rule

Anything under:

web/modules/contrib/
web/themes/contrib/
vendor/
recipes/contrib/
may be a Composer projection.

Do not edit the projection.

Find:

PACKAGE OWNER
SOURCE REPOSITORY
RECIPE OWNER
COMPOSER CONSTRAINT
and fix the owner.

Permanent law:

Do not edit what Composer installed. Edit what Composer installs from.


30. Bluefly Module Ownership

Custom Bluefly modules exist only for Bluefly-specific capability.

Valid examples: - governance semantics - provenance - Bluefly domain model - Cedar integration - cross-system correlation - product-specific UI - product-specific policy

Invalid reasons: - "we needed an HTTP client" - "we needed CRUD" - "we needed a webhook" - "we needed a field" - "we needed a queue" - "we needed an agent tool" - "we needed vector storage" - "we needed a migration" (core Migrate + migrate_plus own it; see Playbooks/drupal/migration-factory-drupal.md) - "we needed a dashboard" (ai_dashboard, ai_metering, ai_logging own AI administration) - "we needed an agent-readable copy of content" (markdownify + llms_txt)

Those require upstream discovery first.


31. The Definition of Done

Drupal work is not done because: - MODULE EXISTS - CONFIG IMPORTS - DRUSH COMMAND PASSES - PAGE LOADS - MR IS GREEN

Applicable completion chain:

UPSTREAM OWNER VERIFIED
  ↓
IMPLEMENTED AT CORRECT LAYER
  ↓
CONFIGURED
  ↓
TESTED
  ↓
RECIPE / CONFIG REPRODUCIBLE
  ↓
CLEAN INSTALL PROVEN
  ↓
CONSUMER PROVEN
  ↓
MR MERGED TO RELEASE
  ↓
RELEASE READBACK
  ↓
DEPLOYED
  ↓
RUNTIME ACCEPTED
  ↓
WITNESS VERIFIED


32. Permanent Drupal Operating Law

SEARCH BEFORE BUILDING.
CORE BEFORE CONTRIB.
CONTRIB BEFORE CUSTOM.
CONFIG BEFORE PHP.
RECIPE BEFORE INSTALL PROFILE LOGIC.
ECA BEFORE CUSTOM WORKFLOW CODE.
TOOL API BEFORE CUSTOM EXECUTION API.
TOOL BELT BEFORE REIMPLEMENTING DRUPAL OPERATIONS.
API ORCHESTRATOR BEFORE CUSTOM HTTP CLIENTS.
EXISTING WEBHOOK OWNERS BEFORE NEW RECEIVERS.
AI CONTEXT BEFORE CUSTOM CONTEXT MODELS.
DRUPAL AI PROVIDERS BEFORE CUSTOM VECTOR CLIENTS.
CANVAS BEFORE CUSTOM PAGE-BUILDER INFRASTRUCTURE.
KEY REFERENCES SECRETS.
1PASSWORD OWNS SECRET VALUES.
DRUPAL GOVERNS CONTENT AND CONTEXT.
CEDAR AUTHORIZES MUTATION.
GAS CITY ORCHESTRATES AGENTS.
BEADS OWNS DURABLE WORK.
GITLAB OWNS SOURCE.
COMPOSER OWNS DEPENDENCY PROJECTION.
DO NOT EDIT CONTRIB PROJECTIONS.
DO NOT CREATE A MODULE TO AVOID CONFIGURATION.
DO NOT CREATE AN INTEGRATION TO AVOID READING UPSTREAM.
DO NOT FREEZE TOOL COUNTS INTO ARCHITECTURE.
DO NOT FREEZE OLD RELEASE NUMBERS INTO ARCHITECTURE.
DO NOT CREATE ANOTHER OWNER WHEN ONE ALREADY EXISTS.
REUSE THE OWNER.
FIX THE OWNER.
EXTEND THE OWNER.
CONTRIBUTE UPSTREAM.
OWN LESS.
SHIP MORE.