Context Map
How Objectuve's bounded contexts relate to each other. This document shows the integration patterns, data flow direction, and coupling points between contexts.
High-Level Context Map
graph TB
subgraph External
Clerk["Clerk (Auth Provider)"]
Stripe["Stripe (Payments)"]
AR["Agent Runner<br/>(Cloud Run + LiteLLM)"]
Strava["Strava / Chess.com<br/>(Activity Providers)"]
end
subgraph Objectuve
GT["Goal Tracking<br/>(Core Domain)"]
SC["Social & Community<br/>(Accountability)"]
UI["User Identity<br/>(Shared Kernel)"]
GP["Gamification<br/>(Supporting)"]
AI["AI Coaching<br/>(Supporting)"]
GA["Guide Assistant<br/>(sub-context of AI Coaching)"]
AW["AI Workforce<br/>(Platform Infra)"]
BL["Billing<br/>(Supporting)"]
TM["Teams<br/>(sub-context of Billing)"]
FB["Feedback<br/>(Supporting)"]
IN["Integrations & Connected Apps<br/>(Supporting)"]
PA["Personal Analytics<br/>(Supporting)"]
IE["Import/Export<br/>(Supporting)"]
CM["Content Moderation<br/>(Cross-Cutting)"]
AD["Admin<br/>(Cross-Cutting)"]
GD["GDPR & Data Rights<br/>(Cross-Cutting)"]
end
Clerk -- "ACL: ClerkJwtVerifier<br/>ClerkUserSync" --> UI
Stripe -- "Webhook (unmediated today;<br/>proposed Billing::StripeAdapter)" --> BL
AR -- "HMAC Webhook<br/>ProcessWebhookResult" --> AW
AW -- "HTTP (OIDC)<br/>Ai::AgentRunnerClient" --> AR
Strava -- "ACL: IntegrationProvider::Adapter<br/>(OAuth webhook / poll)" --> IN
IN -- "AutoCheckIn -> GoalEvent<br/>(write)" --> GT
PA -. "Read-only: Goal, GoalEvent,<br/>HabitCompletion" .-> GT
PA -. "Read-only: badge-XP proxy" .-> GP
PA -. "Read-only: streak, mood logs" .-> UI
IE -. "Read-only: goal names<br/>(collision detection)" .-> GT
UI -. "Shared Kernel:<br/>every context reads User" .-> GT
UI -. "Shared Kernel" .-> SC
UI -. "Shared Kernel" .-> GP
UI -. "Shared Kernel" .-> AI
UI -. "Shared Kernel" .-> AW
UI -. "Shared Kernel" .-> BL
UI -. "Shared Kernel" .-> FB
GT -- "Events trigger<br/>badge jobs" --> GP
GT -- "Events trigger<br/>feed generation" --> SC
SC -- "Community join<br/>triggers badge" --> GP
UI -- "Sign-in triggers<br/>streak XP" --> GP
GT -. "Goal data<br/>(read-only)" .-> AI
AI -. "Generates milestones,<br/>insights" .-> GT
UI -. "EnneagramAssessment<br/>(conformist read)" .-> AI
AI -- "Owns Guide Assistant<br/>(stateless, no shared state)" --> GA
SC -- "Reads goals for<br/>feed & sharing" --> GT
BL -- "Owns billing, seats,<br/>sub-communities" --> TM
GT -- "Job enqueue: leaderboard +<br/>collective goal upsert" --> TM
CM -. "Screens content from" .-> SC
CM -. "Screens content from" .-> GT
GD -. "Deletion/anonymization<br/>cascades" .-> GT
GD -. "Deletion/anonymization<br/>cascades" .-> SC
GD -. "Deletion/anonymization<br/>cascades" .-> GP
GD -. "Deletion/anonymization<br/>cascades" .-> FB
GD -. "Reads for export<br/>preview" .-> GT
GD -. "Reads for export<br/>preview" .-> SC
GD -. "Reads for export<br/>preview" .-> BL
GD -. "Reads for export<br/>preview" .-> UI
AD -. "Audits actions across" .-> GT
AD -. "Audits actions across" .-> SC
AD -. "Audits actions across" .-> FB
AD -. "Audits actions across" .-> AW
AD -. "Reviews artifacts" .-> AW
AD -. "Updates post status" .-> FB
Integration Patterns
Shared Kernel: User Identity
Every context needs the User reference. User Identity is the shared kernel — a small set of models and interfaces that multiple contexts depend on.
| What's Shared | How It's Used |
|---|---|
User model (id, public_id, name) | Foreign key on Goal, CommunityMember, UserAlly, MoodLog, etc. |
context[:current_user] | GraphQL resolvers pass the authenticated user to every interaction |
User.public_find(id) | Standard lookup pattern across all contexts |
Rule: Other contexts may read User fields (name, public_id, email) but must not write to User directly. Profile mutations go through User Identity interactions (UpdateUser, UpdateUserPhoto).
Exception: User writes originating outside User Identity split into mediated (routed through a domain event User Identity subscribes to) and unmediated (a direct write from the other context's own code). scripts/check-shared-kernel-writes.mjs enforces the unmediated side — it greps rails_api/app/ and rails_api/lib/ for direct User write call sites and fails the build on any that aren't in its allowlist. This table is the allowlist's doc-side mirror; keep the two in sync when either changes.
| Writer | Context | Mediated? | Status |
|---|---|---|---|
GamificationService.award_xp → User.xp, User.level | Gamification | ❌ direct (user.save! inside user.with_lock, not update-family — outside the guard's regex, but still an unmediated write in practice) | Sanctioned exception |
Gamification::AwardStimXp → User.stim_xp_total, stim_streak_current, stim_streak_longest, stim_last_played_on | Gamification | ✅ event-mediated via critical_path.stim_completed (see 04 — Domain Events) | Sanctioned exception — see Resolved: Critical Path → Gamification (event-driven) below |
Billing::Subscribers → User.supporter_tier, supporter_until | Billing | ✅ event-mediated via billing.payment_processed (see 04 — Domain Events, 04-domain-events.md:44-58) | Sanctioned exception |
Billing::ExpireSupportersJob → User.supporter_tier, supporter_until | Billing | ❌ direct, scheduled job (expire_supporters_job.rb:12) | Sanctioned exception |
AdminRole#sync_legacy_admin_flag → User.admin | Admin | ❌ direct, after_save callback (admin_role.rb:17) | Sanctioned exception |
Gdpr::UserDataCascade#scrub_user_row! → PII columns | GDPR | ❌ direct (user.update_columns, user_data_cascade.rb:280) | Sanctioned exception |
Resolved: StripeService.find_or_create_customer used to write user.stripe_customer_id directly (stripe_service.rb:17, pre-OBJ-2366). It now calls UserIdentity::AssignStripeCustomerId (rails_api/app/interactions/user_identity/assign_stripe_customer_id.rb) — a User Identity interaction, so the write is mediated by construction and carries no allowlist entry.
Anti-Corruption Layer: Clerk
Clerk is an external authentication provider. We isolate it behind two services:
sequenceDiagram
participant Client as Frontend
participant GQL as GraphQL
participant ACL as ClerkJwtVerifier
participant Clerk as Clerk JWKS
participant Sync as ClerkUserSync
participant DB as User table
Client->>GQL: Request + SessionToken header
GQL->>ACL: Verify JWT
ACL->>Clerk: Fetch JWKS (cached)
Clerk-->>ACL: Public keys
ACL-->>GQL: Decoded claims (clerk_user_id)
GQL->>DB: User.find_by(clerk_user_id:)
GQL->>GQL: Set context[:current_user]
Note over Client,DB: On first sign-in only:
Client->>GQL: syncUser mutation
GQL->>Sync: ClerkUserSync.call
Sync->>DB: Find or create User
ClerkJwtVerifierdecodes RS256 JWTs via Clerk's JWKS endpoint. It never exposes Clerk internals to the rest of the system.ClerkUserSyncmaps Clerk profile data to a localUserrecord. Runs once per session on first sign-in.
Upstream/Downstream: Goal Tracking → Gamification
Goal Tracking is upstream — it produces domain events that Gamification subscribes to. The coupling is now event-driven (loosely coupled):
sequenceDiagram
participant CI as CheckInHabit
participant DE as DomainEvents
participant GS_sub as Gamification::Subscribers
participant GS as GamificationService
Note over CI: Goal Tracking context
CI->>CI: Create HabitCompletion
CI->>CI: Update goal streak
CI->>DE: publish('goal_tracking.habit_checked_in', {...})
Note over DE: Event Facade
DE->>GS_sub: Emit event (async-safe)
Note over GS_sub: Gamification context
GS_sub->>GS: award_xp(user, 25, 'Habit Check-in')
GS_sub->>GS: AwardPartnerBonus.call(user:, completed_date:)
GS->>GS: user.xp += 25
GS->>GS: check_for_level_up
Current state (v1.12 Phase 53): CheckInHabit publishes the goal_tracking.habit_checked_in domain event after persisting the habit completion. Gamification::Subscribers subscribes to this event and handles XP award and partner bonus independently. Goal Tracking no longer knows about Gamification's internals.
Legacy state: AddGoal still directly triggers badge detection jobs. This remains a candidate for future migration to goal.created event (see Phase 54).
Upstream/Downstream: Goal Tracking → Social & Community
When users log progress, the Social & Community context needs to propagate that to followers:
sequenceDiagram
participant AGE as AddGoalEvent / CheckInHabit
participant Job as GenerateFollowFeedItemsJob
Note over AGE: Goal Tracking context
AGE->>AGE: Create GoalEvent
AGE->>Job: Enqueue (async)
Note over Job: Social & Community context
Job->>Job: Find goal followers
Job->>Job: Create UserFeedItem per follower
Current state: This is already loosely coupled via an async background job. The interaction doesn't know how feed items are created — it just fires the job. This is a good pattern.
Upstream/Downstream: Goal Tracking → Teams
Goal Tracking is upstream of Teams (a sub-context of Billing — see 01 — Bounded Contexts). Two Goal Tracking interactions enqueue the same pair of Teams jobs so a team's leaderboard and collective-goal progress stay current whenever a member logs progress:
sequenceDiagram
participant CI as CheckInHabit /<br/>AddGoalEvent
participant LB as Teams::TeamLeaderboardUpsertJob
participant CG as Teams::CollectiveGoalContributionUpsertJob
Note over CI: Goal Tracking context
CI->>CI: Persist HabitCompletion / GoalEvent
CI->>LB: Enqueue (async)
CI->>CG: Enqueue (async)
Note over LB,CG: Teams context (sub-context of Billing)
LB->>LB: Upsert TeamLeaderboardEntry for (team, community, user, period)
CG->>CG: Upsert CollectiveGoalContribution.events_contributed
Current state: Both GoalTracking::CheckInHabit and GoalTracking::AddGoalEvent enqueue Teams::TeamLeaderboardUpsertJob and Teams::CollectiveGoalContributionUpsertJob (4 enqueue call sites, 2 job classes) after persisting their own record. This is the same loosely coupled async job shape as Goal Tracking → Social & Community above — the interaction doesn't know how leaderboard/contribution rows are computed, it just fires the jobs. Previously undocumented; see item 7 in 05 — Application Services for the coupling-heatmap entry.
Upstream/Downstream: Social & Community → Gamification
JoinCommunity → JoinFirstCommunityActionJob → UserAction.join_first_communitySame pattern as Goal Tracking → Gamification: direct job trigger from the interaction. Should become a domain event.
Resolved: Critical Path → Gamification (event-driven)
Critical Path used to be tightly coupled to User Identity — CriticalPath::AwardStimXp wrote directly to four progression fields on User without routing through GamificationService:
CriticalPath::RecordPlay
→ CriticalPath::AwardStimXp
→ user.increment!(:stim_xp_total, delta)
→ user.update!(stim_streak_current:, stim_streak_longest:, stim_last_played_on:)Decided 2026-08-09 (Option C, docs/architecture/proposals/critical-path-user-coupling.md): Option B (event-driven, no direct User write) is the target state; Option A's direct-write shape was recorded as a transitional exception until the migration shipped. Migrated per OBJ-2370 — CriticalPath::RecordPlay now publishes critical_path.stim_completed (see 04 — Domain Events); Gamification::Subscribers consumes it and calls the moved Gamification::AwardStimXp interaction, which performs the same row-locked write, now from inside the context that's already the sanctioned User-writing exception:
sequenceDiagram
participant RP as CriticalPath::RecordPlay
participant DE as DomainEvents
participant GS_sub as Gamification::Subscribers
participant AS as Gamification::AwardStimXp
Note over RP: Critical Path context
RP->>RP: Find or create CriticalPathPlay
RP->>DE: publish('critical_path.stim_completed', {...})
Note over DE: Event Facade (synchronous, in-process)
DE->>GS_sub: Emit event
Note over GS_sub: Gamification context
GS_sub->>AS: AwardStimXp.call(user:, critical_path_play:)
AS->>AS: user.lock! + increment!/update! stim fields
The subscriber runs synchronously, not via perform_later — PlayView.vue does a read-after-write refetch of stim-XP status immediately after the mutation resolves, which only works if the award has already landed by the time RecordPlay returns. scripts/check-shared-kernel-writes.mjs's allowlist entry now points at rails_api/app/interactions/gamification/award_stim_xp.rb as a permanent entry, alongside gamification_service.rb's exception; the old transitional critical_path/award_stim_xp.rb entry is gone because that file no longer exists.
Phase 2 coupling-point: Entry 6 in the Modularization Guide migration order — now marked migrated.
Secondary coupling: CriticalPath::SetActiveTheme and CriticalPath::SetReminderPreferences write to UserDetail — this follows the same loose belongs_to :user pattern used by Billing and Feedback and is low-risk.
Cross-Cutting: GDPR & Data Rights
GDPR & Data Rights is a cross-cutting concern that reads from all bounded contexts to fulfill data-subject rights (export, deletion, rectification). See 01 — Bounded Contexts for the full reconciled section — there is no GdprExport model; export state lives on GdprRequest itself (export_file_url, export_expires_at).
sequenceDiagram
participant User
participant Submit as Gdpr::SubmitRequest
participant Ack as Gdpr::SendAcknowledgmentJob
participant Export as Gdpr::ExecuteExport /<br/>RequestSelfServiceExport
participant Build as Gdpr::BuildExportBundleJob
participant Builder as Gdpr::ExportBuilder
participant Contexts as All Bounded Contexts
participant Mail as GdprMailer
User->>Submit: Submit export/deletion/rectification request
Submit->>Submit: Create GdprRequest(status: 'received')
Submit->>Ack: enqueue
Ack->>Mail: acknowledgment(request) — starts 72h SLA clock
Export->>Export: GdprRequest status → 'processing'
Export->>Build: enqueue(request_id, actor_id)
Build->>Builder: build export bundle
Builder->>Contexts: Read user data (all contexts)
Contexts-->>Builder: Goals, events, posts, badges, etc.
Build->>Build: Upload ZIP to private GCS bucket
Build->>Build: GdprRequest status → 'fulfilled', export_file_url set
Build->>Mail: export_ready(request, download_url, expires_at)
- Cascade Preview:
Gdpr::CascadePreviewis read-only and shows the admin exactly what a deletion will touch (per-model counts). - Execution: the actual erasure cascade lives in
Gdpr::UserDataCascade(app/services/gdpr/user_data_cascade.rb), shared by two callers —Gdpr::ExecuteDeletion(admin-triggered, step-up-gated) andGdpr::DeleteSelfServiceAccount(self-service, OBJ-2183) — each wrapping the call in its own transaction and handling its own admin-flow bookkeeping (step-up token,GdprRequestfinalization,AdminActionaudit row). The cascade is a mixed hard/soft-delete, not a uniform soft-delete — goals, mood logs, badges, devices, community memberships, and AI memories are hard-deleted (really_destroy!/delete_all); community posts/comments and theUserrow itself are soft-deleted after PII scrubbing. See 01 — Bounded Contexts for the full breakdown.Gdpr::ExecuteExport(admin) andGdpr::RequestSelfServiceExport(self-service) both delegate the actual build/upload toGdpr::BuildExportBundleJob. - Reads: GDPR reads from Goal Tracking, Social, Gamification, Billing, Feedback, User Identity, and AI. Write dependencies are limited to its own
GdprRequestaggregate (no separate export aggregate exists).
Conformist: AI Coaching → Goal Tracking
AI Coaching reads Goal Tracking data as-is. It has no influence over the Goal model structure — it conforms to whatever Goal Tracking provides. This is appropriate for a supporting domain.
Anti-Corruption Layer + Upstream/Downstream: Integrations & Connected Apps → Goal Tracking
Integrations & Connected Apps (see 01 — Bounded Contexts) is both an ACL and a write-upstream of Goal Tracking:
sequenceDiagram
participant Provider as Strava / Chess.com
participant Adapter as IntegrationProvider::Adapter
participant Map as MapIncomingActivity
participant ACI as AutoCheckIn
participant CI as GoalTracking::CheckInHabit
Note over Provider,Adapter: External provider format
Provider->>Adapter: Webhook / poll response
Adapter->>Adapter: Normalize to IntegrationActivity (activity_type, timestamp)
Note over Map: Integrations context
Adapter->>Map: score against candidate goals
Map-->>ACI: decision: auto (score >= 0.85)
Note over ACI: Integrations context
ACI->>CI: CheckInHabit.call(goal_id:, completed_date:)
Note over CI: Goal Tracking context
CI->>CI: Create HabitCompletion, update streak
ACI->>ACI: Create attribution GoalEvent (source: :integration)
Current state: IntegrationProvider::Adapter (and its Strava/Chess implementations) is the anti-corruption layer isolating each provider's own vocabulary from Objectuve's activity_type taxonomy — the same shape as the Clerk ACL for auth. Once inside the boundary, Integrations::AutoCheckIn is the sole write path into Goal Tracking: it calls GoalTracking::CheckInHabit directly and creates its own attribution GoalEvent, rather than publishing a domain event Goal Tracking subscribes to. This is the inverse direction of every other Goal-Tracking-is-upstream pattern in this document — here, Integrations is upstream and Goal Tracking is the target of a direct interaction call.
Conformist (Multi-Context): Personal Analytics → Goal Tracking, Gamification, User Identity
Personal Analytics (see 01 — Bounded Contexts) owns no aggregates of its own — every series it returns is a read-only projection over three other contexts:
| Reads From | What | Interaction |
|---|---|---|
| Goal Tracking | Goal, GoalEvent, HabitCompletion | CalculateCompletionByCategory, CalculateActivityHeatmap, CalculateMonthInReview |
| Gamification | Badge-unlock XP proxy (PersonalAnalytics::BadgeXpProxy, no per-event XP ledger exists) | CalculateXpOverTime, CalculateMonthInReview |
| User Identity | User#streak_activity_dates, #streak_length_on, #current_streak, MoodLog | CalculateStreakHistory, CalculateActivityHeatmap, CalculateMonthInReview |
This is the same conformist relationship AI Coaching has with Goal Tracking, fanned out across three upstream contexts instead of one — Personal Analytics has no influence over any of their models and adapts entirely to what they provide. Nothing in Goal Tracking, Gamification, or User Identity is aware Personal Analytics reads it; there is no coupling in the reverse direction.
Conformist (Read-Only): Import/Export → Goal Tracking
Import/Export (see 01 — Bounded Contexts) reads Goal Tracking once per parse, for collision detection only — it never writes:
| Reads From | What | Interaction |
|---|---|---|
| Goal Tracking | current_user.goals names (memoized Set, one query per parse) | ImportExport::ParseCsv |
As of Phase 2 (v4.64), this context stages a validated preview and stops — a staged DataImport is never itself written into goals/habit_completions; that write path is Phase 3+ and does not exist yet. ImportExport::CsvSchema (Phase 1) is also the read-side contract Gdpr::ExportBuilder writes against on export, making the CSV column shape a published language shared by GDPR & Data Rights and this context, not something either side owns independently.
Published Language: GraphQL Schema
The GraphQL schema is the published language between the frontend and backend. It defines the API contract that both sides agree on.
- Queries and mutations are organized by domain concern (goals, communities, users, gamification, AI)
- Types mirror aggregate structure (GoalType has events, milestones, etc.)
- The frontend's
graphql.jsis a 1:1 mapping of this contract
Coupling Summary
| From | To | Mechanism | Coupling Level | Action |
|---|---|---|---|---|
| Goal Tracking | Gamification | Event-driven via goal_tracking.habit_checked_in | Loose | ✅ Implemented in v1.12 Phase 53 (commit 6b61cffc) |
| Goal Tracking | Gamification | Inline badge job triggers | Tight | Replace with domain event |
| Goal Tracking | Social | Async GenerateFollowFeedItemsJob | Loose | Keep as-is |
| Social | Gamification | Inline JoinFirstCommunityActionJob | Tight | Replace with domain event |
| Social | Goal Tracking | ToggleGoalEventReaction reads goal event | Loose (read-only) | Keep as-is |
| Social | Goal Tracking | ToggleGoalEventEncouragement reads goal event | Loose (read-only) | Keep as-is |
| Social | Goal Tracking | BuildUnifiedFeed reads goals/events | Loose (read-only) | Keep as-is |
| Social | Goal Tracking | AddGoalToCommunity reads goal | Loose (read-only) | Keep as-is |
| User Identity | Gamification | Direct GamificationService.award_xp in update_streak! | Tight | Replace with domain event |
| Content Moderation | Social | AddPostComment calls ContentModerationService.screen | Loose (inline service call) | Keep as-is |
| Content Moderation | Goal Tracking | Screens GoalEventComment content | Loose (inline service call) | Keep as-is |
| AI Coaching | Goal Tracking | Reads goal data | Loose (conformist) | Keep as-is |
| User Identity (Weekly Digest) | Gamification | WeeklyDigest::AssembleDigestData reads StimXpLedger for weekly XP | Loose (conformist read-only) | Keep as-is — digest assembly conforms to Gamification's ledger shape |
| User Identity (Weekly Digest) | Goal Tracking | WeeklyDigest::AssembleDigestData reads user.goals for CTA URL | Loose (conformist read-only) | Keep as-is |
| User Identity (Weekly Digest) | Social | WeeklyDigest::AssembleDigestData reads ally list + GoalEvent activity for recent-allies section | Loose (conformist read-only) | Keep as-is |
| User Identity (Weekly Digest) | AI Coaching | WeeklyDigest::AssembleDigestData calls Ai::ServiceClient (batch/insights model) for personalized insight | Loose (service call, read-only) | Keep as-is — same AI seam used by all coach surfaces |
| All | User Identity | context[:current_user] | Shared Kernel | Accept as infrastructure |
| User Identity | Clerk | ClerkJwtVerifier, ClerkUserSync | ACL | Keep isolated |
| AI Workforce | User Identity | Reads User for operator-triggered runs, role lookups | Loose (read-only) | Keep as-is |
| AI Workforce | Goal Tracking | Potential future read access (employees summarizing user progress) | None today (loose if added) | Keep read-only |
| AI Workforce | Agent Runner (external) | Ai::AgentRunnerClient HTTP + HMAC webhook | Webhook-driven | Wrap with AiWorkforce::AgentRunnerAdapter ACL |
| AI Workforce | AI Coaching (shared infra) | Shared AiUsageEvent write path | Loose (append-only) | Keep as-is |
| Billing | User Identity | Billing::Subscribers writes User.supporter_tier/supporter_until (event-mediated via billing.payment_processed); Billing::ExpireSupportersJob writes the same fields directly (scheduled job) | Sanctioned shared-kernel exception (both writes land on User, not on Billing's own PaymentRecord/Plan) | Keep as-is — see Shared Kernel exception table |
| Billing | Stripe (external) | StripeService + Webhooks::StripeController | Webhook-driven | Stripe ACL implemented in v1.5 (Billing::ProcessStripeWebhook); Billing::StripeAdapter extraction still proposed |
| Billing | RevenueCat (external) | Webhooks::RevenueCatController → Billing::ProcessRevenueCatWebhook | Webhook-driven | Mirrors Stripe pattern; same billing.payment_processed event + same Billing::Subscribers handler. Verified: no new cross-context reads (reads only User Shared Kernel + Billing's own Plan/PaymentRecord). |
| AI Coaching | User Identity (Enneagram) | Reads EnneagramAssessment for persona personalization | Loose (conformist read-only) | Keep as-is — AI Coaching conforms to User Identity's assessment data shape |
| Feedback | User Identity | FeedbackPost/Comment/Vote belongs_to :user | Loose (read-only on User) | Keep as-is |
| Critical Path | User Identity | Gamification::AwardStimXp writes stim_xp_total, stim_streak_current, stim_streak_longest, stim_last_played_on, triggered by critical_path.stim_completed (published from CriticalPath::RecordPlay) | Loose (event-driven) | Decided 2026-08-09 (Option C, docs/architecture/proposals/critical-path-user-coupling.md); migrated per OBJ-2370 — see Shared Kernel exception table |
| Critical Path | User Identity | SetActiveTheme writes UserDetail.critical_path_active_theme; SetReminderPreferences writes UserDetail + conditionally User.timezone | Loose | Keep as-is |
| Goal Tracking | Teams (sub-context of Billing) | CheckInHabit and AddGoalEvent each enqueue Teams::TeamLeaderboardUpsertJob + Teams::CollectiveGoalContributionUpsertJob | Loose (async job enqueue) | Keep as-is — newly documented (previously untracked); see 05 — Application Services |
| Admin | All | Audit via AdminAction polymorphic target; RBAC via AdminRole | Cross-cutting | Accept as infrastructure |
| Admin | AI Workforce | Approve/reject artifacts, update employees | Direct interaction calls | Keep as-is |
| Admin | Feedback | Update post status, mark official comments | Direct interaction calls | Keep as-is |
| Integrations & Connected Apps | Goal Tracking | Integrations::AutoCheckIn calls GoalTracking::CheckInHabit directly, writes attribution GoalEvent | Tight (direct interaction call, not event-driven) | Candidate for a goal_tracking.check_in_requested style event if a second upstream writer emerges; low priority today — single caller |
| Integrations & Connected Apps | Strava / Chess.com (external) | IntegrationProvider::Adapter (OAuth webhook for Strava, polled REST for Chess.com) | ACL | Keep isolated — same pattern as the Clerk ACL |
| Personal Analytics | Goal Tracking | Reads Goal, GoalEvent, HabitCompletion for heatmap/category/month-in-review series | Loose (conformist read-only) | Keep as-is |
| Personal Analytics | Gamification | Reads badge-unlock XP proxy (PersonalAnalytics::BadgeXpProxy) | Loose (conformist read-only) | Keep as-is |
| Personal Analytics | User Identity | Reads streak_activity_dates, streak_length_on, current_streak, MoodLog | Loose (conformist read-only) | Keep as-is |
| Import/Export | Goal Tracking | ImportExport::ParseCsv reads current_user.goals names for collision detection | Loose (conformist read-only) | Keep as-is |
Dependency Direction
┌──────────────┐
│ Clerk (ext) │
└──────┬───────┘
│ ACL
┌──────▼───────┐
┌─────│User Identity │─────┐
│ │(Shared Kernel)│ │
│ └──────────────┘ │
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌───────────┐ ┌──────────┐
│Goal Tracking│ │ Social & │ │ AI │
│ (Core) │ │ Community │ │ Coaching │
└──┬───┬──────┘ └──┬──┬─────┘ └──────────┘
│ │ │ │
│ │ ┌──────────┘ │
│ ▼ ▼ │
│ ┌─────────────────┐│
│ │ Gamification ││
│ │ (Downstream) ││
│ └─────────────────┘│
│ │
└──┐ ┌────┘
▼ ▼
┌─────────────────────┐
│ Content Moderation │
│ (Cross-Cutting) │
└─────────────────────┘Dependencies flow downward. Gamification is the most downstream context — it reacts to events from Goal Tracking, Social & Community, and User Identity. No other context depends on Gamification's internals.
AI Workforce, Billing, Feedback, and Integrations & Connected Apps sit as sibling supporting contexts alongside Goal Tracking / Social, each with their own external seams:
- AI Workforce ↔ Agent Runner: bidirectional HTTP + HMAC webhook (already mediated by
Ai::AgentRunnerClient+Webhooks::AiWorkforceController; proposedAgentRunnerAdapterwould tighten the ACL further). - Billing ↔ Stripe: unidirectional inbound webhook today handled in the controller (unmediated). Candidate for the
Billing::StripeAdapterACL that mirrors the Clerk pattern — this is the single biggest boundary gap in the current context map. - Feedback: no external coupling. The lightest-weight of the supporting contexts.
- Integrations & Connected Apps ↔ Strava / Chess.com: bidirectional — inbound OAuth webhook for Strava (real-time), outbound polled REST for Chess.com — both already mediated by
IntegrationProvider::Adapter, the same ACL shape Clerk uses. Unlike the other sibling contexts, it also writes into a domain sibling (Goal Tracking) rather than only outward to an external provider — see Anti-Corruption Layer + Upstream/Downstream: Integrations & Connected Apps → Goal Tracking above.
Teams (sub-context of Billing) has no external seam of its own — billing/checkout flows through Billing's Stripe seam via Teams::StartTeamCheckout / Teams::ProvisionTeamSubscription. Its one cross-context dependency is inbound, from a sibling internal context rather than an external one: Goal Tracking → Teams via the two job enqueues documented in Upstream/Downstream: Goal Tracking → Teams above.
Guide Assistant (sub-context of AI Coaching) has no cross-context edges at all, in either direction — it reads only the committed guide corpus (rails_api/config/ai/guide_corpus.json), never live domain data from Goal Tracking, User Identity, or any other context. See 01 — Bounded Contexts.
Personal Analytics has no external seam and no sibling-context dependency of its own to document here — it is purely a downstream reader of Goal Tracking, Gamification, and User Identity (see Conformist (Multi-Context): Personal Analytics → Goal Tracking, Gamification, User Identity above), the same shape as AI Coaching's relationship to Goal Tracking.
Architectural Decision Records
ADR: Models as Publishers (Phase 54 Decision)
Decision: Models never publish domain events directly. Only interactions and background jobs may publish. When a model's state change produces a side effect in another context, the publishing responsibility moves to the interaction that called the model method or to a subscriber job that reacts to the model change.
Rationale: The interaction layer is the single write boundary across all contexts. Publishing from interactions ensures a clear, auditable flow: interaction → model mutation → event emission → subscribers. Publishing from models (or from model callbacks) obscures the transaction boundary and prevents interactions from controlling the emission order or payload. Phase 53's CheckInHabit interaction publishing goal_tracking.habit_checked_in proved this pattern is cheap and correct; endorsing bounded exceptions would undermine it.
Implication: Goal#update_habit_streak! was refactored in Phase 54.1b to return the streak delta instead of publishing gamification.streak_advanced directly. The publishing responsibility moved to the two callers (CheckInHabit and UseStreakFreeze interactions), which now publish conditionally and can suppress publication when appropriate (e.g., system-triggered repairs suppress publish to avoid duplicate event storms).
Follow-up: Background jobs publish domain events too — e.g., ProcessHabitStreaksJob triggers streak-lapse logic. The general rule is: interactions publish for user-triggered events; scheduled jobs publish for time-based events. Deferred to Phase 55+ for formal policy.
Recorded: 2026-05-01
Webhook-Driven vs. Direct-Call Coupling
A useful lens for planning Phase 2 (Domain Events): webhook-driven boundaries are already asynchronous and loosely coupled to the producer. They are the highest-leverage seams for introducing a unified event emitter, because the mental model (receive → translate → publish) is already the right shape.
| Boundary | Kind | Today | Status |
|---|---|---|---|
| Clerk → User Identity | Webhook (sync, signed) | ACL service pair | Candidate for event emission |
| Agent Runner → AI Workforce | Webhook (HMAC) | ProcessWebhookResult interaction | Candidate for event emission (Phase 54+) |
| Stripe → Billing | Webhook (signed) | Unmediated in controller | Candidate for ACL + event emission (Phase 54+) |
| Goal Tracking → Gamification (CheckInHabit) | Event-driven | ✅ Emit goal_tracking.habit_checked_in | Implemented in v1.12 Phase 53 |
| Goal Tracking → Gamification (AddGoal badges) | Direct call | UserAction.create_first_goal_action_job | Candidate for goal.created event |
Last updated: 2026-09-15 — added Import/Export as a new node, conformist read-only edge to Goal Tracking, and Coupling Summary row (v4.64 Phase 2, OBJ-3822).
Previously: 2026-09-11 — added Guide Assistant (sub-context of AI Coaching) as a node with no cross-context edges (OBJ-3629).