Monorepo for a local-first trading card scanner/catalog app (frontend + backend + infra).
Stack:
- Frontend: Next.js (Pages Router)
- Backend: NestJS + Prisma
- DB: Postgres
- Object storage: Garage (S3-compatible)
Roadmap from MVP to end-state is documented in roadmap.md.
- Node.js 22+
- npm 10+
- Docker + Docker Compose
- Copy and fill env files:
.env.example->.envapps/backend/.env.example->apps/backend/.envapps/frontend/.env.example->apps/frontend/.env
- Garage config is generated from template:
- Source:
garage.toml.template - Generated:
garage.toml(gitignored)
npm run infra:upnpm run dev:localnpm run dev:dockernpm run dev:docker:buildnpm run stop:localnpm run infra:terraform:fmt
npm run infra:terraform:validatenpm run clean:dev-cache- Frontend:
3000 - Backend:
3001 - Postgres (host):
5433 - Garage S3 API:
3900 - Garage RPC:
3901 - Garage Web:
3902 - Garage Admin API:
3903
Base URL: http://localhost:3001/api/v1
POST /auth/signupPOST /auth/loginPOST /auth/logoutGET /auth/mePOST /scans(multipart upload:imagerequired,backImageoptional)GET /scans/:scanIdPOST /scans/:scanId/confirmGET /cardsGET /cards/:cardIdPATCH /cards/:cardIdPOST /import/cards/csv(multipart CSV upload)
Health endpoint stays outside prefix:
GET http://localhost:3001/health
Swagger docs:
GET http://localhost:3001/api/docs
Auth now uses an HttpOnly cookie named trading_card_session. Guests can browse the demo binder, but scans, imports, and card edits require login.
From repo root:
npm exec -w apps/backend prisma generate
npm run db:migrate -w apps/backend
npm run db:seed -w apps/backendRailway / hosted database rescue is now explicit instead of happening during app startup.
Inspect the target database first:
TARGET_DATABASE_URL="postgres://..." npm run db:railway:inspectIf the database is still on the legacy pre-normalized Card table, export the catalog, migrate a clean database, then import:
TARGET_DATABASE_URL="postgres://legacy-db" npm run db:catalog:export
TARGET_DATABASE_URL="postgres://clean-db" npm exec -w apps/backend prisma migrate deploy
TARGET_DATABASE_URL="postgres://clean-db" npm run db:catalog:importIf the database already matches the current normalized Prisma schema and only lacks _prisma_migrations, verify the diff is empty and then mark migrations as applied once:
TARGET_DATABASE_URL="postgres://..." npm run db:railway:mark-appliedstart:prod no longer tries to auto-baseline P3005 at boot. Partial drift should be treated as a manual rescue task, not something hidden inside deployment startup.
To export legacy local Card rows into the normalized catalog seed before a reset:
npm run db:export:catalog -w apps/backendBackend unit tests:
npm run test -w apps/backendTypecheck both apps:
npm run typecheckTerraform scaffolding for the AWS move now lives in infra/terraform.
It provisions:
- one private bucket for profile images
- one private bucket for card media
- versioning, public-access blocks, lifecycle cleanup, and CORS configuration
- IAM policy document outputs scoped to
profiles/,user-cards/, andcanonical-cards/
Example flow:
cp infra/terraform/terraform.tfvars.example infra/terraform/terraform.tfvars
npm run infra:terraform:fmt
npm run infra:terraform:validate
terraform -chdir=infra/terraform planBackend env wiring for AWS-compatible storage:
S3_ENDPOINTstays optional so local Garage keeps workingS3_PROFILE_BUCKETS3_CARD_BUCKETS3_ACCESS_KEYS3_SECRET_KEYS3_REGION
Operational scripts for media cutover:
npm run storage:verify -w apps/backend
npm run storage:migrate:s3 -w apps/backendMigration uses the configured target S3_* variables and optional SOURCE_S3_* variables for the current source storage. Object keys are preserved exactly; legacy HTTP URLs and local/* paths are skipped.
- OCR defaults to
tesseractin backend (OCR_PROVIDER=tesseract) with fallback mode available viaOCR_PROVIDER=stub. - Reverse lookup always includes
duckduckgo, and automatically layers ingoogle_visionwhen Google Vision credentials are configured. - Scan upload supports
image(front, required) andbackImage(optional but recommended). - Matching now uses weighted scoring with structured OCR hints (
year,card number,brand,season,set) when available. - CSV import supports
imageUrl/image_urlto fetch card images and store them in Garage/S3. - Validation now uses lexical scoring; scan review links come from lookup hints (DuckDuckGo/web lookup).