A production-quality monorepo for managing One Piece TCG inventory, pricing, and public vendor pages.
/
βββ apps/
β βββ api/ # NestJS backend API
β β βββ src/
β β βββ modules/
β β β βββ auth/ # Authentication (minimal Stage 1)
β β β βββ cards/ # Card catalog endpoints
β β β βββ health/ # Health check endpoint
β β β βββ inventory/ # Inventory CRUD
β β β βββ pricing/ # Pricing endpoints
β β β βββ prisma/ # Database service
β β β βββ profile/ # Seller profile endpoints
β β β βββ vendor/ # Public vendor page
β β βββ app.module.ts
β β βββ main.ts
β β
β βββ web/ # Next.js frontend
β βββ src/
β βββ app/
β βββ components/
β βββ lib/
β
βββ packages/
β βββ shared/ # Shared types (future use)
β
βββ infra/
β βββ docker/ # Docker compose for PostgreSQL
β
βββ prisma/
β βββ schema.prisma # Database schema
β βββ migrations/ # Migration history
β βββ seed.ts # Seed script
β
βββ package.json # Root monorepo scripts
- Node.js 18+
- pnpm 9+
- Docker (for PostgreSQL)
pnpm installpnpm db:upThis starts PostgreSQL on localhost:5432 with:
- User:
postgres - Password:
postgres_password - Database:
slabhub
pnpm prisma:generatepnpm prisma:migratepnpm seedThis creates:
- 1 Demo Seller Profile (
nami-treasures) - 30 One Piece TCG Card Profiles
- 90 Card Variants (3 per card)
- 30 Pricing Snapshots
- 18 Inventory Items (10 RAW, 5 GRADED, 3 SEALED)
# Start both API and Web
pnpm dev
# Or start individually
pnpm dev:api # API on http://localhost:3001
pnpm dev:web # Web on http://localhost:3000| Script | Description |
|---|---|
pnpm dev |
Run API + Web in parallel |
pnpm dev:api |
Run API only |
pnpm dev:web |
Run Web only |
pnpm build |
Build all packages |
pnpm db:up |
Start PostgreSQL container |
pnpm db:down |
Stop PostgreSQL container |
pnpm db:logs |
View PostgreSQL logs |
pnpm prisma:generate |
Generate Prisma Client |
pnpm prisma:migrate |
Run migrations (dev) |
pnpm prisma:studio |
Open Prisma Studio |
pnpm seed |
Seed the database |
pnpm pricecharting:crawl:onepiece |
Run PriceCharting One Piece crawler |
pnpm pricecharting:cleanup:links |
Clean up referral links in PriceCharting sales data |
The PriceCharting crawler is a robust ingestion pipeline for One Piece TCG cards.
| Command | Description |
|---|---|
pnpm pricecharting:crawl:onepiece |
Start a full crawl of One Piece cards |
pnpm pricecharting:cleanup:links |
Clean up existing referral/tracking links in the database |
... --dryRun |
Fetch/Parse/Cleanup but do not save to database |
... --maxProducts 10 |
Limit the number of products for testing |
... --linkRefProducts |
Link crawled URLs to existing RefProduct records |
... --onlySetSlug <slug> |
Only crawl a specific set (e.g. one-piece-500-years-in-the-future) |
- Rate Limiting: Implementation ensures max 1 request per second to respect the target site.
- Resilience: Automatic retries (3 times) with exponential backoff on 429/5xx errors.
- Idempotency: Products are upserted based on their canonical URL; re-running the crawler will update existing records.
- Data Capture: Stores full "Details" block as JSON, along with normalized TCGPlayerID, PriceChartingID, and Card Number.
- Traversal: Automatically discovers set pages from the category entrypoint and handles pagination within sets.
Base URL: http://localhost:3001
| Method | Endpoint | Description |
|---|---|---|
| GET | /health |
Health check with DB status |
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/me |
Get current seller profile |
| PATCH | /v1/me |
Update seller profile |
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/cards |
List all cards |
| GET | /v1/cards?query=luffy |
Search cards |
| GET | /v1/cards/:id |
Get card by ID |
| GET | /v1/cards/:id/variants |
Get card variants |
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/inventory |
List seller's inventory |
| GET | /v1/inventory/:id |
Get inventory item |
| POST | /v1/inventory |
Create inventory item |
| PATCH | /v1/inventory/:id |
Update inventory item |
| DELETE | /v1/inventory/:id |
Delete inventory item |
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/pricing |
List all pricing |
| POST | /v1/pricing/refresh |
Refresh all prices |
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/vendor/:handle |
Get vendor page data |
For Stage 1, authentication is header-based:
# Use x-user-handle header
curl -H "x-user-handle: nami-treasures" http://localhost:3001/v1/me
# Or x-user-id header
curl -H "x-user-id: <seller-id>" http://localhost:3001/v1/me
# If no header, defaults to "nami-treasures" demo sellercurl http://localhost:3001/healthcurl http://localhost:3001/v1/mecurl -X PATCH http://localhost:3001/v1/me \
-H "Content-Type: application/json" \
-d '{"shopName": "Updated Shop Name", "meetupsEnabled": true}'curl http://localhost:3001/v1/cards
# With search
curl "http://localhost:3001/v1/cards?query=luffy"curl http://localhost:3001/v1/cards/op01-001curl http://localhost:3001/v1/inventorycurl -X POST http://localhost:3001/v1/inventory \
-H "Content-Type: application/json" \
-d '{
"itemType": "SINGLE_CARD_RAW",
"cardVariantId": "<variant-id>",
"condition": "NM",
"quantity": 1,
"stage": "ACQUIRED",
"acquisitionPrice": 25
}'curl -X POST http://localhost:3001/v1/inventory \
-H "Content-Type: application/json" \
-d '{
"itemType": "SINGLE_CARD_GRADED",
"cardVariantId": "<variant-id>",
"gradeProvider": "PSA",
"gradeValue": "10",
"certNumber": "CERT-12345",
"quantity": 1,
"stage": "LISTED",
"acquisitionPrice": 150,
"listingPrice": 299
}'curl -X POST http://localhost:3001/v1/inventory \
-H "Content-Type: application/json" \
-d '{
"itemType": "SEALED_PRODUCT",
"productName": "Romance Dawn Booster Box",
"productType": "BOOSTER_BOX",
"language": "JP",
"integrity": "MINT",
"quantity": 1,
"stage": "IN_STOCK",
"acquisitionPrice": 120,
"configuration": {
"containsBoosters": true,
"packsPerUnit": 24,
"containsFixedCards": false,
"containsPromo": false
}
}'curl -X PATCH http://localhost:3001/v1/inventory/<item-id> \
-H "Content-Type: application/json" \
-d '{"stage": "LISTED", "listingPrice": 45.99}'curl -X DELETE http://localhost:3001/v1/inventory/<item-id>curl http://localhost:3001/v1/pricingcurl -X POST http://localhost:3001/v1/pricing/refreshcurl http://localhost:3001/v1/vendor/nami-treasures| mockApi Function | Real API Endpoint |
|---|---|
getCurrentUser() |
GET /v1/me |
updateProfile(patch) |
PATCH /v1/me |
listCardProfiles(query) |
GET /v1/cards?query=... |
getCardProfile(id) |
GET /v1/cards/:id |
listInventory() |
GET /v1/inventory |
createInventoryItem(item) |
POST /v1/inventory |
updateInventoryItem(id, patch) |
PATCH /v1/inventory/:id |
deleteInventoryItem(id) |
DELETE /v1/inventory/:id |
listPricing() |
GET /v1/pricing |
refreshPricing() |
POST /v1/pricing/refresh |
| (new) Vendor page | GET /v1/vendor/:handle |
- SellerProfile: User/seller account with shop info
- CardProfile: Global card catalog (One Piece TCG)
- CardVariant: Specific variants (language, foil type)
- PricingSnapshot: Global pricing per card
- InventoryItem: Seller's inventory (RAW, GRADED, SEALED)
- PricingSnapshot is GLOBAL per CardProfile
- InventoryItem references CardVariant (not CardProfile directly)
- Market price is derived from PricingSnapshot via CardVariant β CardProfile
ACQUIRED- Just purchasedIN_TRANSIT- Shipping to sellerBEING_GRADED- At grading companyAUTHENTICATED- Verified authenticIN_STOCK- Ready but not listedLISTED- For sale publiclySOLD- Transaction completeARCHIVED- No longer active
SINGLE_CARD_RAW- Ungraded single cardSINGLE_CARD_GRADED- Professionally graded cardSEALED_PRODUCT- Unopened product
- Backend: NestJS, TypeScript, Prisma
- Database: PostgreSQL
- Frontend: Next.js, React, TailwindCSS, Shadcn/UI
- Infrastructure: Docker, pnpm workspaces
SlabHub uses a session-based authentication with email OTP (magic codes).
- Request OTP: User enters email on
/login. - Generate & Store: Backend generates a 6-digit code, hashes it, and stores it in the
OtpChallengetable. - Send: In development, the OTP is printed to the server console.
- Verify: User enters the code on
/otp. - Session: Backend verifies the code, upserts the user, and creates a session record. A secure
HttpOnlycookie is set in the browser. - Current User: The
GET /v1/meendpoint returns the current user based on the session cookie.
- Ensure you have the following in your
.env:SESSION_COOKIE_NAME=slabhub_session OTP_TTL_MINUTES=10 OTP_SECRET=dev-secret
- Start the API and Web app.
- Navigate to
http://localhost:3000/login. - Enter any email.
- Check the API terminal output for the OTP code.
- Enter the code on the OTP page.
- You should be redirected to the dashboard.
Note: Facebook and Google login buttons are currently stubs for UI demonstration.
SlabHub stores binary files in DigitalOcean Spaces (or any S3-compatible storage) with content-hash based deduplication.
Add these to your .env:
S3_ENDPOINT=https://nyc3.digitaloceanspaces.com
S3_REGION=nyc3
S3_BUCKET=slabhub-files-dev
S3_ACCESS_KEY_ID=your-access-key
S3_SECRET_ACCESS_KEY=your-secret-key
S3_PUBLIC_BASE_URL=https://slabhub-files-dev.nyc3.digitaloceanspaces.com # Public origin
S3_CDN_BASE_URL=https://slabhub-files-dev.nyc3.cdn.digitaloceanspaces.com # CDN origin
S3_FORCE_PATH_STYLE=false- Deduplication: Same photo uploaded twice only takes space once.
- CDN Support: URLs automatically use the CDN base if configured.
- Robust Ingestion: PriceCharting images are automatically stored in the media layer.
MIT