Skip to content

System Overview

Shaker is the mobile application component of Bartendie, a sophisticated home bar management system. Built with React Native and Expo, Shaker provides cocktail enthusiasts with tools to manage their bar inventory, discover recipes, plan events, and streamline their home bartending experience.

┌─────────────────────────────────────────────────────────┐
│ Shaker Mobile App │
│ (React Native + Expo) │
├─────────────────────────────────────────────────────────┤
│ Presentation Layer │
│ - Expo Router Navigation │
│ - RetroUI Components (NeoBrutalism Design) │
│ - NativeWind Styling │
├─────────────────────────────────────────────────────────┤
│ Application Layer │
│ - Apollo Client (GraphQL) │
│ - Local Data Layer (Recipes, Ingredients, Products) │
│ - Custom Hooks & Utilities │
├─────────────────────────────────────────────────────────┤
│ Infrastructure Layer │
│ - Clerk Authentication │
│ - Phoenix Channels (Real-time) │
│ - Expo Modules (Camera, SecureStore, etc.) │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Well Backend │
│ (Elixir + Phoenix) │
│ - GraphQL API │
│ - PostgreSQL Database │
│ - Real-time Subscriptions │
└─────────────────────────────────────────────────────────┘
  • React Native - Cross-platform mobile development
  • Expo SDK 54 - Development platform with managed workflow
  • TypeScript - Type safety and developer experience
  • Expo Router 6 - File-based navigation system
  • Apollo Client 3.13 - GraphQL client with normalized cache
  • Local Data Layer - TypeScript-based data structures for offline-first features
  • MMKV (react-native-mmkv) - Fast binary key-value storage for persisting domain data
  • React Hooks - Component state and side effects
  • NativeWind - Tailwind CSS for React Native
  • RetroUI Design System - NeoBrutalism design tokens
  • Tailwind Variants - Type-safe component variants
  • Design Tokens - Centralized theme configuration
  • GraphQL - Type-safe API queries and mutations
  • Phoenix Channels - WebSocket-based real-time updates
  • Absinthe Socket - Phoenix channel integration for GraphQL subscriptions
  • Clerk - User authentication and session management
  • expo-secure-store - Encrypted local storage
  • JWT Tokens - Secure API authentication
  • expo-camera - Barcode scanning for product UPC lookup
  • expo-image - Optimized image loading and caching
  • expo-haptics - Tactile feedback
  • expo-keep-awake - Prevents screen sleep during bar service and event management
  • expo-file-system - File operations for import/export
  • expo-print - PDF generation for shopping lists

Shaker implements the Bartendie domain model with the following core entities:

Represents a cocktail recipe with ingredients, instructions, and metadata.

Key Attributes:

  • Name, description, category, difficulty
  • Ingredients (with quantities and units)
  • Step-by-step instructions
  • Glass type, ice type, garnishes
  • Flavor profile (calculated from ingredients)
  • Tags, ratings, images

Abstract concept of a bar ingredient (e.g., “Vodka”, “Lime Juice”).

Key Attributes:

  • Name, type (SPIRIT, LIQUEUR, JUICE, etc.)
  • Category (e.g., “Jamaican Rum”, “Citrus Juice”)
  • Flavor profile (8-dimensional vector)
  • Substitute ingredients (bidirectional relationships)

Flavor Profile Dimensions:

  • Sweet, Sour, Bitter, Umami
  • Spicy, Herbal, Floral, Fruity
  • Each dimension: 0-10 scale

Physical item in inventory (e.g., “Tito’s Vodka 750ml”).

Key Attributes:

  • Name, brand, size, price
  • UPC/barcode for scanning
  • Current level (percentage remaining)
  • Location in bar
  • Purchase history and price tracking
  • Maps to one or more Ingredients

Tracks stock of products, equipment, glassware, and tools.

Types:

  • Products (spirits, mixers, etc.)
  • Equipment (shakers, strainers, etc.)
  • Glassware (coupe, rocks, highball, etc.)
  • Tools (jiggers, muddlers, etc.)

Occasion with theme, guest list, and custom menu.

Key Attributes:

  • Name, date, theme, venue
  • Guest list with RSVPs
  • Custom menu (featured and standard drinks)
  • Bar setup configuration
  • Shopping list (auto-generated)

Procurement list for ingredients and supplies.

Features:

  • Auto-generation from recipes and events
  • Consolidation of duplicate ingredients
  • Quantity calculation based on guest count
  • Purchase tracking and inventory updates

Shaker follows DDD principles to maintain alignment with the Bartendie domain:

  1. Ubiquitous Language - Consistent terminology (Recipe, Ingredient, Product, Event)
  2. Bounded Contexts - Clear separation between Recipe, Inventory, and Event contexts
  3. Value Objects - Immutable objects like FlavorProfile, RecipeIngredient
  4. Aggregates - Recipe with Ingredients, Event with Menu and Guests
  • Local data layer for recipes and reference data
  • Apollo Client cache for GraphQL data
  • Optimistic UI updates
  • Background sync when online
  • Touch-optimized interactions (44pt minimum touch targets)
  • Haptic feedback for important actions
  • Loading states and skeleton screens
  • Error boundaries and graceful degradation
  • Platform-specific adaptations (iOS/Android)
  • Bold, high-contrast colors
  • Thick black borders (3-6px)
  • Solid shadows (no blur)
  • Playful, energetic aesthetic
  • Accessibility-first (WCAG AA compliance)
User Action → Component
Apollo Client (useQuery/useMutation)
GraphQL Request → Well Backend
Response → Apollo Cache (normalized)
Component Re-render (reactive)
User Action → Component
Local Data Layer (TypeScript)
Component State Update
Component Re-render
Backend Event → Phoenix Channel
Absinthe Socket Subscription
Apollo Cache Update
Component Re-render (reactive)

Shaker persists domain data on-device using MMKV for fast binary storage. This enables data to survive app restarts without requiring a network call.

infra/
├── localStorage.ts # MMKV wrapper (getItem / setItem)
├── hydrateLocalStorage.ts # Startup hydration – loads all domain data
└── migrateAsyncStorageToMMKV.ts # One-time migration from legacy AsyncStorage

All keys are namespaced under @bartendie/:

KeyData
inventory_productsProducts, instances, quantity tracking
venuesVenue list
inventory_toolsBar tools
inventory_glasswareGlassware collection
inventory_equipmentBar equipment
eventsEvents
menusEvent menus
budgetsBudget configurations
shopping_list_itemsShopping list items
bar_setupsBar setup configurations per event
ingredientsIngredient catalog
user_recipesUser-created recipes

Called from app/_layout.tsx after fonts load:

import { hydrateFromLocalStorage } from '@/infra/hydrateLocalStorage';
// For each domain entity:
// 1. Try to load from MMKV
// 2. If found, hydrate in-memory data store
// 3. If missing, seed MMKV from default mock data for next launch
await hydrateFromLocalStorage();

On first launch after upgrade, a one-time migration copies all data from AsyncStorage (the previous storage backend) to MMKV:

import { migrateAsyncStorageToMMKVIfNeeded } from '@/infra/migrateAsyncStorageToMMKV';
// Reads each STORAGE_KEY from AsyncStorage, writes to MMKV, sets migration flag
await migrateAsyncStorageToMMKVIfNeeded();

The migration flag @bartendie/_migrated_from_async prevents it from running more than once.

2026-03-06