Enrollments API

The Enrollments module adds canonical user-to-post enrollment rows, an effective activity projection, and a structure-aware post-side enrollment projection. It is designed to work with levels, access gates, Bricks Builder, redirects, and Drip without introducing a second derived structure table or per-request ancestry scans on hot paths.

Runtime Ownership

  • Canonical enrollment source rows: wp_brm_enrollments
  • Effective user-post activity projection: wp_brm_enrollment_activity
  • Effective post-side requirement projection: wp_brm_post_data
  • Canonical authored post-side rule state: dedicated post meta keys such as _brm_enrollment_rule_mode

This split is intentional. Canonical writes do not live in wp_brm_post_data, because that table is part of the derived runtime projection layer. The authored tri-state rule for a post now survives normal WordPress editing states because it is stored on the post itself and then projected into wp_brm_post_data.

Key Services

  • EnrollmentService: canonical write service for source rows, state refresh, events, scheduling side effects
  • EnrollmentQueryService: read model for source rows and effective enrollment state
  • EnrollmentRequirementService: authored rule normalization plus local target resolution
  • EnrollmentRequirementProjectionService: computes and writes effective enrollment owner/target projection to wp_brm_post_data
  • EnrollmentActivityService: maintains current-access timing projection
  • EnrollmentScheduleService: exact timestamp due-bucket queue for scheduled activation/expiry
  • EnrollmentAutomationService: level-driven automatic enrollment rules

Authored Rule Model

Each post uses a tri-state authored rule:

  • required
  • inherit
  • disabled

When the rule is required, the post also defines a target mode:

  • current_post
  • parent_level
  • top_level
  • tracking_level
  • specific_post

The authored API lives in EnrollmentRequirementService and should be used instead of direct writes to PostDataService.

$rule = EnrollmentRequirementService::get_instance()->get_authored_post_enrollment_rule( $post_id );

EnrollmentRequirementService::get_instance()->save_authored_post_enrollment_rule(
	$post_id,
	array(
		'enrollment_rule_mode'      => 'required',
		'enrollment_target_mode'    => 'current_post',
		'enrollment_target_post_id' => 0,
	)
);

Effective Inheritance Semantics

inherit does not mean “copy the parent’s literal target mode and reinterpret it relative to the child.” That would break current_post semantics. Instead, the child reuses the ancestor’s already-resolved effective anchor.

Examples:

  • Course = required + current_post, Lesson = inherit → lesson checks course enrollment
  • Module = required + current_post, Lesson = inherit → lesson checks module enrollment
  • Ancestor = required + specific_post → descendant checks the same specific anchor
  • Ancestor = disabled → inherited descendants are not enrollment-gated unless they define their own required rule

This effective rule is calculated by EnrollmentRequirementProjectionService::calculate_effective_post_enrollment_projection() and stored as:

  • enrollment_required
  • effective_enrollment_owner_post_id
  • effective_enrollment_target_post_id

Why the Projection Matters

Hot paths such as access checks, Bricks conditions, dynamic tags, and query loops should not walk ancestor chains every time. They should read the effective projection from wp_brm_post_data.

That is why runtime consumers read:

  • ContentAccessGateService
  • BricksConditionsRegistrar
  • BricksQueryFilters
  • EnrollmentDynamicTags
  • DripService when using post_enrollment_activated

Only rebuild/cascade flows should do ancestry work, and even there the code should stay inside the services that own it.

Canonical Enrollment Writes

Source rows live in wp_brm_enrollments. One row represents one grant source for one user and one anchor post.

Important fields:

  • user_id
  • post_id
  • status
  • source
  • source_ref
  • enrolled_at
  • activated_at
  • expires_at

Use EnrollmentService for all writes. Do not mutate the tables directly from UI controllers or Bricks integration code.

$result = EnrollmentService::get_instance()->upsert_post_enrollment_source(
	$user_id,
	$post_id,
	array(
		'source'       => 'manual',
		'source_ref'   => 'support-ticket-193',
		'activated_at' => '2026-03-26 09:00:00',
		'expires_at'   => '2026-06-26 09:00:00',
	)
);

The service recalculates effective state, updates activity projection, emits events, schedules due work, and invalidates caches.

Effective Enrollment State

EnrollmentQueryService collapses multiple source rows into one effective state for a given user_id + post_id.

Main rules:

  • active rows win over scheduled rows
  • scheduled rows win over paused or terminal rows
  • multiple active rows collapse to one effective state with source = multiple
  • the activity projection provides stable current_access_started_at for automations and drip timing

Public Runtime API

brm_user_has_active_post_enrollment( $user_id, $post_id );
brm_get_user_post_enrollment_state( $user_id, $post_id );
brm_get_user_enrolled_post_ids( $user_id, array( 'status' => 'active' ) );
brm_get_post_enrolled_user_ids( $post_id, array( 'status' => 'scheduled' ) );

brm_enroll_user_in_post( $user_id, $post_id, $args );
brm_update_post_enrollment_source( $user_id, $post_id, $args );
brm_cancel_post_enrollment_source( $user_id, $post_id, 'manual', '' );

These wrappers already guard against the module being disabled.

Bricks Integration

The module exposes enrollment-aware Bricks features in four places:

  • Query controls: post loops and user loops can filter by enrollment state
  • Conditions: active, scheduled, and explicit status checks
  • Dynamic tags: enrollment state, dates, source, count, URLs
  • Form actions: advanced enrollment writes

The simple frontend self-service path is not a Bricks Form requirement anymore. A normal Bricks Button can use:

  • {brm_enrollment:enroll_url}
  • {brm_enrollment:unenroll_url}

Those URLs resolve against the effective enrollment target for the current post context.

Frontend Action Security Model

The self-service action controller still starts from a URL because that is what makes the Bricks button UX simple, but the write is not completed as a raw GET-side mutation anymore.

Current flow:

  1. the button opens a signed action URL
  2. the controller validates the nonce and login state
  3. the request is bridged into a tiny POST submit page
  4. the actual enrollment mutation happens on POST
  5. the user is redirected back to the original page

This keeps the UX simple while avoiding direct GET-side writes.

Drip Integration

Drip keeps ownership of release timing. Enrollments keep ownership of access.

The key integration is the post_enrollment_activated trigger. When used, DripService looks up the current effective enrollment target and reads current_access_started_at from wp_brm_enrollment_activity.

This is what makes course-level anchor enrollment work for inherited lessons: drip timing can follow the anchor enrollment date instead of the child post itself.

Admin Surfaces

  • BricksMembers → Enrollments: manual enrollment, current enrollments, level enrollment rules
  • BRM post meta box: tri-state authoring plus effective preview
  • Structure editor: tri-state authoring plus descendant inherit cascade controls
  • BricksMembers → Access Control: enrollment redirect section

Keep these surfaces aligned with existing BRM patterns. Use the existing card structure, search pickers, filter rows, and tab behavior instead of inventing feature-specific admin markup conventions.

Extension Rules

  • Use EnrollmentRequirementService for authored rule reads/writes
  • Use EnrollmentRequirementProjectionService for effective projection refresh
  • Use EnrollmentService for source-row writes
  • Do not store canonical authored state in JSON blobs or treat wp_brm_post_data as canonical write storage
  • Do not broad-scan structure meta on normal runtime requests
  • Prefer the effective projection for hot-path reads
Early Bird Deal

Start Building Your Membership Site Today

Create, sell, and manage your content without limits. BricksMembers gives you everything you need to build membership and LMS sites with Bricks Builder.

Lifetime updates & bug fixes • Premium support • 0% transaction fees • 60-day money-back guarantee