scry/convex/schema.ts
Chris Kruining 83ab4df537
Migrate from .NET MAUI to Expo + Convex
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>
2026-02-09 16:16:34 +01:00

93 lines
2.7 KiB
TypeScript

import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
import { authTables } from "@convex-dev/auth/server";
// Override the default users table to store NO personal data (GDPR compliance)
// User profile info (name, email, etc.) must be fetched from Zitadel OIDC userinfo endpoint
const minimalAuthTables = {
...authTables,
// Override users table - only store what's required for auth to function
users: defineTable({
// No name, email, image, or any PII stored
// The auth system needs this table to exist but we strip all profile data
}),
};
export default defineSchema({
...minimalAuthTables,
// Card printings with perceptual hashes
cards: defineTable({
scryfallId: v.string(),
oracleId: v.string(),
name: v.string(),
setCode: v.string(),
collectorNumber: v.string(),
rarity: v.string(),
artist: v.optional(v.string()),
imageUri: v.optional(v.string()),
hash: v.bytes(), // 24-byte perceptual hash
hashVersion: v.number(), // Algorithm version for migrations
updatedAt: v.number(),
})
.index("by_scryfall", ["scryfallId"])
.index("by_oracle", ["oracleId"])
.index("by_name", ["name"])
.index("by_updated", ["updatedAt"]),
// Oracle cards (abstract game cards)
oracles: defineTable({
oracleId: v.string(),
name: v.string(),
manaCost: v.optional(v.string()),
cmc: v.optional(v.number()),
typeLine: v.optional(v.string()),
oracleText: v.optional(v.string()),
colors: v.optional(v.array(v.string())),
colorIdentity: v.optional(v.array(v.string())),
keywords: v.optional(v.array(v.string())),
power: v.optional(v.string()),
toughness: v.optional(v.string()),
})
.index("by_oracle", ["oracleId"])
.index("by_name", ["name"]),
// MTG sets
sets: defineTable({
setId: v.string(),
code: v.string(),
name: v.string(),
setType: v.optional(v.string()),
releasedAt: v.optional(v.string()),
cardCount: v.optional(v.number()),
iconSvgUri: v.optional(v.string()),
})
.index("by_set", ["setId"])
.index("by_code", ["code"]),
// User collections
collections: defineTable({
userId: v.id("users"),
cardId: v.id("cards"),
quantity: v.number(),
isFoil: v.boolean(),
addedAt: v.number(),
})
.index("by_user", ["userId"])
.index("by_user_card", ["userId", "cardId", "isFoil"]),
// Scan history
scanHistory: defineTable({
userId: v.id("users"),
cardId: v.optional(v.id("cards")),
confidence: v.number(),
scannedAt: v.number(),
addedToCollection: v.boolean(),
}).index("by_user", ["userId"]),
// Sync metadata
metadata: defineTable({
key: v.string(),
value: v.string(),
}).index("by_key", ["key"]),
});