{"id":32544062,"url":"https://github.com/mehotkhan/safarnak.app","last_synced_at":"2026-04-14T05:33:41.758Z","repository":{"id":320747970,"uuid":"1081953419","full_name":"mehotkhan/safarnak.app","owner":"mehotkhan","description":"Safarnak - AI-powered offline-first travel companion with multi-storage architecture. Built with Expo React Native, Cloudflare Workers (D1, KV, Vectorize, R2, Durable Objects), Drizzle ORM, and GraphQL. Features bilingual support (EN/FA), NativeWind styling, and type-safe end-to-end architecture.","archived":false,"fork":false,"pushed_at":"2025-12-18T09:41:02.000Z","size":36758,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-12-20T08:29:22.866Z","etag":null,"topics":["ai-powered","cloudflare-d1","cloudflare-workers","drizzle-orm","durable-objects","expo","graphql","i18n","mobile-app","nativewind","offline-first","react-native","real-time","serverless","travel-app","typescript","vector-database"],"latest_commit_sha":null,"homepage":"https://safarnak.app","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mehotkhan.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":["mehotkhan"],"patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"custom":["https://paypal.me/mehotkhan","https://buymeacoffee.com/mehotkhan"]}},"created_at":"2025-10-23T14:22:49.000Z","updated_at":"2025-12-18T09:41:06.000Z","dependencies_parsed_at":"2025-11-01T04:01:07.759Z","dependency_job_id":null,"html_url":"https://github.com/mehotkhan/safarnak.app","commit_stats":null,"previous_names":["mehotkhan/safarnak.app"],"tags_count":88,"template":false,"template_full_name":null,"purl":"pkg:github/mehotkhan/safarnak.app","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mehotkhan%2Fsafarnak.app","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mehotkhan%2Fsafarnak.app/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mehotkhan%2Fsafarnak.app/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mehotkhan%2Fsafarnak.app/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mehotkhan","download_url":"https://codeload.github.com/mehotkhan/safarnak.app/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mehotkhan%2Fsafarnak.app/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31784253,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-14T02:24:21.117Z","status":"ssl_error","status_checked_at":"2026-04-14T02:24:20.627Z","response_time":153,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai-powered","cloudflare-d1","cloudflare-workers","drizzle-orm","durable-objects","expo","graphql","i18n","mobile-app","nativewind","offline-first","react-native","real-time","serverless","travel-app","typescript","vector-database"],"created_at":"2025-10-28T17:22:37.669Z","updated_at":"2026-04-14T05:33:41.751Z","avatar_url":"https://github.com/mehotkhan.png","language":"TypeScript","funding_links":["https://github.com/sponsors/mehotkhan","https://paypal.me/mehotkhan","https://buymeacoffee.com/mehotkhan"],"categories":[],"sub_categories":[],"readme":"# 🌍 Safarnak\n\n\u003e **سفرناک** - A modern offline-first travel companion built with Expo React Native and Cloudflare Workers\n\n[![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue)](https://www.typescriptlang.org/)\n[![React Native](https://img.shields.io/badge/React%20Native-0.81.5-green)](https://reactnative.dev/)\n[![Expo](https://img.shields.io/badge/Expo-~54-purple)](https://expo.dev/)\n[![Cloudflare Workers](https://img.shields.io/badge/Cloudflare-Workers-orange)](https://workers.cloudflare.com/)\n[![GraphQL Codegen](https://img.shields.io/badge/GraphQL-Codegen-purple)](https://the-guild.dev/graphql/codegen)\n[![New Architecture](https://img.shields.io/badge/New%20Architecture-Enabled-green)](https://reactnative.dev/blog/2024/10/23/the-new-architecture-is-here)\n[![License](https://img.shields.io/badge/License-MIT-yellow)](LICENSE)\n[![Version](https://img.shields.io/badge/Version-1.13.0-blue)](https://github.com/mehotkhan/safarnak.app/releases)\n[![CI/CD](https://img.shields.io/badge/CI%2FCD-Passing-green)](https://github.com/mehotkhan/safarnak.app/actions)\n\n**Live Demo**: [safarnak.app](https://safarnak.app) | **Download APK**: [Latest Release](https://github.com/mehotkhan/safarnak.app/releases)\n\n---\n\n## Table of Contents\n- [What is This?](#-what-is-this)\n- [Architecture Overview](#-architecture-overview)\n- [Quick Start](#-quick-start)\n- [Codebase Structure](#-codebase-structure)\n- [Routing \u0026 URLs](#-routing--urls)\n- [Database Model](#-database-model-er-diagram)\n- [How to Add New Features](#-how-to-add-new-features)\n- [Configuration](#-configuration)\n- [Common Commands](#-common-commands)\n- [Technology Stack](#-technology-stack)\n- [Development Tips](#-development-tips)\n- [Authentication Flow](#-authentication-flow)\n- [Internationalization](#-internationalization)\n- [Key Concepts](#-key-concepts)\n- [Offline-First Architecture](#-offline-first-architecture)\n- [Technical Review \u0026 Checklist (Summary)](#-technical-review--checklist-summary)\n- [Contributing](#-contributing)\n- [Code of Conduct](#-code-of-conduct)\n- [Suggested Improvements](#-suggested-improvements)\n- [License](#-license)\n- [Resources](#-resources)\n\n## 📚 What is This?\n\n**Safarnak** (سفرناک) is a full‑stack, offline‑first travel companion. It helps users discover destinations, plan trips, and share experiences. The project is a single‑root monorepo with a clean separation of concerns and shared types across client and server.\n\n### Highlights\n\n- **Offline‑first by design**: Automatic Apollo → Drizzle sync via `DrizzleCacheStorage` on every cache write. Works seamlessly offline with SQL queries over cached data.\n- **Shared GraphQL + Codegen**: One schema in `graphql/` powers both the Worker and the client. Codegen produces strongly‑typed hooks in `api/`.\n- **Unified Drizzle schema**: A single `database/schema.ts` defines both server tables (Cloudflare D1) and client cached tables (Expo SQLite), all with UUID IDs. Separate adapters (`server.ts`, `client.ts`) consume the same schema.\n- **Edge backend**: Cloudflare Workers with GraphQL Yoga, Cloudflare D1 (SQLite), KV, R2, and Durable Objects for real‑time subscriptions.\n- **Modern RN stack**: React 19, Expo Router 6, NativeWind 4 (Tailwind), New Architecture enabled.\n- **Great DX**: Path aliases, one‑command dev, `yarn codegen`, `yarn db:migrate`, friendly linting.\n\n### Onboarding roadmap\n\n- Start with [Quick Start](#-quick-start) (install, migrate DB, codegen, run dev)\n- Skim [Architecture Overview](#-architecture-overview) (keep charts; refer often)\n- Learn the [Schema → Codegen → Hooks](#-how-to-add-new-features) workflow\n- Review [Offline‑First Architecture](#-offline-first-architecture) and local DB usage\n\n---\n\n## 🏗️ Architecture Overview\n\n### System Architecture (High-Level)\n\n```mermaid\nflowchart TB\n  subgraph Client[\"📱 Client Layer (React Native + Expo)\"]\n    subgraph UI[\"UI Layer\"]\n      A[\"app/ - Expo Router Pages\"]\n      B[\"ui/ - UI Components, Hooks, State, Utils\"]\n    end\n    subgraph State[\"State Management\"]\n      C[\"ui/state/ - Redux + Persist\"]\n      D[\"api/ - Apollo Client + DrizzleCacheStorage\"]\n    end\n    subgraph Local[\"Local Storage\"]\n      E[\"Apollo Cache\u003cbr/\u003e(raw)\"]\n      F[\"Drizzle Cache\u003cbr/\u003e(structured)\"]\n      G[\"AsyncStorage\u003cbr/\u003e(Mutation Queue)\"]\n    end\n  end\n\n  subgraph Shared[\"🔗 Shared Layer\"]\n    H[\"graphql/ - Schema + Operations\"]\n    I[\"database/schema.ts - Shared Drizzle Schema\u003cbr/\u003e(Server + Client Tables)\"]\n  end\n\n  subgraph Worker[\"⚡ Server Layer (Cloudflare Workers)\"]\n    J[\"worker/ - GraphQL Resolvers\"]\n    K[\"GraphQL Yoga Server\"]\n    L[\"D1 Database\u003cbr/\u003e(SQLite)\"]\n    M[\"KV Store\u003cbr/\u003e(Sessions/Cache)\"]\n    N[\"Vectorize\u003cbr/\u003e(Embeddings)\"]\n    O[\"R2 Storage\u003cbr/\u003e(Media Files)\"]\n  end\n\n  A --\u003e C\n  B --\u003e C\n  C --\u003e D\n  D --\u003e E\n  D --\u003e F\n  D --\u003e G\n  D \u003c--\u003e|HTTPS GraphQL\u003cbr/\u003eAuth Headers| K\n  K --\u003e J\n  J --\u003e L\n  J --\u003e M\n  J --\u003e N\n  J --\u003e O\n  H --\u003e D\n  H --\u003e K\n  I --\u003e F\n  I --\u003e L\n  I --\u003e J\n```\n\n### Client Architecture (Detailed)\n\n```mermaid\nflowchart TB\n  subgraph Component[\"React Components\"]\n    C1[\"app/*.tsx\u003cbr/\u003ePages\"]\n    C2[\"ui/*.tsx\u003cbr/\u003eUI Components\"]\n  end\n\n  subgraph Hooks[\"Hook Layer\"]\n    H1[\"DrizzleCacheStorage\u003cbr/\u003eapi/cache-storage.ts\"]\n    H2[\"Custom Hooks\u003cbr/\u003eui/hooks/*.ts\"]\n    H3[\"Redux Hooks\u003cbr/\u003eui/state/hooks.ts\"]\n  end\n\n  subgraph Data[\"Data Layer\"]\n    D1[\"Apollo Client\u003cbr/\u003eapi/client.ts\"]\n    D2[\"Redux Store\u003cbr/\u003eui/state/index.ts\"]\n      D3[\"Local DB\u003cbr/\u003edatabase/client.ts\"]\n  end\n\n  subgraph Storage[\"Storage Layer\"]\n    S1[\"Apollo Cache (raw)\u003cbr/\u003esafarnak_local.db: apollo_cache_entries\"]\n    S2[\"Drizzle Cache\u003cbr/\u003e(structured)\"]\n    S3[\"AsyncStorage\u003cbr/\u003eMutation Queue\"]\n    S4[\"Redux Persist\u003cbr/\u003eAsyncStorage\"]\n  end\n\n  C1 --\u003e H1\n  C2 --\u003e H1\n  C1 --\u003e H3\n  H1 --\u003e D1\n  H2 --\u003e D3\n  H3 --\u003e D2\n  D1 --\u003e S1\n  D1 --\u003e S2\n  D2 --\u003e S4\n  D3 --\u003e S2\n  D1 --\u003e S3\n```\n\n### Data Flow Architecture\n\n```mermaid\nflowchart LR\n  subgraph Query[\"Query Flow\"]\n    Q1[\"Component\"] --\u003e Q2[\"Generated Hook\"]\n    Q2 --\u003e Q3[\"Apollo Client\"]\n    Q3 --\u003e Q4[\"GraphQL Server\"]\n    Q4 --\u003e Q5[\"D1 Database\"]\n    Q5 --\u003e Q4\n    Q4 --\u003e Q3\n    Q3 --\u003e Q6[\"Apollo Cache (raw)\"]\n    Q6 --\u003e Q7[\"Auto Sync\"]\n    Q7 --\u003e Q8[\"Drizzle Cache (structured)\"]\n    Q2 --\u003e Q1\n  end\n\n  subgraph Mutation[\"Mutation Flow\"]\n    M1[\"Component\"] --\u003e M2[\"Generated Hook\"]\n    M2 --\u003e M3{\"Online?\"}\n    M3 --\u003e|Yes| M4[\"Apollo Client\"]\n    M3 --\u003e|No| M5[\"Queue in\u003cbr/\u003eAsyncStorage\"]\n    M4 --\u003e M6[\"GraphQL Server\"]\n    M6 --\u003e M7[\"D1 Database\"]\n    M7 --\u003e M6\n    M6 --\u003e M4\n    M4 --\u003e M8[\"Apollo Cache\"]\n    M8 --\u003e M9[\"Auto Sync\"]\n    M9 --\u003e M10[\"Drizzle Cache\"]\n    M5 --\u003e M11[\"Process Queue\u003cbr/\u003eon Reconnect\"]\n    M11 --\u003e M4\n  end\n```\n\n### Offline-First Sync Architecture\n\n```mermaid\nsequenceDiagram\n  participant UI as UI Component\n  participant AH as Generated Hook\n  participant AC as Apollo Client\n  participant API as GraphQL API\n  participant ACache as Apollo Cache\u003cbr/\u003e(SQLite)\n  participant DCache as Drizzle Cache\u003cbr/\u003e(SQLite)\n  participant Queue as Mutation Queue\u003cbr/\u003e(AsyncStorage)\n\n  Note over UI,Queue: Online Scenario\n  UI-\u003e\u003eAH: useGetTripsQuery()\n  AH-\u003e\u003eAC: Query with cache-and-network\n  AC-\u003e\u003eAPI: GraphQL Request\n  API--\u003e\u003eAC: Response Data\n  AC-\u003e\u003eACache: Persist to SQLite\n  AC--\u003e\u003eAH: onCompleted callback\n    AC-\u003e\u003eDCache: DrizzleCacheStorage.setItem() (automatic)\n  DCache--\u003e\u003eAH: Sync complete\n  AH--\u003e\u003eUI: Return data\n\n  Note over UI,Queue: Offline Mutation\n  UI-\u003e\u003eAH: useCreateTripMutation()\n  AH-\u003e\u003eAC: Mutation request\n  AC-\u003e\u003eAPI: GraphQL Request\n  API--\u003e\u003eAC: Network Error\n  AC-\u003e\u003eQueue: Queue mutation\n  AC--\u003e\u003eAH: Error (handled)\n  AH-\u003e\u003eDCache: Optimistic update\n  DCache--\u003e\u003eAH: Update complete\n  AH--\u003e\u003eUI: Optimistic UI update\n\n  Note over UI,Queue: Online Sync\n  AC-\u003e\u003eQueue: Check pending mutations\n  Queue--\u003e\u003eAC: Return queued mutations\n  AC-\u003e\u003eAPI: Process queued mutations\n  API--\u003e\u003eAC: Success\n  AC-\u003e\u003eQueue: Remove from queue\n  AC-\u003e\u003eDCache: Mark as synced\n```\n\n### Storage Layer Architecture\n\n```mermaid\nflowchart TB\n  subgraph ClientStorage[\"Client Storage\"]\n    subgraph Apollo[\"Apollo Cache\"]\n      A1[\"safarnak_local.db\u003cbr/\u003e(apollo_cache_entries table)\"]\n      A2[\"Normalized Cache\u003cbr/\u003eKey-Value Pairs\"]\n    end\n    subgraph Drizzle[\"Drizzle Cache\"]\n      D1[\"safarnak_local.db\u003cbr/\u003e(SQLite)\"]\n      D2[\"cachedTrips\"]\n      D3[\"cachedUsers\"]\n      D4[\"cachedTours\"]\n      D5[\"syncMetadata\"]\n      D6[\"pendingMutations\"]\n    end\n    subgraph Async[\"AsyncStorage\"]\n      AS1[\"@safarnak_user\u003cbr/\u003e(Auth Data)\"]\n      AS2[\"Mutation Queue\u003cbr/\u003e(Offline)\"]\n      AS3[\"Redux Persist\u003cbr/\u003e(UI State)\"]\n    end\n  end\n\n  subgraph ServerStorage[\"Server Storage\"]\n    subgraph D1DB[\"Cloudflare D1\"]\n      S1[\"users\"]\n      S2[\"trips\"]\n      S3[\"tours\"]\n      S4[\"messages\"]\n    end\n    subgraph KV[\"Cloudflare KV\"]\n      K1[\"token:xxx\u003cbr/\u003e(Sessions)\"]\n      K2[\"cache:xxx\u003cbr/\u003e(API Cache)\"]\n    end\n    subgraph Vector[\"Vectorize\"]\n      V1[\"User Preferences\u003cbr/\u003eEmbeddings\"]\n      V2[\"Locations\u003cbr/\u003eEmbeddings\"]\n    end\n    subgraph R2Storage[\"R2 Storage\"]\n      R2Avatars[\"avatars/\"]\n      R2Images[\"images/\"]\n      R2Attachments[\"attachments/\"]\n    end\n  end\n\n  A1 --\u003e A2\n  D1 --\u003e D2\n  D1 --\u003e D3\n  D1 --\u003e D4\n  D1 --\u003e D5\n  D1 --\u003e D6\n  A2 -.-\u003e|Auto Sync| D2\n  A2 -.-\u003e|Auto Sync| D3\n  A2 -.-\u003e|Auto Sync| D4\n  D2 -.-\u003e|Sync| S2\n  D3 -.-\u003e|Sync| S1\n  D4 -.-\u003e|Sync| S3\n```\n\n### Authentication Flow\n\n```mermaid\nsequenceDiagram\n  participant U as User\n  participant UI as Login Screen\n  participant AC as Apollo Client\n  participant AL as Auth Link\n  participant API as GraphQL API\n  participant KV as KV Store\n  participant DB as D1 Database\n  participant RS as Redux Store\n  participant AS as AsyncStorage\n\n  U-\u003e\u003eUI: Enter credentials\n  UI-\u003e\u003eAC: useLoginMutation()\n  AC-\u003e\u003eAL: Add Bearer token header\n  AL-\u003e\u003eAPI: POST /graphql (login)\n  API-\u003e\u003eDB: Verify user (PBKDF2)\n  DB--\u003e\u003eAPI: User record\n  API-\u003e\u003eAPI: Generate token (SHA-256)\n  API-\u003e\u003eKV: Store token:userId (30 days)\n  KV--\u003e\u003eAPI: Stored\n  API--\u003e\u003eAC: Return user + token\n  AC-\u003e\u003eRS: Dispatch login action\n  RS-\u003e\u003eAS: Persist user data\n  AC-\u003e\u003eAS: Store token\n  RS--\u003e\u003eUI: Update auth state\n  UI--\u003e\u003eU: Navigate to app\n\n  Note over AC,API: Subsequent Requests\n  UI-\u003e\u003eAC: useGetTripsQuery()\n  AC-\u003e\u003eAL: Get token from AsyncStorage\n  AL-\u003e\u003eAPI: POST /graphql (Authorization: Bearer token)\n  API-\u003e\u003eKV: Verify token:userId\n  KV--\u003e\u003eAPI: userId\n  API-\u003e\u003eDB: Query trips for userId\n  DB--\u003e\u003eAPI: Trips data\n  API--\u003e\u003eAC: Return trips\n```\n\n### Error Handling Architecture\n\n```mermaid\nflowchart TB\n  subgraph Network[\"Network Errors\"]\n    N1[\"Apollo Request\"] --\u003e N2{\"Network\u003cbr/\u003eAvailable?\"}\n    N2 --\u003e|No| N3[\"Catch Network Error\"]\n    N2 --\u003e|Yes| N4{\"Backend\u003cbr/\u003eReachable?\"}\n    N4 --\u003e|No| N3\n    N4 --\u003e|Yes| N5[\"Process Request\"]\n    N3 --\u003e N6[\"Error Link\"]\n    N6 --\u003e N7[\"Suppress Network Errors\u003cbr/\u003e(Expected when offline)\"]\n    N7 --\u003e N8[\"Use Cached Data\u003cbr/\u003e(errorPolicy: all)\"]\n  end\n\n  subgraph GraphQL[\"GraphQL Errors\"]\n    G1[\"GraphQL Response\"] --\u003e G2{\"Has\u003cbr/\u003eErrors?\"}\n    G2 --\u003e|Yes| G3[\"Error Link\"]\n    G3 --\u003e G4[\"Log in Dev Mode\"]\n    G4 --\u003e G5[\"Return Partial Data\u003cbr/\u003e(if available)\"]\n    G2 --\u003e|No| G6[\"Process Successfully\"]\n  end\n\n  subgraph Component[\"Component Error Handling\"]\n    C1[\"Generated Hook\"] --\u003e C2{\"Error\u003cbr/\u003ePresent?\"}\n    C2 --\u003e|Yes| C3[\"onError Callback\"]\n    C3 --\u003e C4[\"Display Error UI\"]\n    C4 --\u003e C5[\"Show Retry Button\"]\n    C2 --\u003e|No| C6[\"Render Data\"]\n  end\n\n  N8 --\u003e C1\n  G5 --\u003e C1\n  G6 --\u003e C1\n```\n\n### Network Status \u0026 Connectivity\n\n```mermaid\nflowchart TB\n  subgraph Detection[\"Status Detection\"]\n    D1[\"NetInfo\u003cbr/\u003eListener\"] --\u003e D2[\"Network State\"]\n    D3[\"Backend Probe\u003cbr/\u003echeckBackendReachable()\"] --\u003e D4[\"Backend State\"]\n    D2 --\u003e D5[\"useSystemStatus Hook\"]\n    D4 --\u003e D5\n  end\n\n  subgraph UI[\"UI Updates\"]\n    D5 --\u003e U1[\"Update isOnline\"]\n    D5 --\u003e U2[\"Update isBackendReachable\"]\n    U1 --\u003e U3[\"Offline Icon\u003cbr/\u003e(Home Page)\"]\n    U2 --\u003e U3\n    U3 --\u003e U4[\"System Status Page\"]\n  end\n\n  subgraph Actions[\"Offline Actions\"]\n    U1 --\u003e A1{\"isOnline = false?\"}\n    A1 --\u003e|Yes| A2[\"Disable Mutations\"]\n    A1 --\u003e|Yes| A3[\"Queue Mutations\"]\n    A2 --\u003e A4[\"Show Offline Indicator\"]\n    A3 --\u003e A4\n    A1 --\u003e|No| A5{\"isBackendReachable = false?\"}\n    A5 --\u003e|Yes| A2\n    A5 --\u003e|Yes| A3\n    A5 --\u003e|No| A6[\"Normal Operation\"]\n  end\n\n  subgraph Sync[\"Auto Sync on Reconnect\"]\n    A2 --\u003e S1[\"Monitor Network\"]\n    S1 --\u003e S2{\"Connection\u003cbr/\u003eRestored?\"}\n    S2 --\u003e|Yes| S3[\"processQueue()\"]\n    S3 --\u003e S4[\"Retry Queued Mutations\"]\n    S4 --\u003e S5[\"Update UI\"]\n  end\n```\n\n### Dev-time GraphQL Codegen Pipeline\n\n```mermaid\nflowchart TB\n  subgraph Input[\"Source Files\"]\n    I1[\"graphql/schema.graphql\u003cbr/\u003eType Definitions\"]\n    I2[\"graphql/queries/*.graphql\u003cbr/\u003eOperations\"]\n  end\n\n  subgraph Codegen[\"Code Generation\"]\n    C1[\"yarn codegen\"] --\u003e C2[\"GraphQL Codegen\"]\n    C2 --\u003e C3[\"Parse Schema\"]\n    C2 --\u003e C4[\"Parse Operations\"]\n    C3 --\u003e C5[\"Generate Types\"]\n    C4 --\u003e C6[\"Generate Hooks\"]\n  end\n\n  subgraph Output[\"Generated Files\"]\n    C5 --\u003e O1[\"api/types.ts\u003cbr/\u003eTypeScript Types\"]\n    C6 --\u003e O2[\"api/hooks.ts\u003cbr/\u003eReact Apollo Hooks\"]\n  end\n\n  subgraph Enhancement[\"Hook Enhancement\"]\n    O2 --\u003e E1[\"api/cache-storage.ts\"]\n    E1 --\u003e E2[\"DrizzleCacheStorage\"]\n    E2 --\u003e E3[\"Automatic Sync on Write\"]\n    E3 --\u003e E4[\"Dual-Write: Raw + Structured\"]\n  end\n\n  subgraph Usage[\"App Usage\"]\n    E4 --\u003e U1[\"Export from @api\"]\n    U1 --\u003e U2[\"Import in Components\"]\n    U2 --\u003e U3[\"Use Generated Hooks\"]\n  end\n\n  I1 --\u003e C1\n  I2 --\u003e C1\n  U3 --\u003e U4[\"Automatic Offline Support\"]\n```\n\n### Complete Request Lifecycle\n\n```mermaid\nsequenceDiagram\n  participant C as Component\n  participant AH as Generated Hook\n  participant AC as Apollo Client\n  participant EL as Error Link\n  participant AL as Auth Link\n  participant API as GraphQL API\n  participant DB as D1 Database\n  participant Cache as Apollo Cache\n  participant Drizzle as Drizzle Cache\n\n  C-\u003e\u003eAH: useGetTripsQuery()\n  AH-\u003e\u003eAC: Query with cache-and-network\n  AC-\u003e\u003eCache: Check cache first\n  Cache--\u003e\u003eAC: Return cached data (if available)\n  AC--\u003e\u003eC: Render with cached data (optimistic)\n  AC-\u003e\u003eAL: Add auth headers\n  AL-\u003e\u003eAPI: POST /graphql\n  API--\u003e\u003eAC: Response or Error\n  alt Success\n    AC-\u003e\u003eCache: Update cache\n    Cache-\u003e\u003eAH: onCompleted callback\n    AC-\u003e\u003eDrizzle: DrizzleCacheStorage.setItem() (automatic)\n    Drizzle--\u003e\u003eAH: Sync complete\n    AH-\u003e\u003eAC: Update with network data\n    AC--\u003e\u003eC: Re-render with fresh data\n  else Network Error\n    AC-\u003e\u003eEL: Network error detected\n    EL-\u003e\u003eEL: Suppress error (offline expected)\n    EL--\u003e\u003eAC: Continue with cached data\n    AC--\u003e\u003eC: Use cached data (errorPolicy: all)\n  else GraphQL Error\n    AC-\u003e\u003eEL: GraphQL error detected\n    EL-\u003e\u003eEL: Log error (dev mode)\n    EL--\u003e\u003eAC: Return partial data\n    AC--\u003e\u003eC: Render with partial data\n  end\n```\n\n### How It Works\n\n1. **Define GraphQL Schema** (`graphql/schema.graphql`) - Shared between client and worker\n2. **Define Operations** (`graphql/queries/*.graphql`) - Queries and mutations\n3. **Run Codegen** - Auto-generates TypeScript types and React hooks in `api/`\n4. **DrizzleCacheStorage** (`api/cache-storage.ts`) - Automatically syncs Apollo cache to Drizzle on every cache write\n5. **Implement Resolvers** (`worker/queries/`, `worker/mutations/`) - Server-side logic using `getServerDB()` from `@database/server`\n6. **Use in App** (`app/`, `ui/`) - Import hooks from `@api` (automatic sync via DrizzleCacheStorage)\n\n---\n\n## 🚀 Quick Start\n\n### Prerequisites\n\n- **Node.js 20+** (check with `node --version`)\n- **Yarn** package manager (install via `npm install -g yarn`)\n- **Android Studio** (for Android development)\n- **Git** (for cloning the repository)\n\n### Setup (5-10 minutes)\n\n```bash\n# 1. Clone the repository\ngit clone https://github.com/mehotkhan/safarnak.app.git\ncd safarnak.app\n\n# 2. Install dependencies\nyarn install\n\n# 3. Setup local database (Cloudflare D1)\nyarn db:migrate\n\n# 4. Generate GraphQL types and hooks\nyarn codegen\n\n# 5. Start development servers\nyarn dev  # Runs both worker (port 8787) and Expo client (port 8081)\n```\n\nThis will start:\n- **Cloudflare Worker** on `http://localhost:8787` (GraphQL API)\n- **Expo Dev Server** on `http://localhost:8081` (React Native app)\n\n### Run on Device/Emulator\n\n```bash\n# Android (New Architecture - recommended)\nyarn android:newarch\n\n# Android (Legacy Architecture)\nyarn android\n\n# Web browser\nyarn web\n\n# iOS (macOS only, not actively tested)\nyarn ios\n```\n\n### First Time Setup Tips\n\n- **Worker URL**: If you see connection errors, check that the worker is running on port 8787\n- **GraphQL Playground**: Visit `http://localhost:8787/graphql` to test GraphQL queries\n- **Metro Bundler**: If you see cache issues, run `yarn clean` and restart\n- **Database**: The local D1 database is stored in `.wrangler/state/v3/d1/`\n\n### Verify Installation\n\n1. Check worker is running: Visit `http://localhost:8787/graphql` - you should see GraphQL Playground\n2. Check Expo: Open Expo Go app on your phone or press `w` for web\n3. Try a query: In GraphQL Playground, run `{ me { id username } }` (after logging in)\n\n---\n\n## 📁 Codebase Structure\n\n### Client-Side (React Native - What You'll Modify Most)\n\n```\napp/                          # 📱 Expo Router pages (file-based routing)\n├── _layout.tsx              # Root layout with providers\n├── (auth)/                  # Auth route group (public routes)\n│   ├── _layout.tsx         # Auth stack layout\n│   ├── welcome.tsx         # /auth/welcome\n│   ├── login.tsx           # /auth/login\n│   └── register.tsx        # /auth/register\n└── (app)/                   # Main app group (protected routes)\n    ├── _layout.tsx         # Tab bar layout (4 tabs: feed, explore, trips, profile)\n    ├── (feed)/             # Feed tab\n    │   ├── index.tsx       # / (home feed)\n    │   ├── [id].tsx        # /:id (post detail)\n    │   └── new.tsx         # /new (create post)\n    ├── (explore)/          # Explore tab\n    │   ├── index.tsx       # /explore\n    │   ├── places/[id].tsx # /explore/places/:id\n    │   ├── tours/[id].tsx  # /explore/tours/:id\n    │   ├── tours/[id]/book.tsx # /explore/tours/:id/book\n    │   ├── locations/[id].tsx  # /explore/locations/:id\n    │   └── users/[id].tsx  # /explore/users/:id\n    ├── (trips)/            # Trips tab\n    │   ├── index.tsx       # /trips (trip list)\n    │   ├── new.tsx         # /trips/new (create trip)\n    │   └── [id]/           # /trips/:id\n    │       ├── index.tsx   # Trip details\n    │       └── edit.tsx    # Edit trip\n    └── (profile)/          # Profile tab\n        ├── index.tsx       # /profile\n        ├── edit.tsx        # /profile/edit\n        ├── trips.tsx       # /profile/trips\n        ├── messages.tsx    # /profile/messages\n        ├── messages/[id].tsx # /profile/messages/:id\n        ├── notifications/[id].tsx # /profile/notifications/:id\n        ├── payments.tsx    # /profile/payments\n        ├── subscription.tsx # /profile/subscription\n        └── settings.tsx    # /profile/settings\n\nui/                           # 🎨 All client UI code\n├── auth/                    # Authentication components\n│   └── AuthWrapper.tsx      # Authentication guard\n├── maps/                    # Map components\n│   ├── MapView.tsx          # MapLibre GL map (native, replaces Leaflet)\n│   ├── MapLibreView.tsx     # MapLibre GL map component\n│   └── MapLibreLayerSelector.tsx  # Map layer selector UI\n├── forms/                   # Form components\n│   ├── CustomButton.tsx\n│   ├── InputField.tsx\n│   ├── TextArea.tsx\n│   └── ...\n├── display/                 # Display components\n│   ├── CustomText.tsx\n│   ├── UserAvatar.tsx\n│   └── ...\n├── feedback/                # Loading/error states\n│   ├── LoadingState.tsx\n│   ├── ErrorState.tsx\n│   └── ...\n├── context/                 # React contexts\n│   ├── LanguageContext.tsx\n│   ├── LanguageSwitcher.tsx\n│   └── ThemeContext.tsx\n├── hooks/                   # 🪝 Custom React hooks\n│   ├── useColorScheme.ts\n│   ├── useAuth.ts\n│   └── ...\n├── state/                   # 📦 Redux Toolkit state management\n│   ├── index.ts             # Store configuration\n│   ├── hooks.ts             # Typed hooks (useAppDispatch, useAppSelector)\n│   ├── slices/              # Redux slices\n│   │   ├── authSlice.ts\n│   │   └── themeSlice.ts\n│   └── middleware/          # Redux middleware\n│       └── offlineMiddleware.ts\n└── utils/                   # Client utilities\n    ├── clipboard.ts\n    ├── validation.ts\n    └── ...\n\napi/                          # 🌐 GraphQL client layer\n├── hooks.ts                 # ✨ Auto-generated React Apollo hooks (never edit manually)\n├── types.ts                 # ✨ Auto-generated TypeScript types (never edit manually)\n├── cache-storage.ts         # DrizzleCacheStorage - automatic Apollo → Drizzle sync\n├── client.ts                # Apollo Client setup\n├── utils.ts                 # API utilities\n├── globals.d.ts             # TypeScript global declarations\n└── index.ts                 # Main exports\n\nconstants/                    # 📋 App constants\n├── app.ts                   # App-wide constants\n├── Colors.ts                # Color palette (light/dark themes)\n└── index.ts                 # Exports\n\nlocales/                      # 🌍 i18n translation files\n├── en/translation.json      # English translations\n└── fa/translation.json      # Persian (Farsi) translations\n\nglobal.css                    # 🎨 Tailwind CSS directives (@tailwind base/components/utilities)\ntailwind.config.js           # 🎨 Tailwind configuration (NativeWind v4)\nbabel.config.js              # ⚙️ Babel config (NativeWind preset)\nmetro.config.js              # 📦 Metro bundler config (path aliases, NativeWind)\n```\n\n### Server-Side\n\n```\nworker/                 # ⚡ Cloudflare Worker\n├── queries/           # Query resolvers (myConversations, me)\n├── mutations/        # Mutation resolvers (register, login)\n└── subscriptions/    # Subscription resolvers (conversationMessages, tripUpdates)\n\ngraphql/               # 📡 Shared GraphQL\n├── schema.graphql    # GraphQL schema (shared)\n└── queries/          # Query definitions (.graphql files)\n\ndatabase/              # 🗄️ Shared database schema and adapters\n├── schema.ts         # Unified schema with UUIDs (server + client tables in one file)\n├── server.ts         # Server adapter (Cloudflare D1) - exports getServerDB()\n├── client.ts         # Client adapter (Expo SQLite) - exports getLocalDB(), sync utilities\n├── index.ts          # Main exports (re-exports from schema, server, client)\n├── types.ts          # Database types\n└── utils.ts          # UUID utilities (createId, isValidId)\nmigrations/           # Server-only migrations (Cloudflare D1, at project root)\n```\n\n## 🧭 Routing \u0026 URLs\n\nSafarnak uses **Expo Router** with file-based routing. Routes are organized into groups using parentheses (which don't appear in URLs).\n\n### Auth Routes (Public)\n- `/auth/welcome` – Onboarding/Welcome screen\n- `/auth/login` – User login\n- `/auth/register` – User registration\n\n### App Routes (Protected - Requires Authentication)\n\n#### Feed Tab (`(feed)`)\n- `/` – Home feed (social posts from community)\n- `/:id` – Post detail view with comments\n- `/new` – Create new post\n\n#### Explore Tab (`(explore)`)\n- `/explore` – Main explore/search page\n- `/explore/places/:id` – Place details page\n- `/explore/tours/:id` – Tour details page\n- `/explore/tours/:id/book` – Tour booking page\n- `/explore/locations/:id` – Location details page\n- `/explore/users/:id` – User profile (public view)\n\n#### Trips Tab (`(trips)`)\n- `/trips` – User's trip list\n- `/trips/new` – Create new trip (AI-powered)\n- `/trips/:id` – Trip details view\n- `/trips/:id/edit` – Edit trip\n\n#### Profile Tab (`(profile)`)\n- `/profile` – User profile home\n- `/profile/edit` – Edit profile\n- `/profile/trips` – User's trips list\n- `/profile/messages` – Messages inbox\n- `/profile/messages/:id` – Individual message/conversation\n- `/profile/notifications` – Notifications list\n- `/profile/notifications/:id` – Notification detail\n- `/profile/payments` – Payment history\n- `/profile/subscription` – Subscription management\n- `/profile/settings` – App settings\n\n### Route Organization\n- Route groups `(auth)` and `(app)` don't appear in URLs\n- Tab groups `(feed)`, `(explore)`, `(trips)`, `(profile)` don't appear in URLs\n- Dynamic routes use `[id]` in file names\n- Nested routes create URL paths (e.g., `trips/[id]/edit.tsx` → `/trips/:id/edit`)\n\n## 🗄️ Database Model (ER Diagram)\n\n```mermaid\nerDiagram\n    USERS ||--o{ MESSAGES : sends\n    USERS ||--o{ USER_SUBSCRIPTIONS : has\n    USERS ||--o{ SUBSCRIPTIONS : has \"GraphQL subscriptions\"\n    USERS ||--o{ USER_PREFERENCES : has\n    USERS ||--o{ TRIPS : creates\n    USERS ||--o{ TOURS : creates\n    USERS ||--o{ POSTS : authors\n    USERS ||--o{ COMMENTS : writes\n    USERS ||--o{ REACTIONS : adds\n    USERS ||--o{ PAYMENTS : makes\n    USERS ||--o{ DEVICES : owns\n    USERS ||--o{ SESSIONS : has\n    USERS ||--o{ NOTIFICATIONS : receives\n\n    TRIPS ||--o{ ITINERARIES : has\n    TRIPS ||--o{ PLANS : has\n    TRIPS ||--o{ THOUGHTS : generated_by\n    TRIPS ||--o{ MESSAGES : discusses\n\n    TOURS ||--o{ PAYMENTS : requires\n\n    LOCATIONS ||--o{ PLACES : contains\n\n    POSTS ||--o{ COMMENTS : has\n    POSTS ||--o{ REACTIONS : has\n    POSTS ||--o| TRIPS : references \"via relatedId\"\n    POSTS ||--o| TOURS : references \"via relatedId\"\n    POSTS ||--o| PLANS : references \"via relatedId\"\n\n    CACHE }|..|{ EXTERNAL_API : stores\n\n    USER_SUBSCRIPTIONS ||--|{ USERS : for\n    USER_SUBSCRIPTIONS ||--o{ PAYMENTS : via\n\n    USERS {\n        uuid id PK \"UUID (text)\"\n        string name\n        string username UK\n        string passwordHash\n        string email UK\n        string phone\n        string avatar \"R2 URL\"\n        boolean isActive\n        timestamp createdAt\n        timestamp updatedAt\n    }\n\n    USER_PREFERENCES {\n        uuid id PK \"UUID (text)\"\n        uuid userId FK \"UUID (text)\"\n        json interests\n        json budgetRange\n        string travelStyle\n        json preferredDestinations\n        json dietaryRestrictions\n        string embedding \"Vectorize\"\n        timestamp updatedAt\n    }\n\n    TRIPS {\n        uuid id PK \"UUID (text)\"\n        uuid userId FK \"UUID (text)\"\n        string title\n        date startDate\n        date endDate\n        string destination\n        integer budget \"cents/stored units\"\n        string status\n        boolean aiGenerated\n        json metadata\n        timestamp createdAt\n        timestamp updatedAt\n    }\n\n    ITINERARIES {\n        uuid id PK \"UUID (text)\"\n        uuid tripId FK \"UUID (text)\"\n        integer day\n        json activities\n        json accommodations\n        json transport\n        string notes\n        integer costEstimate\n        timestamp createdAt\n        timestamp updatedAt\n    }\n\n    PLANS {\n        uuid id PK \"UUID (text)\"\n        uuid tripId FK \"UUID (text)\"\n        json mapData \"stops, directions\"\n        json details\n        string aiOutput\n        timestamp createdAt\n    }\n\n    TOURS {\n        uuid id PK \"UUID (text)\"\n        string title\n        text description\n        integer price \"cents\"\n        integer rating\n        string location\n        string category\n        boolean isActive\n        timestamp createdAt\n        timestamp updatedAt\n    }\n\n    MESSAGES {\n        uuid id PK \"UUID (text)\"\n        text content\n        uuid userId FK \"UUID (text)\"\n        string type\n        json metadata\n        boolean isRead\n        timestamp createdAt\n    }\n\n    POSTS {\n        uuid id PK \"UUID (text)\"\n        uuid userId FK \"UUID (text)\"\n        text content\n        json attachments \"R2 URLs\"\n        string type \"plan/trip/tour\"\n        uuid relatedId \"trip/tour/plan UUID\"\n        timestamp createdAt\n    }\n\n    COMMENTS {\n        uuid id PK \"UUID (text)\"\n        uuid postId FK \"UUID (text)\"\n        uuid userId FK \"UUID (text)\"\n        text content\n        timestamp createdAt\n    }\n\n    REACTIONS {\n        uuid id PK \"UUID (text)\"\n        uuid postId FK \"UUID (text)\"\n        uuid commentId FK \"UUID (text)\"\n        uuid userId FK \"UUID (text)\"\n        string emoji\n        timestamp createdAt\n    }\n\n    PAYMENTS {\n        uuid id PK \"UUID (text)\"\n        uuid userId FK \"UUID (text)\"\n        uuid tourId FK \"UUID (text)\"\n        uuid subscriptionId FK \"UUID (text)\"\n        string transactionId\n        integer amount\n        string currency \"default: IRR\"\n        string status\n        timestamp createdAt\n    }\n\n    USER_SUBSCRIPTIONS {\n        uuid id PK \"UUID (text)\"\n        uuid userId FK \"UUID (text)\"\n        string tier \"free/member/pro\"\n        date startDate\n        date endDate\n        boolean active\n        timestamp createdAt\n    }\n\n    DEVICES {\n        uuid id PK \"UUID (text)\"\n        uuid userId FK \"UUID (text)\"\n        string deviceId UK\n        string type\n        timestamp lastSeen\n    }\n\n    SESSIONS {\n        string id PK \"KV key (not in D1)\"\n        uuid userId FK \"UUID (text)\"\n        string token\n        timestamp expiresAt\n    }\n\n    NOTIFICATIONS {\n        uuid id PK \"UUID (text)\"\n        uuid userId FK \"UUID (text)\"\n        string type \"tour invite/payment/etc\"\n        json data\n        boolean read\n        timestamp createdAt\n    }\n\n    LOCATIONS {\n        uuid id PK \"UUID (text)\"\n        string name UK\n        string country\n        json coordinates\n        text description\n        json popularActivities\n        integer averageCost\n        string embedding \"Vectorize\"\n        string imageUrl \"R2\"\n        timestamp createdAt\n    }\n\n    PLACES {\n        uuid id PK \"UUID (text)\"\n        string name\n        uuid locationId FK \"UUID (text)\"\n        uuid ownerId FK \"UUID (text)\"\n        string type \"market/room/etc\"\n        text description\n        integer price\n        integer rating\n        json coordinates\n        string embedding \"Vectorize\"\n        string imageUrl \"R2\"\n        timestamp createdAt\n    }\n\n    THOUGHTS {\n        uuid id PK \"UUID (text)\"\n        uuid tripId FK \"UUID (text)\"\n        text step \"AI reasoning step\"\n        json data \"logs/sources\"\n        timestamp createdAt\n    }\n\n    SUBSCRIPTIONS {\n        uuid id PK \"UUID (text)\"\n        string connectionId \"GraphQL subscription\"\n        string connectionPoolId \"Durable Object\"\n        string subscription \"GraphQL query\"\n        string topic\n        string filter\n        uuid userId FK \"UUID (text)\"\n        boolean isActive\n        timestamp createdAt\n        timestamp expiresAt\n    }\n\n    CACHE {\n        string key PK \"KV key\"\n        json value \"cached API data\"\n        timestamp expiresAt\n    }\n```\n\n### Data Storage Architecture\n\n- **D1 (Relational DB with Drizzle)**: \n  - **Server Tables**: Users, user preferences, trips, itineraries, plans, tours, messages, posts, comments, reactions, payments, user subscriptions (tiers), devices, notifications, locations, places, thoughts, subscriptions (GraphQL subscriptions).\n  - **All IDs are UUID (text)** - consistent across server and client\n  - **Shared Schema**: Same schema definitions used by both server (D1) and client (expo-sqlite) adapters\n- **KV (Key-Value Store)**: Sessions (user tokens stored as `token:userId`), cache (external API data like TripAdvisor, web searches).\n- **Vectorize (Vector DB)**: Embeddings (user preferences, destinations, places, activities for similarity searches).\n- **R2 (Object Storage)**: Avatars, image URLs, galleries, attachments (media, maps, docs).\n- **Durable Objects**: Real-time subscriptions (connection state for GraphQL subs via `SubscriptionPool`).\n\n**Note**: The ER diagram above shows the server-side D1 database schema. The client uses cached versions of these tables (e.g., `cachedUsers`, `cachedTrips`) with additional sync metadata fields (`cachedAt`, `lastSyncAt`, `pending`) for offline-first functionality.\n\n### Shared (Critical)\n\n- **`graphql/`** - GraphQL schema and operations (shared between client \u0026 worker)\n- **`database/schema.ts`** - Unified Drizzle schema (shared between client \u0026 worker - same table definitions, different adapters)\n  - Server tables: `users`, `trips`, `tours`, etc. (used by worker via `database/server.ts`)\n  - Client cached tables: `cachedUsers`, `cachedTrips`, etc. (used by client via `database/client.ts`)\n  - Both use UUID (text) IDs for consistency\n- **`api/`** - Auto-generated client code (run `yarn codegen` to update)\n\n## 📡 Social Feed \u0026 Streaming (How it Works)\n\n### Overview\n- Unified feed via normalized `feed_events` for all entities (Post/Trip/Tour/Place/Location).\n- Semi‑realtime: GraphQL `feedNewEvents` subscription with server‑side filters; client shows “Show 3 new” banner (capped), merges on tap.\n- Personalization: Per‑user `feed_preferences` (topics, entity types, following‑only, close‑friends‑only, mutes).\n- Follow graph: `follow_edges`, `close_friends` with filters and boosts (following +1, close‑friends +2, topic matches +0.5 each).\n- Trending: KV counters updated on writes; Durable Object `TrendingRollup` compacts/decays periodically; `getTrending` prefers KV (fallback to D1 for ENTITY).\n- Search: `search` (lexical) and `searchSemantic` (Vectorize + Workers AI + Queues). Writers upsert into `search_index`; producers enqueue embeddings; consumer upserts vectors.\n- Offline‑first: Queries work from Apollo/Drizzle cache; subscriptions no‑op when offline.\n\n### Key GraphQL APIs\n- Feed:\n  - `getFeed(first, after, filter: FeedFilter)` → paginated `FeedConnection`\n  - `feedNewEvents(filter: FeedFilter)` → subscription (server filters applied)\n  - `getFeedPreferences` / `updateFeedPreferences(input: FeedFilter!)`\n- Search:\n  - `search(query, entityTypes, topics, first, after)` (lexical)\n  - `searchSuggest(prefix, limit)`\n  - `searchSemantic(query, entityTypes, first, after)` (Vectorize KNN)\n- Trending:\n  - `getTrending(type: TrendingType!, window: TimeWindow!, entityTypes?, limit)` (KV preferred)\n\n### Storage \u0026 Infra\n- D1: `feed_events`, `feed_preferences`, `search_index`, `follow_edges`, `close_friends`, `embeddings_meta`.\n- KV: `top:entity:\u003cwindow\u003e`, `top:topic:\u003cwindow\u003e` lists.\n- DO: `SubscriptionPool` (subs), `TrendingRollup` (decay/trim); cron every 10 minutes.\n- Queues: `EMBED_QUEUE` producer/consumer; Workers AI (`@cf/baai/bge-m3`) to embed.\n- Vectorize: upsert vectors with metadata `{ entityType, entityId, lang }`.\n\n\n---\n\n\n\n## 💡 How to Add New Features\n\n### Complete Workflow: Adding a GraphQL Query/Mutation\n\nThis is the **standard workflow** for adding new features. Follow these steps:\n\n#### Step 1: Define in GraphQL Schema\n```graphql\n# graphql/schema.graphql\ntype Query {\n  getTours(category: String, limit: Int): [Tour!]!\n}\n\ntype Tour {\n  id: ID!\n  title: String!\n  location: String!\n  price: Float!\n  # ... other fields\n}\n```\n\n#### Step 2: Create Operation File\n```graphql\n# graphql/queries/getTours.graphql\nquery GetTours($category: String, $limit: Int) {\n  getTours(category: $category, limit: $limit) {\n      id\n    title\n    location\n    price\n    rating\n    reviews\n  }\n}\n```\n\n#### Step 3: Run GraphQL Codegen\n```bash\nyarn codegen\n```\n\nThis generates:\n- `api/types.ts` - TypeScript types for `Tour`, `GetToursQuery`, etc.\n- `api/hooks.ts` - React hooks like `useGetToursQuery()`\n\n#### Step 4: Implement Resolver (Worker)\n```typescript\n// worker/queries/getTours.ts\nimport { getServerDB, tours } from '@database/server';\nimport { eq, and } from 'drizzle-orm';\n\nexport const getTours = async (\n  _: any,\n  { category, limit }: { category?: string; limit?: number },\n  context: any\n) =\u003e {\n  const db = getServerDB(context.env.DB);\n  let query = db.select().from(tours).where(eq(tours.isActive, true));\n  \n  if (category) {\n    query = query.where(eq(tours.category, category));\n  }\n  \n  const results = await query.limit(limit || 100).all();\n  return results;\n};\n```\n\nDon't forget to export it:\n```typescript\n// worker/queries/index.ts\nexport * from './getTours';\n```\n\n#### Step 5: Use in Component\n```typescript\n// app/(app)/(explore)/tours/index.tsx\nimport { useGetToursQuery } from '@api'; // Auto-generated hook with automatic Drizzle sync\nimport { ActivityIndicator, View, Text } from 'react-native';\n\nexport default function ToursScreen() {\n  // DrizzleCacheStorage automatically syncs to Drizzle on every cache write\n  const { data, loading, error } = useGetToursQuery({\n    variables: { category: 'adventure', limit: 10 }\n    // fetchPolicy defaults to 'cache-and-network' for offline support\n  });\n\n  if (loading) return \u003cActivityIndicator /\u003e;\n  if (error) return \u003cText\u003eError: {error.message}\u003c/Text\u003e;\n\n  return (\n    \u003cView\u003e\n      {data?.getTours.map(tour =\u003e (\n        \u003cTourCard key={tour.id} tour={tour} /\u003e\n      ))}\n    \u003c/View\u003e\n  );\n}\n```\n\n**Important**: After changing the GraphQL schema or operations, **always run `yarn codegen`** before using the new hooks.\n\n### Adding a New UI Component\n\n1. **Create Component** (using NativeWind/Tailwind):\n```typescript\n// ui/cards/TourCard.tsx\nimport { View, Text, TouchableOpacity } from 'react-native';\nimport { CustomText } from '@ui/CustomText';\n\ninterface TourCardProps {\n  tour: { id: string; name: string };\n  onPress?: () =\u003e void;\n}\n\nexport default function TourCard({ tour, onPress }: TourCardProps) {\n  return (\n    \u003cTouchableOpacity \n      onPress={onPress}\n      className=\"bg-white dark:bg-gray-800 p-4 rounded-lg shadow-sm mb-3\"\n    \u003e\n      \u003cText className=\"text-lg font-semibold text-gray-900 dark:text-white\"\u003e\n        {tour.name}\n      \u003c/Text\u003e\n    \u003c/TouchableOpacity\u003e\n  );\n}\n```\n\n2. **Use Path Aliases**:\n```typescript\nimport { useColorScheme } from '@hooks/useColorScheme';\nimport { colors } from '@constants/Colors';\nimport { useAppDispatch } from '@state/hooks';\n```\n\n### Adding to Redux Store\n\n1. **Create Slice**:\n```typescript\n// ui/state/slices/toursSlice.ts\nimport { createSlice } from '@reduxjs/toolkit';\n\nconst toursSlice = createSlice({\n  name: 'tours',\n  initialState: { tours: [] },\n  reducers: {\n    setTours: (state, action) =\u003e {\n      state.tours = action.payload;\n    },\n  },\n});\n\nexport const { setTours } = toursSlice.actions;\nexport default toursSlice.reducer;\n```\n\n2. **Add to Store**:\n```typescript\n// ui/state/index.ts\nimport toursReducer from './slices/toursSlice';\n\n// Add to combineReducers\ntours: toursReducer,\n```\n\n---\n\n## 🔧 Configuration\n\n### Environment \u0026 Configuration\n\n#### GraphQL Endpoint\n\nThe client determines the GraphQL URL in this order:\n\n1. `app.config.js` → `expo.extra.graphqlUrl` (recommended)\n2. `process.env.EXPO_PUBLIC_GRAPHQL_URL_DEV` or `process.env.EXPO_PUBLIC_GRAPHQL_URL`\n3. `process.env.GRAPHQL_URL_DEV` or `process.env.GRAPHQL_URL`\n4. Fallback in dev to `http://192.168.1.51:8787/graphql`\n\nConfigure production and development endpoints via environment variables used by `app.config.js`:\n\n```bash\n# .env\nEXPO_PUBLIC_GRAPHQL_URL=https://safarnak.app/graphql\n# Optional dev override\nEXPO_PUBLIC_GRAPHQL_URL_DEV=http://127.0.0.1:8787/graphql\n```\n\nRelevant sources:\n- `api/client.ts` (URI resolution and auth link)\n- `app.config.js` (`expo.extra.graphqlUrl` derived from env)\n\n#### App Identity (Android)\n\nCustomize via env for EAS or local builds:\n\n```bash\nAPP_NAME=\"سفرناک\"\nBUNDLE_IDENTIFIER=ir.mohet.safarnak\nAPP_SCHEME=safarnak\nANDROID_VERSION_CODE=800   # optional override\n```\n\n### Path Aliases\n\n```typescript\nimport { useLoginMutation } from '@api';\nimport { useAppDispatch } from '@state/hooks';\nimport { login } from '@state/slices/authSlice';\nimport { useColorScheme } from '@hooks/useColorScheme';\nimport Colors from '@constants/Colors';\n```\n\n**Never use relative imports** (`../../api`, `../store`). Always use path aliases.\n\n### GraphQL Codegen\n\nAuto-generates TypeScript types and React hooks from GraphQL schema:\n\n1. **Schema** (`graphql/schema.graphql`) defines types\n2. **Operations** (`graphql/queries/*.graphql`) define queries/mutations\n3. **Run** `yarn codegen` to generate `api/hooks.ts` and `api/types.ts`\n4. **Use** generated hooks: `import { useLoginMutation } from '@api'`\n\n**Important**: Always run `yarn codegen` after modifying GraphQL schema or operations.\n\n---\n\n## 📋 Common Commands\n\n```bash\n# Development\nyarn dev              # Start both worker \u0026 client\nyarn start            # Expo dev server only\nyarn worker:dev       # Worker only\n\n# Database\nyarn db:generate      # Generate migration from schema changes (server tables only)\nyarn db:migrate       # Apply migrations to local D1 (server database)\nyarn db:studio        # Open Drizzle Studio\n\n# GraphQL\nyarn codegen          # Generate types \u0026 hooks\nyarn codegen:watch    # Watch mode\n\n# Build\nyarn android          # Run on Android\nyarn build:debug      # EAS debug build (Android)\nyarn build:release    # Build release APK\nyarn build:local      # Local gradle release build\n\n# Utilities\nyarn clean            # Clear caches\nyarn lint             # Check code quality\nyarn lint:fix         # Fix issues\n \n# Versioning \u0026 Commits\nyarn commit:generate  # Generate a conventional commit message\nyarn version:minor    # Release-it minor bump (CI)\n```\n\n---\n\n## 🛠️ Technology Stack\n\n| Layer | Technology | Purpose |\n|-------|-----------|---------|\n| **Frontend** | React Native 0.81.5 | Mobile UI |\n| **Backend** | Cloudflare Workers | Serverless API |\n| **Server Database** | Cloudflare D1 (SQLite) | Server database via Drizzle |\n| **Client Database** | Expo SQLite | Local offline database via Drizzle |\n| **GraphQL** | GraphQL Yoga 5.16.0 | API layer |\n| **ORM** | Drizzle 0.44.7 | Type-safe queries (shared schema for both server \u0026 client) |\n| **Styling** | NativeWind 4.1.21 + Tailwind CSS 3.4.17 | Utility-first CSS |\n| **State** | Redux Toolkit 2.9.2 | Client state |\n| **Codegen** | GraphQL Codegen 6.0.1 | Auto-generate types |\n| **Router** | Expo Router 6.0.13 | File-based routing |\n\n**Full stack**: TypeScript 5.9, ESLint, Prettier, React i18next, New Architecture enabled\n\n---\n\n## 🧪 Development Tips\n\n1. **Metro Cache Issues**: Run `yarn clean`\n2. **Database Reset**: Delete `.wrangler/state/v3/d1/` and run `yarn db:migrate`\n3. **Type Errors**: Run `yarn codegen` to regenerate types\n4. **GraphQL Changes**: Always run `yarn codegen` after schema changes\n5. **Worker Logs**: Check terminal running `yarn worker:dev`\n6. **Worker URL**: `http://127.0.0.1:8787/graphql` (or `http://localhost:8787/graphql`)\n7. **Styling Issues**: Ensure `global.css` is imported in `app/_layout.tsx`, check `tailwind.config.js` content paths\n8. **NativeWind Not Working**: Clear Metro cache and restart: `yarn clean \u0026\u0026 yarn start`\n\n---\n\n## 🔐 Authentication Flow\n\n1. User logs in → Client calls `login` mutation\n2. Worker validates → Returns user + token\n3. Client stores → Redux + AsyncStorage\n4. Apollo adds token → Automatic auth headers\n5. Auto-redirect → Logged-in users can't access auth pages\n\n**Auth Pages**: `app/(auth)/login.tsx`, `app/(auth)/register.tsx`, `app/(auth)/welcome.tsx`  \n**Auth Guard**: `ui/auth/AuthWrapper.tsx`\n\n---\n\n## 🎨 Styling with NativeWind (Tailwind CSS)\n\nSafarnak uses **NativeWind v4** for utility-first styling with Tailwind CSS. This provides a consistent, maintainable styling approach across the app.\n\n### Key Features\n\n- **Utility-first CSS**: Use Tailwind classes directly via `className` prop\n- **Dark mode**: Automatic dark mode support with `dark:` prefix\n- **Theme integration**: Automatically syncs with Redux theme state\n- **Responsive**: Built-in responsive utilities\n\n### Usage Example\n\n```typescript\nimport { View, Text } from 'react-native';\n\nexport default function MyScreen() {\n  return (\n    \u003cView className=\"flex-1 bg-white dark:bg-black p-4\"\u003e\n      \u003cText className=\"text-xl font-bold text-gray-900 dark:text-white mb-4\"\u003e\n        Welcome to Safarnak\n      \u003c/Text\u003e\n      \u003cView className=\"bg-primary rounded-lg p-3\"\u003e\n        \u003cText className=\"text-white text-center\"\u003ePrimary Button\u003c/Text\u003e\n      \u003c/View\u003e\n    \u003c/View\u003e\n  );\n}\n```\n\n### Configuration Files\n\n- `tailwind.config.js` - Tailwind configuration with custom colors and fonts\n- `global.css` - Tailwind directives (imported in `app/_layout.tsx`)\n- `babel.config.js` - NativeWind Babel preset\n- `metro.config.js` - NativeWind Metro integration\n\n### Custom Colors\n\nThe app uses custom colors defined in `tailwind.config.js`:\n- `primary` - Main brand color (#30D5C8)\n- `danger` - Error/warning color (#ef4444)\n- `success` - Success color (#10b981)\n- `neutral` - Neutral grays\n\n---\n\n## 🌍 Internationalization\n\nSupports English and Persian (Farsi). Note: RTL layout toggling is currently disabled (Android `supportsRtl=false`); translations work without forcing RTL.\n\n```typescript\nimport { useTranslation } from 'react-i18next';\n\nconst { t } = useTranslation();\n\u003cCustomText\u003e{t('common.welcome')}\u003c/CustomText\u003e\n```\n\n**Translation files**: `locales/en/translation.json`, `locales/fa/translation.json`\n\n---\n\n## 🎯 Key Concepts\n\n### Perfect Separation\n\n- **`graphql/`** - Shared schema and operations (used by both client \u0026 worker)\n- **`api/`** - Auto-generated client code only (client-side GraphQL hooks with automatic Drizzle sync)\n- **`worker/`** - Server-only resolvers (entry: `worker/index.ts`)\n- **`database/`** - **Shared** database schema with separate adapters for server and client\n  - `schema.ts` - **Unified schema** defining both server tables (users, trips, etc.) and client cached tables (cachedUsers, cachedTrips, etc.) with UUID IDs\n  - `server.ts` - Server adapter: `getServerDB(d1)` for Cloudflare D1 (worker resolvers use this)\n  - `client.ts` - Client adapter: `getLocalDB()` for Expo SQLite (client components use this)\n  - Both adapters import from the same `schema.ts` file, ensuring schema consistency\n  - `migrations/` - Server-only migrations (located at project root, for D1 database)\n\n### Auto-Generated Code\n\n**Never manually edit**:\n- `api/hooks.ts` - Auto-generated React Apollo hooks (from GraphQL operations)\n- `api/types.ts` - Auto-generated TypeScript types (from GraphQL schema)\n\n**Use these instead**:\n- `api/cache-storage.ts` - DrizzleCacheStorage automatically syncs on every Apollo cache write\n- `api/index.ts` - Main exports (re-exports hooks + utilities)\n\n**Generation Process**:\n1. Define schema in `graphql/schema.graphql`\n2. Define operations in `graphql/queries/*.graphql`\n3. Run `yarn codegen` to generate `api/hooks.ts` and `api/types.ts`\n4. Use generated hooks directly; DrizzleCacheStorage handles sync automatically\n\n### Path Aliases\n\nAlways use aliases, never relative imports:\n- ✅ `@api`, `@ui/*`, `@hooks/useColorScheme`, `@state/*`, `@utils/*`\n- ❌ `../../api`, `../store/hooks`, `@components/*`, `@store/*`\n\n---\n\n## 📶 Offline-First Architecture\n\nSafarnak implements a comprehensive **offline-first architecture** with automatic data synchronization. The system uses a **shared Drizzle schema** that works seamlessly between client and server, with automatic Apollo cache synchronization to enable advanced SQL queries on cached data.\n\n### Shared Drizzle Schema Architecture\n\nThe app uses a **unified Drizzle schema** (`database/schema.ts`) that is **shared between worker and client**. This single source of truth ensures schema consistency across both environments:\n\n#### Schema Structure\n\n1. **Single Schema File** (`database/schema.ts`):\n   - Contains **all table definitions** in one file\n   - Defines both server tables and client cached tables\n   - Uses **UUID (text) IDs** throughout for consistency\n   - Shared field definitions reduce duplication (`userFields`, `tripFields`, etc.)\n\n2. **Server Tables** (for Cloudflare D1):\n   - Tables: `users`, `trips`, `tours`, `messages`, `subscriptions`, etc.\n   - Used by worker resolvers via `database/server.ts` → `getServerDB(d1)`\n   - All IDs are UUIDs: `text('id').primaryKey().$defaultFn(() =\u003e createId())`\n   - No ID conversions needed - UUIDs work seamlessly with GraphQL `ID!` type\n   - Server-only fields: `passwordHash` (users), `aiGenerated` (trips), etc.\n\n3. **Client Cached Tables** (for Expo SQLite):\n   - Tables: `cachedUsers`, `cachedTrips`, `cachedTours`, `cachedPlaces`, `cachedMessages`\n   - Used by client components via `database/client.ts` → `getLocalDB()`\n   - Same UUID format as server tables - perfect consistency\n   - Reuses shared field definitions from server tables via spread operator\n   - Includes sync metadata: `cachedAt`, `lastSyncAt`, `pending`, `deletedAt`\n   - Sync management tables: `pendingMutations`, `syncMetadata`\n\n4. **Shared Field Definitions**:\n   - Common columns extracted into reusable objects: `userFields`, `tripFields`, `tourFields`, `placeFields`, `messageFields`\n   - Metadata columns: `timestampColumns`, `syncMetadataColumns`, `pendingColumn`\n   - Reduces duplication and improves maintainability\n\n5. **Separate Adapters**:\n   - **Server**: `database/server.ts` exports `getServerDB(d1)` - uses Cloudflare D1\n   - **Client**: `database/client.ts` exports `getLocalDB()` - uses Expo SQLite\n   - Both import from the same `schema.ts` file, ensuring perfect schema alignment\n\n6. **UUID Generation** (`database/utils.ts`):\n   - **Cloudflare Workers**: Uses native `crypto.randomUUID()` (fastest, most secure)\n   - **React Native Expo**: Uses `crypto.getRandomValues()` with manual UUID construction\n   - **Fallback**: Math.random() only if crypto APIs unavailable (with dev warning)\n   - RFC 4122 compliant UUID v4 format\n\n### Folder Structure\n\n```\ndatabase/\n├── schema.ts         # Unified schema with UUIDs (server + client tables)\n├── server.ts         # Server adapter (Cloudflare D1)\n├── client.ts         # Client utilities (db, sync, stats)\n├── index.ts          # Main exports\n├── types.ts          # TypeScript types and enums\n└── utils.ts          # UUID utilities\nmigrations/           # Server-only migrations (Cloudflare D1, at project root)\n```\n\n**Important Architecture Points**: \n- **Shared Schema**: Both worker and client import from the same `database/schema.ts` file\n- **Separate Adapters**: Server uses `getServerDB(d1)` (D1), client uses `getLocalDB()` (expo-sqlite)\n- **UUID Consistency**: All tables use UUID (text) IDs - no conversions needed between server/client\n- **Migrations**: \n  - Server migrations at project root (`migrations/`) for D1 database\n  - Client cached tables auto-migrated on app initialization (see `database/client.ts`)\n- **Schema Exports**: `drizzle.config.ts` points to `schema.ts` (exports `serverSchema` as `schema` for migrations)\n- **UUID Generation**: `createId()` from `database/utils.ts` (runtime-optimized for each platform)\n\n### GraphQL Query System with Automatic Sync\n\nAll GraphQL queries and mutations automatically sync to the local Drizzle database:\n\n1. **Query Flow**:\n   ```\n   Component → Generated Hook (useGetTripsQuery) → Apollo Client → GraphQL Server\n                                                       ↓\n                                                  Apollo Cache (raw)\n                                                       ↓\n                                              Automatic Sync\n                                                       ↓\n                                                  Drizzle DB (structured)\n   ```\n\n2. **DrizzleCacheStorage** (`api/cache-storage.ts`):\n   - Implements Apollo's PersistentStorage interface\n   - Automatically syncs on every Apollo cache write (via `setItem()`)\n   - Dual-write: raw cache (`apollo_cache_entries`) + structured tables (cachedUsers, cachedTrips, etc.)\n   - No wrapper hooks needed - all Apollo hooks automatically benefit\n\n3. **Sync Mechanism**:\n   - **Event-driven**: Triggers on every Apollo cache write (via `DrizzleCacheStorage.setItem()`)\n   - **Automatic**: No manual sync calls needed - happens transparently\n   - **Background**: Sync runs in background, doesn't block UI\n   - **Dual-write**: Single transaction writes to both raw cache and structured tables\n\n### Data Storage\n\nThe app uses three storage layers:\n\n1. **Apollo Cache (SQLite)**: Normalized GraphQL cache\n   - Stored in `apollo_cache_entries` table via DrizzleCacheStorage\n   - Stored as JSON string in SQLite\n   - Handles GraphQL query responses automatically\n\n2. **Drizzle Cache (SQLite)**: Structured relational cache\n   - Separate tables per entity type (`cachedTrips`, `cachedUsers`, etc.)\n   - Enables advanced SQL queries (filtering, sorting, aggregations)\n   - Sync metadata for offline management\n\n3. **AsyncStorage**: Mutation queue\n   - Stores pending mutations when offline\n   - Automatically processed when connection restored\n\n### Offline Capabilities\n\n- **Read**: Query local Drizzle database even when offline\n- **Write**: Queue mutations when offline, sync when online\n- **Sync**: Automatic bidirectional sync when connection restored\n- **Statistics**: Real-time database statistics (entity counts, sync status, pending mutations)\n\n#### Reconnect \u0026 Queue Behavior\n\n- When offline, mutations are persisted to an AsyncStorage-backed queue.\n- On reconnect, the queue is processed in order; successful mutations are removed and corresponding cached entities are marked synced.\n- DrizzleCacheStorage continues to dual-write on every Apollo cache update; no wrapper hooks are required.\n- Network status is derived from NetInfo plus a lightweight backend reachability probe.\n\n### Usage Examples\n\n#### Query with Automatic Sync\n\n```typescript\nimport { useGetTripsQuery } from '@api';\n\nfunction TripsScreen() {\n  // DrizzleCacheStorage automatically syncs to Drizzle on every cache write\n  const { data, loading, error } = useGetTripsQuery({\n    fetchPolicy: 'cache-and-network', // Recommended for offline support\n  });\n  \n  // Data is automatically synced to both Apollo cache and Drizzle database\n}\n```\n\n#### Query Local Database (Offline)\n\n```typescript\nimport { getLocalDB, cachedTrips } from '@database/client';\nimport { eq, desc } from 'drizzle-orm';\n\n// Works offline - queries local SQLite database\nconst db = await getLocalDB();\nconst trips = await db\n  .select()\n  .from(cachedTrips)\n  .where(eq(cachedTrips.userId, userId))\n  .orderBy(desc(cachedTrips.cachedAt));\n```\n\n#### Worker Resolver Usage (Server)\n\n```typescript\nimport { getServerDB, trips } from '@database/server';\nimport { eq } from 'drizzle-orm';\n\nexport const getTrips = async (_: any, args: any, context: any) =\u003e {\n  const db = getServerDB(context.env.DB);\n  const userTrips = await db\n    .select()\n    .from(trips)\n    .where(eq(trips.userId, context.userId));\n  return userTrips;\n};\n```\n\n#### Get Database Statistics\n\n```typescript\nimport { getDatabaseStats } from '@database/client';\n\nconst stats = await getDatabaseStats();\nconsole.log(stats.entities.trips.count); // Number of cached trips\nconsole.log(stats.pendingMutations.total); // Pending mutations\n```\n\n### System Status\n\nThe app includes a comprehensive system status page (`app/(app)/(profile)/system-status.tsx`) that shows:\n- Network connectivity status\n- Backend reachability\n- Database statistics (entity counts, sync status, storage usage)\n- Pending mutations queue\n- Sync timestamps per entity type\n\n### Technical Details\n\n- **Sync Triggers**: On query/mutation completion (event-driven, no polling)\n- **Performance**: Sync runs in background, doesn't block UI\n- **Storage**: \n  - Apollo Cache: stored in `apollo_cache_entries` table (normalized GraphQL cache)\n  - Drizzle Cache: `safarnak_local.db` (structured relational cache)\n  - Server: Cloudflare D1 (SQLite via Drizzle)\n- **ID Types**: All tables use UUID (text) IDs - consistent across server, client, and GraphQL\n- **Schema Consistency**: Single source of truth (`database/schema.ts`) ensures server and client schemas stay in sync\n\nFor more details, see the offline architecture implementation in `database/` folder.\n\n---\n \n---\n\n## 🤝 Contributing\n\nPlease read `CONTRIBUTING.md` for setup, workflow, and PR checklist.\n\n---\n\n## 🧭 Code of Conduct\n\nCommunity guidelines are in `CODE_OF_CONDUCT.md`.\n\n---\n\n## 📝 Suggested Improvements \u0026 Roadmap\n\nThis section outlines potential features and improvements. These are suggestions, not commitments.\n\n### 🎯 Priority Features (Near-term)\n\n#### Offline \u0026 Sync Management\n- **Offline Downloads Manager**: UI to manage cached trips, tours, and places for offline access\n- **Sync Queue Screen**: View pending offline mutations with retry controls\n- **Data Management**: Clear cache, view storage usage, selective data purge\n\n#### Trip Planning Enhancements\n- **Itinerary Editor**: Day-by-day editable view using `itineraries` table\n- **AI Planner Chat**: Conversational interface for refining trips using `thoughts` table\n- **Trip Map View**: Visual map showing trip activities and locations\n- **Trip Export/Share**: Export itinerary as PDF or ICS calendar file\n\n#### Explore \u0026 Discovery\n- **Global Search**: Unified search across tours, places, locations, and users\n- **Advanced Filters**: Price range, rating, duration, distance with saved filter sets\n- **Bookmarks System**: Implement UI for `bookmarkTour` and `bookmarkPlace` mutations\n- **Location/Tour/Place Indexes**: Dedicated browse pages for each content type\n\n### 🚀 Future Enhancements\n\n#### Social Features\n- **Rich Post Composer**: Multi-image upload, location tagging, trip linking\n- **Comments Thread**: Full-screen comment view with reactions\n- **Enhanced User Profiles**: Public profiles with posts, trips, and places showcase\n\n#### Profile \u0026 Settings\n- **Travel Preferences**: Edit `user_preferences` (interests, budget, style, dietary)\n- **Device Management**: View and revoke logged-in devices using `devices` table\n- **Billing History**: Detailed payment history with receipts from `payments` table\n- **Notification Settings**: Per-category notification preferences\n\n#### Commerce\n- **My Bookings**: List of purchased tours from `payments.tourId`\n- **Booking Details**: Receipt, cancellation, refund status\n- **Checkout Flow**: Dedicated checkout page with payment integration\n\n### 💡 Where We're Going\n\nThe project is currently at **v1.13.0**. Our focus is on:\n\n1. **Stability**: Fixing authentication security issues, adding input validation\n2. **Core Features**: Completing trip planning, explore, and social features\n3. **Offline Support**: ✅ **Implemented** - Shared Drizzle schema with automatic Apollo → Drizzle sync (see [Offline-First Architecture](#-offline-first-architecture) section)\n4. **Testing**: Adding unit and integration tests\n5. **Documentation**: ✅ **Updated** - Comprehensive docs in README\n\nSee `TECHNICAL_REVIEW.md` for current technical debt and priorities.\n\n## 📄 License\n\nMIT\n\n---\n\n## 🔗 Resources\n\n- [Expo Docs](https://docs.expo.dev/)\n- [Cloudflare Workers](https://developers.cloudflare.com/workers/)\n- [GraphQL Codegen](https://the-guild.dev/graphql/codegen)\n- [Drizzle ORM](https://orm.drizzle.team/)\n\nBuilt with ❤️ using Expo, Cloudflare Workers, and GraphQL Codegen\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmehotkhan%2Fsafarnak.app","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmehotkhan%2Fsafarnak.app","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmehotkhan%2Fsafarnak.app/lists"}