# Agent Instructions ## Overview Scry is a Magic: The Gathering card scanner app built with Expo (React Native) and Convex. It uses perceptual hashing to match photographed cards against a database of known card images from Scryfall. **Tech Stack:** - **Frontend**: Expo/React Native with Expo Router (file-based routing) - **Backend**: Convex (serverless functions + real-time database) - **Image Processing**: React Native Skia, fast-opencv - **Camera**: Adaptive (expo-camera in dev, react-native-vision-camera in production) - **Auth**: Convex Auth with Zitadel OIDC (GDPR-compliant, no PII stored) - **Package Manager**: Bun (not npm/yarn) ## Build Commands Use `just` commands (defined in `.justfile`): | Task | Command | Notes | |------|---------|-------| | Start dev server | `just dev` | Runs Convex + Expo together | | Expo only | `just start` | Just the Expo dev server | | Convex only | `just convex-dev` | Just the Convex dev server | | Run on Android | `just android` | Starts Android emulator | | Install deps | `just expo-install` | Runs `bun install` | | Migrate hashes | `just expo-migrate` | Migrate card hashes to Convex | | Type check | `just expo-typecheck` | TypeScript check | | Start emulator | `just emu` | Virtual camera (submodule) | ### Direct Bun Commands ```bash bun install # Install dependencies bun run dev # Convex + Expo hot reload bun run dev:convex # Convex dev server only bun run dev:expo # Expo dev server only bun run android # Run on Android bun run migrate # Migrate hashes to Convex bun run typecheck # TypeScript check bunx convex dev # Convex CLI ``` ## Project Structure ``` app/ # Expo Router pages ├── _layout.tsx # Root layout (Convex + HashCache providers) ├── modal.tsx # Card details modal ├── +not-found.tsx # 404 page └── (tabs)/ # Tab navigation group ├── _layout.tsx # Tab bar layout ├── index.tsx # Collection tab (home) ├── scan.tsx # Camera scan tab └── settings.tsx # Settings tab components/ ├── camera/ # Adaptive camera system │ ├── index.tsx # AdaptiveCamera wrapper │ ├── ExpoCamera.tsx # expo-camera (Expo Go) │ └── VisionCamera.tsx # react-native-vision-camera (production) └── *.tsx # Shared UI components convex/ # Backend (Convex functions + schema) ├── schema.ts # Database schema ├── auth.ts # Zitadel OIDC configuration ├── http.ts # HTTP endpoints for auth ├── cards.ts # Card queries/mutations ├── collections.ts # User collection functions ├── users.ts # User functions ├── scanHistory.ts # Scan history functions └── _generated/ # Auto-generated types lib/ ├── recognition/ # Card recognition pipeline │ ├── recognitionService.ts # Main recognition logic │ ├── cardDetector.ts # Edge detection, find card quad │ ├── perspectiveCorrection.ts # Warp to rectangle │ ├── clahe.ts # CLAHE lighting normalization │ ├── perceptualHash.ts # 192-bit color hash (24 bytes) │ ├── imageUtils.ts # Resize, rotate, grayscale │ ├── imageLoader.ts # Load/resize images │ └── skiaDecoder.ts # Decode images with Skia ├── hooks/ # React hooks │ ├── useAuth.ts # OAuth flow with expo-auth-session │ ├── useCamera.ts # Adaptive camera permissions │ ├── useConvex.ts # Convex data hooks │ ├── useSync.ts # Hash sync hook │ └── useUserProfile.ts # Fetch profile from Zitadel ├── context/ # React contexts │ └── HashCacheContext.tsx # In-memory hash cache └── db/ # Local database utilities ├── localDatabase.ts # SQLite wrapper └── syncService.ts # Sync with Convex scripts/ └── migrate-hashes.ts # Migration script TestImages/ # Test images (225 files) ``` ## Architecture ### Recognition Pipeline ``` Camera Image │ ▼ ┌─────────────────────┐ │ loadImageAsBase64 │ ← Resize to 480×640 └─────────────────────┘ │ ▼ ┌─────────────────────┐ │ decodeImageBase64 │ ← Skia decodes to RGBA pixels └─────────────────────┘ │ ▼ ┌─────────────────────┐ │ detectCard │ ← Edge detection, find card quad │ (optional) │ └─────────────────────┘ │ ▼ ┌─────────────────────┐ │ warpPerspective │ ← Warp detected quad to rectangle └─────────────────────┘ │ ▼ ┌─────────────────────┐ │ applyCLAHE │ ← Lighting normalization └─────────────────────┘ │ ▼ ┌─────────────────────┐ │ computeColorHash │ ← Compute 192-bit color hash (24 bytes) └─────────────────────┘ │ ▼ ┌─────────────────────┐ │ recognizeCard │ ← Hamming distance match against cache └─────────────────────┘ ``` ### Data Model (Convex Schema) | Table | Purpose | |-------|---------| | `users` | Minimal auth (no PII, GDPR compliant) | | `cards` | Card printings with 24-byte perceptual hashes | | `oracles` | Abstract game cards (one per unique card name) | | `sets` | MTG sets with metadata | | `collections` | User card collections | | `scanHistory` | Scan history with confidence scores | | `metadata` | Sync metadata | ### GDPR Compliance - Database stores **no user PII** - only auth subject ID - User profile (name, email, image) fetched from Zitadel userinfo endpoint on demand - Profile held in memory only, never persisted ### Adaptive Camera System The app detects its runtime environment and uses the appropriate camera: - **Expo Go** (`Constants.appOwnership === "expo"`): Uses `expo-camera` - **Production builds**: Uses `react-native-vision-camera` Both expose the same `CameraHandle` interface with `takePhoto()`. ## Key Algorithms ### Perceptual Hash (pHash) Color-aware 192-bit hash: 1. Resize to 32×32 2. For each RGB channel: - Compute 2D DCT - Extract 8×8 low-frequency coefficients (skip DC) - Compare each to median → 63 bits per channel 3. Concatenate R, G, B hashes → 24 bytes (192 bits) Matching uses Hamming distance with threshold ≤25 bits and minimum confidence 85%. ### CLAHE (Contrast Limited Adaptive Histogram Equalization) Applied in LAB color space to L channel only: - Tile-based histogram equalization (8×8 tiles) - Clip limit prevents over-amplification of noise - Bilinear interpolation between tiles for smooth output ## Code Conventions ### General - **TypeScript**: Strict mode enabled - **Formatting**: Prettier defaults - **Package Manager**: Bun (never use npm/yarn) ### React/React Native - Functional components with hooks - Expo Router for navigation (file-based) - Convex hooks for data (`useQuery`, `useMutation`) ### Naming - Hooks: `useCardHashes`, `useCameraPermission` - Components: PascalCase (`AdaptiveCamera`, `ScanScreen`) - Files: camelCase for modules, PascalCase for components - Convex functions: camelCase (`cards.ts`, `getByScryfallId`) ### Convex Backend - Queries are reactive and cached - Mutations are transactional - Use `v.` validators for all arguments - Index frequently queried fields ## Environment Variables ### Convex Backend (`convex/.env.local`) ```bash AUTH_ZITADEL_ID=your-client-id AUTH_ZITADEL_SECRET=your-client-secret AUTH_ZITADEL_ISSUER=https://your-zitadel-instance ``` ### Client Side (`.env`) ```bash EXPO_PUBLIC_CONVEX_URL=https://your-deployment.convex.cloud EXPO_PUBLIC_ZITADEL_ISSUER=https://your-zitadel-instance ``` ## Common Tasks ### Adding a New Convex Function 1. Add function to appropriate file in `convex/` (e.g., `cards.ts`) 2. Run `bunx convex dev` to regenerate types 3. Import from `convex/_generated/api` in client code ### Testing Recognition 1. Add test images to `TestImages/` 2. Use the scan tab in the app 3. Check console logs for `[Scry]` prefixed messages ### Debugging Camera Issues - In Expo Go: Uses `expo-camera`, check for "Dev mode" indicator - In production: Uses Vision Camera, requires EAS build ## Dependencies ### Core - `expo` ~54.0.33 - `expo-router` ~6.0.23 - `convex` ^1.31.7 - `@convex-dev/auth` ^0.0.90 - `react` 19.1.0 - `react-native` 0.81.5 ### Image Processing - `@shopify/react-native-skia` ^2.4.18 - `react-native-fast-opencv` ^0.4.7 ### Camera - `expo-camera` ^17.0.10 (Expo Go) - `react-native-vision-camera` ^4.7.3 (production) ### Auth - `expo-auth-session` ^7.0.10 - `expo-secure-store` ^15.0.8 ## External Resources - [Scryfall API](https://scryfall.com/docs/api) - Card data source - [Convex Docs](https://docs.convex.dev/) - Backend documentation - [Expo Router](https://docs.expo.dev/router/introduction/) - Navigation - [docs/CARD_RECOGNITION.md](docs/CARD_RECOGNITION.md) - Recognition architecture