A local-first mobile app built with Expo and Livestore, featuring SQLite persistence and reactive state management.
Status: Early development - Android only
- 🚀 Local-first architecture with SQLite persistence
- ⚡ Reactive state management with Livestore
- 📱 Android native app built with Expo
- 🎨 NativeWind (Tailwind CSS for React Native)
- 🔧 Effect-based service layer for configuration and dependency injection
- 🔄 React 19 with automatic compiler optimizations
- Expo (v54) - React Native framework with dev client
- React (v19.1) - UI framework with compiler optimizations
- React Native (v0.81.4) - Mobile framework
- TypeScript (v5.9.3) - Type safety
- Livestore (v0.3.1) - Local-first state management and sync
@livestore/adapter-expo- Expo SQLite persistence adapter@livestore/react- React hooks@livestore/devtools-expo- Development tools@livestore/sync-cf- Cloudflare sync (planned)
- @effect-atom/atom-react (v0.3.3) - Reactive atoms with Effect integration
- NativeWind (v5 preview) - Tailwind CSS for React Native
- @rn-primitives/portal - Portal primitives
- class-variance-authority - Variant utilities
- tailwindcss-animate - Animation utilities
- Effect (v3.15.4) - Functional programming primitives for services, configuration, and logging
- Husky - Git hooks for pre-commit checks
- Lint-staged - Run Prettier on staged files
- Prettier - Code formatting with Tailwind plugin
- ESLint - Code linting with Expo config + React Compiler plugin
- Client: React Native app built with Expo (
src/app.tsx) - Target: Android only (iOS support TBD)
- Local Database: Multi-tenant SQLite with Expo adapter
- State Management: Livestore with reactive queries + Effect atoms for services
The app uses a dual-database architecture for multi-tenancy:
- System Database (
luna-system) - Stores cross-user configuration (sessionID, sync settings) - User Databases (
luna-{sessionID}) - Isolated per-user data (cycle data, symptoms, etc.)
This architecture enables:
- Persistent anonymous sessions that survive app restarts
- Seamless transition from anonymous to authenticated users
- Cross-device sync for authenticated users (sessionID = userId)
- Data isolation between users on shared devices
Livestore - For user domain data (queries, mutations) Effect Atoms - For service layer and cross-cutting concerns (auth, config)
Effect atoms provide reactive state with Effect integration, allowing service logic to be accessed from React components while maintaining Suspense compatibility.
- Remote Sync: Cloudflare Durable Objects via Livestore sync engine (TBD)
- Encryption: Awaiting Livestore native support or custom implementation (TBD)
- Authorization: Serverless solution preferred for production (currently mocked)
- Conflict Resolution: Latest-wins strategy (Livestore default) - may be enhanced later
- Bun >= 1.0.0 (JavaScript runtime and package manager)
- Android Studio and Android SDK (for Android development)
-
Clone the repository:
git clone git@github.com:brutcha/luna-clone.git cd luna-clone -
Install dependencies:
bun install
-
Set up environment variables:
cp .env.example .env.local
Available environment variables:
EXPO_PUBLIC_LOG_LEVEL- Log level (Trace|Debug|Info|Warn|Error|Fatal, default:Info)
-
Run the app:
bun start
Then press
ato open on Android emulator/device.
| Command | Description |
|---|---|
bun start |
Start Expo development server |
bun android |
Build Android app (alias for expo run:android) |
bun android:debug |
Build Android app in debug optimized mode |
bun lint |
Run ESLint with Expo config |
bun typecheck |
Run TypeScript type checking |
bun prepare |
Set up Husky git hooks (runs automatically after install) |
luna-clone/
├── assets/ # Images and static assets
├── docs/ # Documentation
│ └── adr/ # Architecture Decision Records
├── src/ # Source code
│ ├── index.ts # Entry point
│ ├── app.tsx # Main app component
│ ├── global.css # Global styles (NativeWind/Tailwind)
│ ├── components/ # React components
│ │ ├── cycle-ring/ # Cycle ring component
│ │ └── ui/ # UI primitives
│ ├── domain/ # Domain models and error types
│ ├── helpers/ # Utility helpers
│ └── lib/ # Core libraries
│ ├── atoms/ # Effect atoms (reactive service state)
│ ├── hooks/ # Custom React hooks
│ ├── livestore/ # Livestore configuration
│ │ ├── schema.ts # User database schema
│ │ ├── system-schema.ts # System database schema
│ │ ├── adapter.ts # Expo SQLite adapter
│ │ └── queries.ts # Reactive queries
│ ├── providers/ # React context providers
│ │ └── livestore-provider.tsx # LiveStoreProvider wrapper
│ └── services/ # Effect services (auth, session, config, logging)
├── app.json # Expo configuration
├── babel.config.js # Babel configuration
├── eslint.config.js # ESLint configuration
├── metro.config.js # Metro bundler configuration
├── nativewind-env.d.ts # NativeWind type definitions (must be in root)
├── package.json # Project dependencies
├── postcss.config.js # PostCSS configuration for Tailwind
└── tsconfig.json # TypeScript configuration
Defines tables, events, and materializers for user-specific data (cycles, symptoms, etc.). Each user gets an isolated database instance.
Defines tables for cross-user configuration (sessionID, sync settings). Single shared database for system-level state.
Configures @livestore/adapter-expo for SQLite persistence with proper SQL.js configuration.
Wraps LiveStoreProvider and manages per-user store lifecycle. Uses session hooks to determine the correct database instance.
Reactive queries using Livestore's queryDb pattern. Queries automatically re-run when underlying data changes.
Pattern:
import { useQuery } from "@livestore/react";
// Use reactive queries in components
const MyComponent = () => {
const data = useQuery(myQuery$);
return <View>{/* render data */}</View>;
};Services use Effect.Service pattern for dependency management and lifecycle control. Services are scoped, ensuring proper resource cleanup.
- AuthService - Manages sessionID lifecycle, login/logout
- SessionStoreService - Manages system database for session persistence
- LivestoreService - Provides access to user Livestore instance
- GlobalConfigService - Environment variables and app configuration
- LoggingService - Structured logging with Effect integration
Effect atoms bridge Effect services with React components, providing reactive state with Suspense support.
Pattern:
// In atom file
const myRuntime = Atom.runtime(MyServiceLayer);
export const myAtom = myRuntime.atom(
Effect.gen(function* () {
const service = yield* MyService;
return yield* service.getData();
})
);
// In component
import { useAtomSuspense } from "@effect-atom/atom-react";
const MyComponent = () => {
const { value } = useAtomSuspense(myAtom);
return <Text>{value}</Text>;
};The project uses TypeScript path aliases for cleaner imports:
import { schema } from "@/lib/livestore/schema";
import { LivestoreService } from "@/lib/services/livestore-service";Configured in tsconfig.json with the @/* alias mapping to ./src/*.
nativewind-env.d.ts- NativeWind/React Native CSS types (must be in root)src/types/- Custom type definitions and module augmentations
SessionID is managed by AuthService with the following behavior:
- Anonymous users: SessionID is auto-generated and persisted in system database
- Authenticated users: SessionID equals userId for cross-device sync
- Logout: Creates new anonymous sessionID
When a user logs in with their userId:
- SessionID is set to userId
- User database changes from
luna-{randomID}toluna-{userId} - Same database is used across all user's devices
- Livestore sync handles data replication
SessionID persists in the system database, surviving app restarts. On first launch:
- AuthService checks system database for existing sessionID
- If none exists, generates new anonymous sessionID
- SessionID is used to determine which user database to load
The project uses the React 19 Compiler for automatic performance optimizations:
- Automatic memoization - no manual
useMemo,useCallback, orReact.memoneeded - Reduced boilerplate code
- Improved re-render efficiency
- Build-time optimization
- ESLint plugin:
eslint-plugin-react-compiler(v19.1.0-rc.2) - Runs automatically during development and production builds
- Follow ESLint warnings to ensure optimal compiler results
The project uses Husky for automated code quality checks:
- Pre-commit: Runs Prettier on staged files via
lint-staged - Setup: Automatically configured by
bun prepare(runs afterbun install)
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- Follow the existing code style
- Run
bun lintbefore committing - Prettier will auto-format your code on commit
This project is licensed under the MIT License - see the LICENSE file for details.
- Resolve Livestore adapter compatibility with Expo SDK 54
- Implement remote sync with Cloudflare Durable Objects
- Add end-to-end encryption
- Replace mocked AuthClient with serverless authentication
- Add iOS support
- Enable Livestore devtools for debugging
- Livestore - Local-first state management
- Expo - React Native framework
- Effect - Functional programming for TypeScript
- NativeWind - Tailwind CSS for React Native