AlejoTaller is organised as a single monorepo that spans two Android applications, a Svelte web client, and a Node microservice, all sharing a common architectural language. Every surface follows the same feature-first, layered decomposition and relies on shared Kotlin modules for business-critical logic — so changes to the sale domain, authentication contracts, or mapper rules propagate consistently without manual duplication.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/danielitoCode/AlejoTaller/llms.txt
Use this file to discover all available pages before exploring further.
Offline-first is a first-class design constraint, not an afterthought. Every surface that touches purchase or reservation data is built to write locally first, reconcile with Appwrite when connectivity allows, and consume real-time Pusher events to reduce perceived latency between actors. This principle shapes both the module boundaries and the repository contracts throughout the codebase.
Monorepo Structure
Feature-First Layered Structure
Both Android applications organise code by feature rather than by layer. Inside each feature, the same three-layer structure applies consistently.app/, alejotallerscan/, and — conceptually — the equivalent Svelte feature directories in web/.
Six Architectural Patterns
1. Offline-First Reconciliation
All purchase and reservation flows prioritise local persistence before remote confirmation. The system:- writes to the local Room database (or IndexedDB on web) immediately
- synchronises with Appwrite when connectivity is available
- maintains a pending sync queue for operations that could not be pushed immediately
- merges remote and local state without losing user context
2. Repository Pattern
Repository classes act as the single point of data access for each feature. They:- abstract local (Room / Dexie) access from domain logic
- abstract remote (Appwrite SDK) access from domain logic
- own the synchronisation and retry rules
- translate between DTOs (remote/local schema) and domain entities
domain/repository/, never with concrete data-layer types.
3. Use Case Per Action
Business logic is neither placed in ViewModels nor in large, multi-purpose repositories. Each meaningful action in the domain is encapsulated in its own use case class underdomain/caseuse/. Representative examples from the codebase include:
- authenticate user
- register a new sale
- interpret an incoming real-time event
- synchronise pending operator decisions
- enrich product data after a QR scan
4. Immutable UI State (StateFlow)
ViewModels expose a singleStateFlow<UiState> that the Compose UI observes. The state object is immutable and carries explicit fields for every condition the screen must handle:
loading— whether an async operation is in progressselectedItem— currently focused domain entityerror— actionable error messagenotice— transient informational feedbacksyncStatus— local/remote reconciliation status
5. Shared Domain Modules
The fourshared-* modules prevent logic from diverging between the Android client and the Android operator as the product grows.
| Module | Contains |
|---|---|
shared-auth | Auth session management, role definitions, operator access contracts |
shared-core | Cross-cutting utilities and commonly reused rules |
shared-data | DTOs, mapper support, base repository implementations, persistence helpers |
shared-sale | Sale entity, buy_state transitions, sale use cases |
app and alejotallerscan depend on these modules via Gradle. Any change to the sale domain propagates to both consumers without manual synchronisation.
6. Event-Driven Feedback
Sale verification does not rely on polling. Once the operator’s Android app confirms or rejects a reservation, the following sequence delivers the outcome to all subscribers in real time:- The customer (Android or web) creates a purchase or reservation in Appwrite.
- The operator Android app reads the pending reservation and makes a decision.
- The operator app updates
buy_statein Appwrite and verifies the remote change was applied. - Only after remote confirmation does the app call
POST /sale-verification/publishonalejo_publisher. - The publisher translates the decision into a
sale:confirmedorsale:rejectedPusher event on the user-specific channelsale-verification-{userId}. - Both the Android client and the Svelte web client are subscribed to that channel and update their UI immediately.
Offline-First Reconciliation in Detail
The reconciliation cycle works as follows:| Stage | What happens |
|---|---|
| Local write | The operation is persisted to Room (Android) or Dexie/IndexedDB (web) immediately |
| Remote push | The repository attempts to push to Appwrite; on failure the item enters the pending sync queue |
| Pending queue | The app retries failed pushes when connectivity is restored, using the stored local record |
| Remote pull | On app resume or network recovery, the repository fetches the latest remote state |
| Merge | Local and remote records are compared; the repository applies the reconciliation strategy for each feature |
| UI update | The StateFlow emits the reconciled entity and the screen reflects the final state |
How Shared Modules Prevent Divergence
Without theshared-* modules, the sale entity, its valid state transitions, and the authentication session model would need to be defined independently in both Android apps. Any change — such as adding a new buy_state value or a new field to the sale entity — would require identical updates in two places, with no compile-time guarantee they stay in sync.
By placing these definitions in dedicated Gradle modules that both apps depend on, a compile error in either app immediately surfaces any inconsistency. The shared modules are the technical enforcer of the product invariant: one same domain, multiple surfaces.