ledgerrz/DECISIONS.md
Daan Meijer d68fc33bcb
Some checks failed
linter / quality (push) Failing after 1m3s
tests / ci (8.3) (push) Failing after 48s
tests / ci (8.4) (push) Failing after 1m5s
tests / ci (8.5) (push) Failing after 1m4s
extra documentation updates
2026-06-22 17:27:36 +02:00

11 KiB

Decisions

This document outlines the decisions made during the development of the Ledgerrz application.

Core Concepts

  • Dynamic: A relationship between two or more users. This will be the core container for interactions.
  • Ledger: A score-tracking entity within a Dynamic. The name "Ledger" will be used instead of "Score" to establish a more distinct domain language.
  • Mutation: An event that modifies a Ledger. It can be a direct modification or a suggestion requiring approval.
  • Participant: A user's role within a Dynamic. This will be used to manage permissions.

Technology Stack

  • Backend: Laravel
  • Frontend: Vue.js with Inertia.js
  • Real-time: Laravel Echo via @laravel/echo-vue for notifications and real-time updates.
  • Testing: Pest for PHP tests.
  • Styling: BEM (Block, Element, Modifier) methodology. Replaced verbose inline Tailwind classes in custom HTML templates with semantic BEM classes (e.g., .c-chat, .user-info__avatar), and mapped them to CSS rules using Tailwind v4's @apply directive inside scoped <style> blocks. This preserves the clean, dark, BDSM-themed aesthetic while significantly improving markup readability.

Architectural & Integration Decisions

1. Style Architecture Shift to BEM & Modern CSS Nesting

To improve front-end maintainability, we transitioned the core reusable layout and page components away from raw utility-class markup. Styles are now strictly encapsulated within Vue Single File Component (SFC) <style scoped> blocks (or dedicated BEM component files under /resources/css/components/).

  • CSS Nesting Standard (Critical Learning): Standard CSS/PostCSS nesting (unlike SASS) does not support class suffix concatenation (e.g. .block { &__element { ... } } is invalid). All nested selectors must declare the full class name explicitly (e.g. .block { .block__element { ... } }), compiling to standard descendant selectors (.block .block__element). Suffix nesting was refactored across all 12 component stylesheets to ensure native CSS browser and LightningCSS compatibility.
  • Encapsulation: Used Tailwind v4 @apply directives inside these blocks, referencing our central stylesheet (@reference "../../css/app.css") to pull in custom themes and variables cleanly.
  • Third-Party Safety: Third-party shadcn-vue library primitives (located in components/ui/) were kept intact to preserve vendor update integrity.

2. BelongsToMany Pivot Loading caveat

When accessing pivot attributes on relationship models on the front-end (e.g. participant.pivot.role), the relationship definition in the Eloquent model must explicitly call ->withPivot('role'). Omitting this will cause the pivot object to fail loading role attributes silently in Javascript, breaking role-based template gates. We added this to App\Models\Dynamic::participants().

3. Robust Real-time Echo Fallback & Deduplication

To address potential initialization and bundling errors under local-development:

  • Module Deduplication: Configured resolve.dedupe: ['@laravel/echo-vue'] in vite.config.ts to ensure exactly one instance of the Echo plugin and configuration state is shared across main and lazy-loaded bundles.
  • Defensive Initialization: Added checks using echoIsConfigured() and configured fallback connection parameters (e.g., 'mock-key') in both app.ts and Chat.vue's setup functions. This ensures that missing environment variables (such as VITE_REVERB_APP_KEY) do not trigger fatal Pusher initialization crashes that block component rendering, allowing the websocket to fail silently (which is correct when Reverb is not running locally).

4. Controller Authorization Fix

We imported the Illuminate\Foundation\Auth\Access\AuthorizesRequests trait directly into LedgerController.php. This fixes the 500 Internal Server Error when loading the ledger page, which was caused by LedgerController calling $this->authorize('view', ...) without inheriting the required method from the Laravel 11 base Controller.

5. Polymorphic Multiple-Media Attachment Support

We added a polymorphic attachments system allowing any database model to attach multiple photos and videos cleanly:

  • Created Media polymorphic model and a migration mapping mediable_id and mediable_type.
  • Attached and verified uploads across Message (inline chat images/videos), Ledger (cover documents), and Mutation (chore submission receipts and proof).
  • Built a highly reusable .c-lightbox CSS component for modal previews of both image and video uploads, avoiding duplication in individual modules.

6. Cryptographically Secure Dynamic Invitation System

We implemented a secure Dynamic Invitation flow to allow owners to invite new members to a Dynamic under a specific role:

  • Signed temporary URLs: Invitation links dispatched in mailable emails (DynamicInvitationMail.php) use Laravel's temporary signed URLs, expiring in 7 days, to prevent link tampering.
  • Intended-Email Check Gate: To prevent link hijacking, the accept gate strictly checks that the authenticated user's email matches the exact email specified on the invitation:
    if ($request->user()->email !== $invitation->email) {
        abort(403, 'This invitation was sent to a different email address.');
    }
    
  • Access Control: Access to invitations and creation is fully protected under Owner-only authorization checks.

7. Centralized Activity Service for System Messages

We created app/Services/ActivityService.php to centralize the creation of system messages and activities.

  • System Messages as null user_id: System messages are stored as Message records with a null user_id, cleanly distinguishing them from user-generated content.
  • Polymorphic Subject Linking: System messages are linked to relevant entities (e.g., a User who joined a dynamic, a Ledger that was created) via a polymorphic subject relationship on the messages table. This allows system messages on the dashboard to link directly to the relevant entity.
  • Seeder Refactoring: The DatabaseSeeder was refactored to use the ActivityService to generate all system messages, ensuring consistency.

8. Event-Driven Automated System Logging

We relocated the dynamic system activity message generation out of individual controller endpoints and into the Eloquent Mutation Model's booted -> created event hook.

  • Unified generation: Any mutation creation (whether occurring via a controller submission, an automated Pest test factory, or seeders during php artisan db:seed) now automatically and reliably generates correct system log messages.
  • Database Seeder fixed: Reverted the database seeder (DatabaseSeeder.php) back to using standard clean Eloquent creations. Since model events automatically trigger, php artisan db:seed executes cleanly and builds a rich, fully populated database history with system messages out-of-the-box.

9. Standardized Policy-Driven UI Capabilities (can prop)

To maintain strict data security and clean up front-end markup, we eliminated unstandardized, hardcoded client-side role checks (like isOwner properties or manual pivot.role === 'owner' checks) and replaced them completely with dynamic policy-driven capabilities returned directly from Laravel policies as can objects.

  • Centralized checks: Both the dynamic and ledger show routes return a standard can prop to the frontend (e.g., can: { update: boolean, close: boolean }).
  • Polymorphic Resource Capabilities: Each mutation model is wrapped in MutationResource which appends its own localized policy checks (update for approving suggestions, void for voiding) at the individual record level.
  • State-based policies: The MutationPolicy methods enforce both ownership authorization and state-based business constraints simultaneously (e.g., a mutation can be approved/updated only if its status is currently 'pending'; and can be voided only if its status is not 'voided'). This keeps the Vue templates purely declarative (e.g., v-if="mutation.can.update") and automatically protects the backend controllers against illegal state transitions.

10. Ledger-Scoped Predefined Mutation Templates ("Rewards")

Predefined mutations act as point-based templates ("purchases" or reusable chores) and belong strictly to specific Ledgers instead of broad Dynamics.

  • Domain Alignment: Moving the resource nesting under ledgers (dynamics.ledgers.predefined-mutations) aligns perfectly with the mental model of spending points on a ledger.
  • Type-Less Rewards: To simplify both the database schema and UI/UX, we eliminated the explicit type column ('reward' vs. 'penalty') from predefined mutations. They act as generic point-carrying "Rewards" whose amount can naturellement be positive (earning points) or negative (making a purchase / deducting points) without requiring restrictive explicit categorization.

11. Silent XHR Chat Pagination & Smooth Scrolling UX

To optimize chat-feed performance and improve overall user experience:

  • Silent pagination (No URL pollution): Rather than using Inertia router.get visits which push ?page=x into the browser URL and break history during page reloads, we implemented a silent background fetch (using native browser fetch()) that queries our dedicated messages JSON API endpoints and prepends older messages silently to the feed.
  • Scroll Preservation: Added preserveScroll: true to the Inertia form.post call in Chat.vue to prevent the active page scroll position from jumping or shifting when a new message is successfully submitted.

Initial Database Schema

I will start with a basic schema and evolve it as I build features.

  • users: Standard Laravel users table.
  • dynamics:
    • id
    • name
    • rules (TEXT)
    • created_at, updated_at
  • ledgers:
    • id
    • dynamic_id
    • name
    • rules (TEXT)
    • score (INTEGER, default 0)
    • created_at, updated_at
  • mutations:
    • id
    • ledger_id
    • user_id (author)
    • type (e.g., 'addition', 'subtraction')
    • amount (INTEGER)
    • description (TEXT)
    • status (e.g., 'approved', 'pending', 'rejected')
    • created_at, updated_at
  • participants: (Pivot table for users and dynamics)
    • user_id
    • dynamic_id
    • role (e.g., 'owner', 'editor', 'viewer')
  • media: For handling media uploads associated with mutations.
  • chats: For the chat streams on Dynamics and Mutations.

Development Approach

  1. Setup: Set up basic project structure, including models and migrations for the initial schema.
  2. Dynamics: Implement the creation and management of Dynamics.
  3. Ledgers: Implement the creation and management of Ledgers within a Dynamic.
  4. Mutations: Implement the core functionality of adding and suggesting mutations.
  5. UI: Build out the Vue components for each feature, focusing on a clean, dark, BDSM-themed aesthetic.
  6. Real-time: Integrate Laravel Reverb for notifications.
  7. Testing: Write Pest tests for all new backend functionality.
  8. Git: Use feature branches and make regular commits.

This is a living document and will be updated as the project progresses.