Overview
Floret is a discreet, mobile-first application for tracking consumption, managing personal inventory and understanding usage over time. It is built for a privacy-sensitive audience: the product deliberately feels like a well-kept garden journal, not a pharmacy dashboard — warm, precise and unobtrusive.
The platform pairs a cross-platform React Native (Expo) application with a Symfony API backed by MongoDB, and is delivered as a freemium product with subscription billing via Stripe. The brief was to build a polished consumer app that works reliably with intermittent connectivity, keeps sensitive data secure, and turns raw consumption logs into genuinely useful insight — run-out predictions, wellbeing correlation and usage analytics.
The Challenge
A consumption tracker only earns daily use if logging is instant and dependable. That set the core constraints:
- Offline-first. A log must always succeed locally and reconcile with the server later, without data loss or duplication.
- Discretion and security. Credentials and tokens must be stored securely on-device, and the interface must stay calm and private.
- Insight, not just records. The product's value is in presentation and analysis — clean data, run-out forecasting, and optional before/after mood tracking.
- A real strain catalogue. Users expect to search a large library rather than type free text, without the cost of maintaining that dataset in-house.
- Sustainable monetisation. A freemium model with a clean paywall and robust subscription lifecycle handling.
The Solution
Floret is a two-tier product: a thin, resilient mobile client over a stateless JSON API, with an on-device database acting as the source of truth for the user until the server confirms a write. The client never blocks on the network — writes are committed to a local SQLite database and enqueued; a background process replays the queue against the API when connectivity returns.
Expo SDK 56 / React Native 0.85 / React 19 client with expo-router navigation, TanStack Query and Zustand — thin screens over a typed API and offline engine.
Symfony 7.4 / PHP 8.2+ API layered controller → service → repository over a MongoDB document store, with JWT auth, rate limiting and scheduled work.
The Mobile Application
Built with Expo SDK 56 / React Native 0.85 / React 19 using expo-router file-based navigation.
- State & data — TanStack Query manages all server state (caching, background refetch, optimistic updates); Zustand holds lightweight client state across focused stores (
auth,offline,theme,presets). A typed API layer wraps every resource behind an Axios client with automatic JWT injection and 401/403 session-recovery interceptors. - Security & platform — expo-secure-store keeps access tokens in the device keystore/keychain; expo-notifications delivers reminders; EAS manages native builds and release channels.
- Interface — “Botanical Calm” — a deliberate design system: a single moss-green brand family, ink-toned data, a warm paper canvas, and semantic supply colours (leaf → amber → clay) that shift as stock runs low. Typeset in Fraunces (display) and Inter (body), with gifted-charts analytics and reanimated motion.
The Offline-First Engine
The heart of the app's reliability. Every write is committed locally first, then reconciled:
- expo-sqlite provides the local database — the user's source of truth until the server confirms.
- A durable sync queue records every create/update/delete with its payload, retry count and last error; the queue drains in order once online.
- NetInfo drives connectivity state and a
useOfflineSynchook orchestrates reconciliation — so the UI reflects pending, syncing and synced states honestly, with no data loss or duplication.
The API & Domain Model
A Symfony 7.4 / PHP 8.2+ application following clean controller → service → repository layering over MongoDB (Doctrine MongoDB ODM). Core documents: User / UserPreferences, Entry (a consumption event — method, amount, unit, notes, timestamp), Inventory, Strain / StrainSnapshot, MoodAssessment and Reminder.
Embedding an immutable StrainSnapshot into each entry captures strain details as they were at the time of logging, so historical records stay accurate even if the catalogue entry later changes.
A weighted average of recent daily usage (14-day window) projects a run-out date with an honest confidence rating (no_data → low → medium → high) based on available data points.
Cache-first search served from MongoDB, falling back to the Weedmaps API (with a strict timeout) only when short of results, persisting what it fetches. A scheduled command keeps the cache warm.
Stripe integration for the freemium upgrade path, with a dedicated webhook controller handling payment, renewal and cancellation events idempotently.
Symfony Scheduler and Messenger drive background jobs — dispatching due reminders and running strain sync on a cadence.
Optional before/after wellbeing capture, surfacing patterns between consumption and how users feel.
Aggregation of entries into usage statistics powering the app's charts and insight surfaces.
Security & Cross-Cutting Concerns
- Authentication — stateless JWT via LexikJWTAuthenticationBundle, with a custom user provider and validators enforcing identity and input integrity.
- Rate limiting — Symfony Rate Limiter protects auth and write endpoints.
- CORS — Nelmio CORS for controlled cross-origin access from the app.
- Email & documents — Symfony Mailer for transactional mail; KnpSnappy available for PDF generation.
Engineering Standards
Quality gates are first-class across both codebases:
- API — PHP 8.2+ on Symfony 7.4; a Pest 4 suite run in parallel with coverage; PHPStan static analysis, Rector refactoring and PHP_CodeSniffer (PSR-12) bundled into a single lint gate; Dockerised with a scripted deploy.
- App — TypeScript throughout with ESLint, and strict separation of concerns (API layer, stores, hooks, DB and UI cleanly isolated) that keeps the offline logic testable and the screens thin.
Technology Stack
| Mobile | React Native 0.85, Expo SDK 56, React 19, expo-router, TypeScript |
| App state | TanStack Query (server state), Zustand (client state) |
| Offline | expo-sqlite, custom sync queue, NetInfo |
| Device | expo-secure-store (tokens), expo-notifications, EAS builds |
| App UI | Fraunces + Inter, react-native-gifted-charts, reanimated/worklets, expo-glass-effect |
| API framework | Symfony 7.4, PHP 8.2+ |
| Data | MongoDB (Doctrine MongoDB ODM) |
| Auth | JWT (Lexik), custom UserProvider, rate limiting |
| Async / scheduled | Symfony Messenger, Symfony Scheduler |
| Billing | Stripe (checkout + webhooks) |
| Integrations | Weedmaps strain API (cached), Symfony Mailer, KnpSnappy (PDF) |
| Infrastructure | Docker, Docker Compose, scripted deploy |
| Quality | Pest (parallel), PHPStan, Rector, PHP_CodeSniffer (PSR-12), ESLint |