Skip to content

feat: enhanced modular w/ better subdomains & boundaries #173

@danielhe4rt

Description

@danielhe4rt

19 modules in app-modules/ grew organically. Circular dependencies exist between auth modules, hub models import from 6+ modules, trivially thin modules waste cognitive overhead, and Filament presentation code is mixed into domain modules. This reorganization separates concerns into clear bounded contexts, standardizes on Actions over Repositories/Services, and splits Filament panels into independent modules.

Current State: 19 Modules

authentication  badge  bot-discord  character  docs  events  feedback
he4rt  integrations  meeting  message  portal  provider  ranking
season  shared  sponsors  tenant  user

Problems

  1. Circular dependency: authentication <-> integrations <-> providerOAuthProviderEnum imports from integrations, integrations imports from authentication
  2. God Objects: Tenant imports from 6 modules, User from 4 external modules
  3. Filament mixed into domain: 95+ Filament files scattered across 10 modules — presentation logic entangled with business logic
  4. Trivially thin modules: ranking (1 action, 0 tables), portal (1 page), sponsors (only relates to events)
  5. Fragmented identity: User, tenant, auth, provider, integrations — 5 modules for one concern
  6. Monolithic integrations: Discord OAuth + Twitch OAuth bundled together despite being independent providers
  7. Over-engineered abstractions: Repositories (Contract + Eloquent impl), Services, Entities, Collections — unnecessary layers in 10+ modules when Actions + Models suffice
  8. All panels in one blob: A single panels module would still create merge conflicts and unclear ownership across 5 different panels

Architectural Changes

1. Actions Replace Repositories, Services, and Entities

Current state: Modules use a mix of Repository (Contract + Eloquent impl), Service classes, Entity value objects, and Collections — creating 4-5 abstraction layers between a use case and the database.

New standard: Actions are the only use-case abstraction. They use Eloquent Models directly. No Repositories, no Services, no Entities, no Collections wrappers.

Action Convention

declare(strict_types=1);

namespace He4rt\Gamification\Character\Actions;

use He4rt\Gamification\Character\DTOs\IncrementExperienceDTO;
use He4rt\Gamification\Character\Models\Character;

final class IncrementExperience
{
    public function __construct(
        private Character $character,
    ) {}

    public function handle(IncrementExperienceDTO $dto): void
    {
        $this->character->increment('experience', $dto->amount);
        // business logic...
    }
}

Rules:

  • Method: handle() (not __invoke)
  • Constructor: inject Models, other Actions, or framework services
  • Parameters: always DTOs for input data (never loose primitives)
  • Interfaces: only where a genuine abstraction is needed (e.g., OAuthClientContract for multiple identity providers)
  • Query-only "actions" (no validation, no exception, no side effects): inline at point of use — just use Model::query() directly. Don't create an Action for Badge::findOrFail($id).

What Gets Deleted Per Module

Layer Example Replacement
Contracts/CharacterRepository.php Interface for repository Delete. Use Model directly in Actions
Repositories/CharacterEloquentRepository.php Eloquent implementation Delete. Model queries live in Actions or inline
Entities/CharacterEntity.php Value object wrapping model data Delete. Use Model attributes directly
Collections/PastSeasonCollection.php Custom collection wrapper Delete. Use Eloquent Collection
Services/UpdateProfileService.php Orchestration class Delete. Becomes an Action
ValueObjects/UserId.php Typed ID wrapper Delete unless genuinely needed for type safety

Modules Affected

Module Has Repositories Has Services Has Entities Has Collections
badge yes - yes yes
character yes - yes yes
feedback yes - yes -
meeting yes - yes -
message yes - yes -
provider yes - yes -
ranking yes - - -
season yes - yes yes
user yes yes yes -

2. shared Module Dies — app/ Becomes Core

Current state: shared module contains Paginator, IntValueObject, TTL, and a contract.

New state: Inline these into app/ or the consuming module:

Shared item Destination
Contract/Paginator.php app/Contracts/Paginator.php
ValueObjects/IntValueObject.php Inline into consuming module or delete
TTL.php Inline into consuming module or delete
Factory.php Inline into consuming module or delete
Paginator.php app/Support/Paginator.php

Cross-module contracts (like OAuthClientContract) also live in app/Contracts/.


3. Panels Split Into Independent Modules

Current state (proposed single panels): One module with 95+ files across 5 panels — same merge conflict problem we're trying to solve.

New state: Each panel is its own module under app-modules/panel-*. This gives each panel its own ServiceProvider, tests, and clear ownership.

app-modules/
├── panel-admin/       # Filament Admin panel (/admin)
├── panel-app/         # Filament User panel (/app)
├── panel-guest/       # Blade + Livewire public site (/)
├── panel-event/       # Filament Event panel (/event)
└── panel-partner/     # Filament Partner panel (/partner)

PanelProviders stay in app/Providers/Filament/ — they are not moved into the modules. The modules contain Resources, Pages, Widgets, and Schemas; the PanelProviders in app/ wire them together.

Shared components (Login page, reusable schemas, cross-panel utilities) live in app/Filament/Shared/ or app/Livewire/.

panel-guest uses no Filament — it's pure Blade + Livewire with its own routes in panel-guest/routes/web.php.


Proposed Structure: 19 -> 16 Modules

app-modules/
├── identity/              # user + tenant + auth + external identity (socialite)
├── gamification/          # character + badge + ranking + season
├── economy/               # wallets, currencies, transactions
├── events/                # events + sponsors
├── community/             # meeting + feedback
├── activity/              # message + voice tracking (multi-provider)
├── integration-discord/   # Discord OAuth client
├── integration-twitch/    # Twitch OAuth + subscriber
├── bot-discord/           # Laracord bot (commands, events, tasks)
├── he4rt/                 # CSS/design system only
├── panel-admin/           # Filament admin panel
├── panel-app/             # Filament user panel
├── panel-guest/           # Blade + Livewire public site
├── panel-event/           # Filament event panel
└── panel-partner/         # Filament partner panel

app/ (core):

app/
├── Contracts/             # Cross-module interfaces (OAuthClientContract, Paginator)
├── Support/               # Shared utilities (Paginator impl, etc.)
├── Enums/                 # FilamentPanel enum
├── Filament/
│   └── Shared/            # Cross-panel components (Login, reusable schemas)
├── Providers/
│   ├── AppServiceProvider.php
│   ├── FilamentServiceProvider.php
│   └── Filament/
│       ├── AdminPanelProvider.php
│       ├── UserPanelProvider.php
│       ├── GuestPanelProvider.php    # may become unnecessary if panel-guest has no Filament
│       ├── EventPanelProvider.php
│       └── PartnerPanelProvider.php
├── Http/
├── Livewire/
└── Rules/

Domain Modules

1. identity (merge: user + tenant + authentication + provider)

All IAM concerns: who you are, what orgs you belong to, how you authenticate.

What Details
Namespace He4rt\Identity
Sub-namespaces User, Tenant, Auth, ExternalIdentity
Tables users, user_address, user_information, sessions, tenants, tenant_users, external_identities, external_identity_tokens, password_resets, personal_access_tokens
Key models User, Address, Information, Tenant, ExternalIdentity, AccessToken
Key actions ResolveUserContext, LinkExternalIdentity, UpdateProfile
DTOs UpdateProfileDTO, UpsertInformationDTO, UserContextDTO, ResolveExternalIdentityDTO

Rename: Provider -> ExternalIdentity

The current Provider model represents an external identity linked to a User or Tenant (polymorphic). It stores the platform user ID, username, avatar, and OAuth tokens. It is also used for activity attribution — messages and voice records reference it to track which external identity generated the interaction.

Current New Reason
Provider (model) ExternalIdentity Formal IAM term — the identity that comes from an external platform
Token (model) AccessToken More explicit — it's an OAuth access/refresh token pair
ProviderEnum (enum) IdentityProvider Discord and Twitch are identity providers
providers (table) external_identities Follows model rename
provider_tokens (table) external_identity_tokens Follows model rename
provider_id (FK in messages/voice) external_identity_id FK to external_identities
provider (column, enum cast) identity_provider Which platform (IdentityProvider enum)
provider_id (column, platform user ID) platform_user_id The user/guild ID on the external platform
ProviderResolver (action) ResolveExternalIdentity Follows domain rename
FindProvider (action) Inline at point of use Query-only, no validation/exception
NewAccountByProvider (action) LinkExternalIdentity Clearer intent

Current files being merged:

  • app-modules/user/src/ -> app-modules/identity/src/User/
  • app-modules/tenant/src/ -> app-modules/identity/src/Tenant/
  • app-modules/authentication/src/ -> app-modules/identity/src/Auth/
  • app-modules/provider/src/ -> app-modules/identity/src/ExternalIdentity/

Deleted in the process:

  • user/Repositories/, user/Contracts/, user/Entities/, user/Services/, user/ValueObjects/
  • provider/Repositories/, provider/Contracts/, provider/Entities/, provider/ValueObjects/
  • All Filament code -> panel-admin/, panel-app/
  • All Plugins -> deleted (panel modules register their own resources)

2. gamification (merge: character + badge + ranking + season)

XP, levels, badges, seasons, leaderboards — one domain.

What Details
Namespace He4rt\Gamification
Sub-namespaces Character, Badge, Season
Tables characters, characters_badges, characters_leveling_logs, badges, seasons, seasons_rankings
Key models Character, PastSeason, Badge, Season
Key actions InitializeCharacter, IncrementExperience, ClaimBadge, PersistDailyBonus, ManageReputation
Absorbs ranking entirely (1 action inlined at point of use)

Deleted:

  • character/Repositories/, character/Contracts/, character/Entities/, character/Collections/
  • badge/Repositories/, badge/Contracts/, badge/Entities/, badge/Collections/
  • season/Repositories/, season/Contracts/, season/Entities/, season/Collections/
  • ranking/ entirely
  • All Filament code -> panel-admin/

3. economy (new module, extracts Wallet from character)

Internal economy: wallets, currencies, and transactions. Supports polymorphic ownership (User, Character, Tenant).

The current characters_wallet table is tightly coupled to Character. The new economy module replaces it with a generic, polymorphic wallet system that any entity can own — enabling tenant-scoped economies where members transact with each other.

What Details
Namespace He4rt\Economy
Tables wallets, transactions
Key models Wallet, Transaction
Key actions CreateWallet, Credit, Debit, Transfer
DTOs CreditDTO, DebitDTO, TransferDTO
Enums Currency, TransactionType

Currency enum (system-wide, not per-tenant):

enum Currency: string
{
    case Coin = 'coin';   // He4rt Coin — gamification rewards, community engagement
    case Cash = 'cash';   // Monetary representation — future store purchases
}

Tenants can customize the label of each currency (e.g., "He4rt Coin" -> "Dev Moeda") but the currency itself is global.

Wallet model (polymorphic):

// wallets table
Schema::create('wallets', function (Blueprint $table) {
    $table->uuid('id')->primary();
    $table->uuidMorphs('owner');          // owner_type + owner_id (User, Character, Tenant)
    $table->string('currency');           // Currency enum
    $table->bigInteger('balance')->default(0);
    $table->timestamps();

    $table->unique(['owner_type', 'owner_id', 'currency']);
});

Transaction model (immutable ledger):

// transactions table
Schema::create('transactions', function (Blueprint $table) {
    $table->uuid('id')->primary();
    $table->foreignUuid('wallet_id')->constrained('wallets');
    $table->string('type');               // TransactionType enum
    $table->bigInteger('amount');          // positive = credit, negative = debit
    $table->bigInteger('balance_after');   // snapshot of wallet balance after transaction
    $table->nullableUuidMorphs('reference'); // polymorphic ref to what caused it (Event, Badge, etc.)
    $table->string('description')->nullable();
    $table->timestamps();
});

TransactionType enum:

enum TransactionType: string
{
    case Reward = 'reward';       // System -> User (event participation, badge claim, etc.)
    case Transfer = 'transfer';   // User -> User (P2P between members)
    case Purchase = 'purchase';   // User -> System (store redemption)
}

Transaction flows:

System reward (event participation):
  events dispatches -> EventAttended { user_id, event_id, tenant_id }
    economy listens -> Credit wallet, create Transaction(type: reward, reference: event)

P2P transfer:
  panel-app action -> Transfer
    economy -> Debit sender wallet, Credit receiver wallet, create 2 Transactions(type: transfer)

Store purchase (future):
  panel-app action -> Purchase
    economy -> Debit wallet, create Transaction(type: purchase, reference: product)

Key rules:

  • Wallets cannot have negative balance — Debit/Transfer actions validate before executing
  • All balance changes go through Actions that create a Transaction record (no direct $wallet->update())
  • balance_after on Transaction ensures auditability without recalculating from history
  • Communication with other modules via Laravel Events (economy listens, never called directly)

Migration from characters_wallet:

  • Create new wallets and transactions tables
  • Migrate existing characters_wallet balances to wallets with owner_type: Character, currency: coin
  • Drop characters_wallet table

4. events (merge: events + sponsors)

Event management + sponsorship — sponsors only exist in relation to events.

What Details
Namespace He4rt\Events
Tables events, events_attendees, events_talks, events_agenda, event_submission_speakers, sponsors, events_sponsors
Key models EventModel, EventSubmission, EventAgenda, Sponsor

Deleted:

  • All Filament code -> panel-admin/, panel-app/, panel-event/

5. community (merge: meeting + feedback)

Tenant-scoped community engagement features.

What Details
Namespace He4rt\Community
Sub-namespaces Meeting, Feedback
Tables meetings, meeting_types, meeting_participants, feedbacks, feedback_reviews
Key models Meeting, MeetingType, Feedback, Review

Deleted:

  • meeting/Repositories/, meeting/Contracts/, meeting/Entities/
  • feedback/Repositories/, feedback/Contracts/, feedback/Entities/
  • All Filament code -> panel-admin/

6. activity (rename: message)

Multi-provider activity tracking: messages, voice, XP attribution.

What Details
Namespace He4rt\Activity
Tables messages, voice_messages
Key models Message, Voice
Key actions NewMessage, NewVoiceMessage, PersistMessage
DTOs NewMessageDTO, NewVoiceMessageDTO

Deleted:

  • message/Repositories/, message/Contracts/, message/Entities/
  • All Filament code -> panel-admin/

7. integration-discord (split from: integrations)

Transport layer: Discord HTTP/OAuth client. No domain logic.

What Details
Namespace He4rt\IntegrationDiscord
Tables none
Contains DiscordOAuthClient (implements OAuthClientContract from app/Contracts/), Discord OAuth DTOs

8. integration-twitch (split from: integrations)

Transport layer: Twitch HTTP/OAuth client + subscriber API. No domain logic.

What Details
Namespace He4rt\IntegrationTwitch
Tables none
Contains TwitchOAuthClient, TwitchSubscriberClient, subscriber DTOs

9. bot-discord (stays)

Laracord bot. Catches Discord events, dispatches Laravel events.

10. he4rt (stays, CSS only)

Design system: custom Blade components, fonts, CSS themes.


Panel Modules

11. panel-admin — Filament Admin (/admin)

Full admin panel. All CRUD resources for all domain modules.

What Details
Namespace He4rt\PanelAdmin
Tables none
Depends on identity, gamification, economy, events, community, activity
app-modules/panel-admin/src/
├── Resources/
│   ├── Users/                  # from identity
│   │   ├── UserResource.php
│   │   ├── Pages/
│   │   ├── Schemas/
│   │   └── Tables/
│   ├── Tenants/                # from identity
│   ├── Badges/                 # from gamification
│   ├── Seasons/                # from gamification
│   ├── Wallets/                # from economy
│   ├── Transactions/           # from economy
│   ├── Events/                 # from events
│   ├── Talks/                  # from events
│   ├── EventAgenda/            # from events
│   ├── Sponsors/               # from events
│   ├── Meetings/               # from community
│   ├── MeetingTypes/           # from community
│   ├── Feedback/               # from community
│   └── Messages/               # from activity
├── RelationManagers/
│   ├── AttendeesRelationManager.php
│   ├── TalksRelationManager.php
│   ├── EventsRelationManager.php
│   └── MembersRelationManager.php
├── Widgets/
│   ├── SeasonStatsOverview.php
│   ├── UsersStatsOverview.php
│   └── ActiveEventsStats.php
├── Providers/
│   └── PanelAdminServiceProvider.php
└── tests/

12. panel-app — Filament User Panel (/app)

Authenticated user dashboard, profile, tenant-scoped features.

What Details
Namespace He4rt\PanelApp
Tables none
Depends on identity, gamification, economy, events
app-modules/panel-app/src/
├── Pages/
│   ├── Dashboard.php
│   └── UserProfile.php
├── Resources/
│   ├── EventModels/            # user-facing event list
│   └── Talks/                  # user-facing talk submissions
├── Widgets/
│   └── LatestEvents.php
├── Schemas/
│   ├── UserInformationForm.php
│   └── UserAddressForm.php
├── Providers/
│   └── PanelAppServiceProvider.php
└── tests/

13. panel-guest — Blade + Livewire Public Site (/)

No Filament. Pure Blade + Livewire with own routes and controllers.

What Details
Namespace He4rt\PanelGuest
Tables none
Depends on identity, events
Absorbs portal + docs modules entirely
app-modules/panel-guest/
├── src/
│   ├── Http/
│   │   └── Controllers/
│   ├── Livewire/
│   │   └── Components/
│   └── Providers/
│       └── PanelGuestServiceProvider.php
├── routes/
│   └── web.php
├── resources/
│   └── views/
└── tests/

14. panel-event — Filament Event Panel (/event)

Per-tenant event landing pages and participant dashboards.

What Details
Namespace He4rt\PanelEvent
Tables none
Depends on identity, events
app-modules/panel-event/src/
├── Pages/
│   ├── EventLandingPage.php
│   └── ParticipantDashboard.php
├── Schemas/
│   └── StartEndFieldsSchema.php
├── Providers/
│   └── PanelEventServiceProvider.php
└── tests/

15. panel-partner — Filament Partner Panel (/partner)

What Details
Namespace He4rt\PanelPartner
Tables none
Depends on identity, events
app-modules/panel-partner/src/
├── Resources/
│   └── ...
├── Providers/
│   └── PanelPartnerServiceProvider.php
└── tests/

Module Standard Structure

After refactoring, every domain module follows this standard:

app-modules/{module}/
├── src/
│   ├── Actions/           # Use cases (handle() method, DTO params)
│   ├── DTOs/              # Input/output data transfer objects
│   ├── Enums/             # Domain enums
│   ├── Exceptions/        # Domain exceptions
│   ├── Models/            # Eloquent models (direct usage, no wrapper)
│   ├── Observers/         # Model observers (if needed)
│   └── Providers/
│       └── {Module}ServiceProvider.php
├── database/
│   └── migrations/
├── routes/
├── config/
└── tests/

NOT allowed in domain modules:

  • Repositories/ — use Model directly in Actions
  • Contracts/ — use app/Contracts/ for cross-module interfaces
  • Entities/ — use Model attributes directly
  • Collections/ — use Eloquent Collection
  • Services/ — becomes an Action
  • Filament/ — lives in panel-* modules
  • Plugins/ — panel modules register their own resources

Every panel module follows this standard:

app-modules/panel-{name}/
├── src/
│   ├── Resources/         # Filament resources (with Pages/, Schemas/, Tables/)
│   ├── Pages/             # Standalone Filament pages
│   ├── Widgets/           # Filament widgets
│   ├── Schemas/           # Reusable form schemas
│   ├── RelationManagers/  # Relation managers (if needed)
│   └── Providers/
│       └── Panel{Name}ServiceProvider.php
└── tests/

Plugin Registration Change

Current: Each domain module registers a Filament Plugin via Panel::configureUsing() in its ServiceProvider.

New: Panel modules register their own resources directly in their ServiceProvider. Domain modules have zero knowledge of Filament.

// BEFORE: app-modules/badge/src/Providers/BadgeServiceProvider.php
Panel::configureUsing(function (Panel $panel): void {
    match ($panel->currentPanel()) {
        FilamentPanel::Admin => $panel->plugin(new AdminBadgePanelPlugin),
        default => null,
    };
});

// AFTER: app-modules/panel-admin/src/Providers/PanelAdminServiceProvider.php
// Resources are auto-discovered or registered directly — no Plugin classes needed.
// Domain modules (badge, etc.) have no Filament code at all.

Communication Architecture: Event-Driven

Current state: Zero Laravel events. All cross-module communication is direct action calls (tight coupling).

New principle: Integration modules are transport layers. Cross-module communication happens through Laravel Events.

Current (tightly coupled):

Discord API -> Laracord Event -> bot-discord/MessageReceivedEvent
  -> directly calls message/NewMessage.persist()
    -> directly calls character/IncrementExperience
    -> directly calls user/ResolveUserContextService

Target (event-driven):

Discord API -> Laracord Event -> bot-discord/MessageReceivedEvent
  -> dispatches Laravel Event: ActivityMessageReceived
    -> activity module LISTENS -> persists message, calculates XP
    -> gamification module LISTENS -> increments character experience

Key Event Flows

Message received (from any platform — Discord, Twitch, WhatsApp):

bot-discord dispatches -> ActivityMessageReceived { external_identity_id, channel_id, content, tenant_id }
  activity listens -> PersistMessage
  gamification listens -> IncrementExperience

Voice state change:

bot-discord dispatches -> ActivityVoiceStateChanged { external_identity_id, channel, state, tenant_id }
  activity listens -> PersistVoiceMessage
  gamification listens -> IncrementExperience (voice)

New member joins:

bot-discord dispatches -> MemberJoined { provider_data, tenant_id }
  identity listens -> ResolveUserContext, LinkExternalIdentity, InitializeCharacter

Where Events Live

Events are defined by the dispatching module. Listeners are defined by the consuming module.

  • bot-discord/src/Events/Laravel/ActivityMessageReceived, ActivityVoiceStateChanged, MemberJoined
  • activity/src/Listeners/OnActivityMessageReceived, OnActivityVoiceStateChanged
  • gamification/src/Listeners/OnActivityMessageReceived (XP calc), OnMemberJoined (char init)
  • identity/src/Listeners/OnMemberJoined (resolve user context)

Dependency Direction

he4rt                <- CSS/views only, no domain deps

identity             <- core domain (user, tenant, auth, external identity)

gamification         <- depends on: identity (models only)

economy              <- depends on: identity (models only, polymorphic owners)

activity             <- depends on: identity (models only)

events               <- depends on: identity (models only)

community            <- depends on: identity (models only)

integration-discord  <- transport layer, depends on: app/Contracts (OAuthClientContract)

integration-twitch   <- transport layer, depends on: app/Contracts (OAuthClientContract)

bot-discord          <- transport layer, dispatches events (minimal direct deps)

panel-admin          <- depends on: identity, gamification, economy, events, community, activity
panel-app            <- depends on: identity, gamification, economy, events
panel-guest          <- depends on: identity, events (Blade + Livewire, no Filament)
panel-event          <- depends on: identity, events
panel-partner        <- depends on: identity, events

Key rules:

  • Domain modules never import from panel-*. Panel modules import from domain modules.
  • integration-* modules are thin transport/HTTP clients. They don't call domain actions.
  • bot-discord catches Laracord (Discord.php) events and dispatches Laravel events. It does NOT directly call domain actions.
  • Domain modules communicate through Laravel events, not direct action imports.
  • Domain modules may depend on identity for User/Tenant models (shared kernel entities).
  • Cross-module contracts live in app/Contracts/, not in any module.

Table Ownership

Module Tables Count
identity users, user_address, user_information, sessions, tenants, tenant_users, external_identities, external_identity_tokens, password_resets, personal_access_tokens 10
gamification characters, characters_badges, characters_leveling_logs, badges, seasons, seasons_rankings 6
economy wallets, transactions 2
events events, events_attendees, events_talks, events_agenda, event_submission_speakers, sponsors, events_sponsors 7
community meetings, meeting_types, meeting_participants, feedbacks, feedback_reviews 5
activity messages, voice_messages 2
integration-discord 0
integration-twitch 0
bot-discord 0
panel-admin 0
panel-app 0
panel-guest 0
panel-event 0
panel-partner 0
he4rt 0
Total 32 (+6 infra)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions