Complete rewrite of Scry using TypeScript stack:
- Expo/React Native with Expo Router (file-based routing)
- Convex backend (serverless functions + real-time database)
- Adaptive camera system (expo-camera in Expo Go, Vision Camera in production)
- React Native Skia + fast-opencv for image processing
- GDPR-compliant auth setup with Zitadel OIDC (pending configuration)
Key features:
- Card recognition pipeline ported to TypeScript
- Perceptual hashing (192-bit color pHash)
- CLAHE preprocessing for lighting normalization
- Local SQLite cache with Convex sync
- Collection management with offline support
Removes all .NET/MAUI code (src/, test/, tools/).
💘 Generated with Crush
Assisted-by: Claude Opus 4.5 via Crush <crush@charm.land>
288 lines
9.9 KiB
Markdown
288 lines
9.9 KiB
Markdown
# 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
|