Node.js/Express API for browsing, comparing, and monitoring PlayFab Catalog marketplace items — with JWT auth, LRU caching, OpenAPI validation, SSE event streams, and webhooks. Built for high throughput with keep-alive HTTP agents, debounced caching, and resilient upstream retries.
- Overview
- Key Features
- Quickstart
- Configuration (Environment)
- Data Files & Local Storage
- Runtime & Architecture
- Security Model
- HTTP API
- Pagination & Caching (ETag/Cache-Control)
- Error Model
- Rate Limiting
- Server-Sent Events (SSE)
- Webhooks
- OpenAPI & Swagger UI
- Logging
- Caching Layers
- Data Model (Transformed Item)
- Directory Layout
- Development & Utilities
- Testing
- Performance Tuning
- Deployment
- Observability & Ops
- Troubleshooting
- FAQ
- Contributing
- License
PlayFab-Catalog Bedrock API provides a read-optimized façade over the PlayFab Catalog. It consolidates listing, search, tag exploration, “featured servers”, price/sale aggregation, as well as advanced search with facets. For live insights, it emits SSE signals for item changes, price changes, and trending creators, and forwards events to external systems using webhooks with signatures and retries.
The service is intentionally stateless (except JSON files on disk) and production-friendly: aggressive keep-alive, retry budget, de-duped in-flight requests, and LRU caches for hot data.
- 🔐 JWT authentication with role guards (admin endpoints).
- 🎛️ Configurable via environment with sensible defaults.
- ⚡ Throughput: Node keep-alive agents + safe retry/jitter/backoff.
- 🧾 ETags and Cache-Control for CDN/proxy friendliness.
- ✅ OpenAPI schema with optional request/response validation.
- 🔎 Advanced search (filters, sort, pagination, facets).
- 🧩 Item enrichment (resolved references, prices, reviews).
- 🧭 Featured servers backed by config + live catalog resolution.
- 🛍️ Sales aggregator across stores; per-creator slicing.
- 📡 SSE for
item.*,price.changed,sale.*,creator.trending. - 🔔 Webhooks with HMAC-style signature (stable SHA-1 over body+secret) and retry/backoff.
- 🧰 Tooling: OpenAPI ref fixer, request logger, pagination helpers.
# 1) Clone & install
git clone https://github.com/Daniel-Ric/PlayFab-Catalog-Service-Bedrock
cd playfab-catalog-api
npm ci
# 2) Configure environment
cp .env.example .env
# IMPORTANT: set JWT_SECRET (>= 32 chars), ADMIN_USER/ADMIN_PASS, DEFAULT_ALIAS/TITLE_ID, etc.
# 3) Start (development)
NODE_ENV=development node src/index.js
# 4) Production
NODE_ENV=production LOG_LEVEL=info node src/index.jsBase URL (default): http://localhost:3000
Required:
JWT_SECRET(>= 32 chars). Server exits if missing/too short.
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
HTTP port |
NODE_ENV |
production |
development |
JWT_SECRET |
— | JWT signing secret (>= 32 chars) |
ADMIN_USER |
— | Username for /login |
ADMIN_PASS |
— | Password for /login |
LOG_LEVEL |
info |
error |
CORS_ORIGINS |
empty | Comma-separated list; empty disables CORS |
TRUST_PROXY |
1 |
Express trust proxy |
ENABLE_DOCS |
false |
Serve Swagger UI at /docs |
VALIDATE_REQUESTS |
false |
Enable OpenAPI request validation |
VALIDATE_RESPONSES |
false |
Enable OpenAPI response validation |
UPSTREAM_TIMEOUT_MS |
20000 |
Axios timeout for PlayFab requests |
HTTP_MAX_SOCKETS |
512 |
Keep-alive sockets (HTTP) |
HTTPS_MAX_SOCKETS |
512 |
Keep-alive sockets (HTTPS) |
| Variable | Default | Description |
|---|---|---|
TITLE_ID |
20CA2 |
Fallback TitleId (used if alias not provided) |
DEFAULT_ALIAS |
prod |
Default alias → TitleId mapping (see /titles) |
FEATURED_PRIMARY_ALIAS |
prod |
Primary alias for featured/SSE sources |
OS |
iOS |
OS label used in upstream requests |
PLAYFAB_CONCURRENCY |
12 |
Parallel PlayFab requests |
PLAYFAB_BATCH |
600 |
Max batch size for bulk PlayFab calls |
| Variable | Default | Description |
|---|---|---|
SESSION_TTL_MS |
1800000 |
LRU session TTL (ms) |
SESSION_CACHE_MAX |
1000 |
Max entries in session cache |
DATA_TTL_MS |
300000 |
LRU generic data TTL (ms) |
DATA_CACHE_MAX |
20000 |
Max entries in generic data cache |
ADV_SEARCH_TTL_MS |
60000 |
TTL for advanced search cache (ms) |
PAGE_SIZE |
100 |
Default page size for list endpoints |
| Variable | Default | Description |
|---|---|---|
ENABLE_SALES_WATCHER |
true |
Enable sales watcher |
ENABLE_ITEM_WATCHER |
true |
Enable item watcher |
ENABLE_PRICE_WATCHER |
true |
Enable price watcher |
ENABLE_TRENDING_WATCHER |
true |
Enable trending watcher |
SSE_HEARTBEAT_MS |
15000 |
SSE heartbeat interval (min 5000) |
SALES_WATCH_INTERVAL_MS |
30000 |
Sales watcher interval |
PRICE_WATCH_INTERVAL_MS |
30000 |
Price watcher interval |
ITEM_WATCH_INTERVAL_MS |
30000 |
Item watcher interval |
ITEM_WATCH_TOP |
150 |
Items per page scanned in item watcher |
ITEM_WATCH_PAGES |
3 |
Pages scanned per cycle in item watcher |
TRENDING_INTERVAL_MS |
60000 |
Trending watcher interval |
TRENDING_WINDOW_HOURS |
24 |
Window for trending scoring |
TRENDING_PAGE_TOP |
200 |
Items per page scanned in trending watcher |
TRENDING_PAGES |
3 |
Pages scanned per cycle in trending watcher |
TRENDING_TOP_N |
20 |
Top creators emitted per trending window |
STORE_CONCURRENCY |
6 |
Parallel store requests |
PRICE_WATCH_MAX_STORES |
50 |
Max stores scanned for price signature |
| Variable | Default | Description |
|---|---|---|
MAX_SEARCH_BATCHES |
10 |
Batches for paginated search |
MAX_FETCH_BATCHES |
20 |
Batches for “all items” fetch |
ADV_SEARCH_BATCH |
300 |
Batch size advanced search |
ADV_SEARCH_MAX_BATCHES |
10 |
Max batches advanced search |
MULTILANG_ALL |
true |
Enrich via GetItems for all results |
MULTILANG_ENRICH_BATCH |
100 |
GetItems batch size |
MULTILANG_ENRICH_CONCURRENCY |
5 |
GetItems concurrency |
STORE_MAX_FOR_PRICE_ENRICH |
500 |
Stores consulted per item (prices) |
| Variable | Default | Description |
|---|---|---|
REVIEWS_ENABLED |
true |
Enable review enrichment in item details |
REVIEWS_FETCH_COUNT |
20 |
Number of reviews to fetch per item (sample) |
| Variable | Default | Description |
|---|---|---|
WEBHOOK_TIMEOUT_MS |
5000 |
Upstream webhook timeout (ms) |
WEBHOOK_MAX_RETRIES |
3 |
Max webhook delivery retries |
WEBHOOK_CONCURRENCY |
4 |
Parallel webhook deliveries |
| Variable | Default | Description |
|---|---|---|
RATE_LIMIT_ENABLED |
true |
Toggle rate limiting globally |
RATE_LIMIT_DEFAULT_WINDOW_MS |
60000 |
Default window for generic routes |
RATE_LIMIT_DEFAULT_MAX |
60 |
Default max requests per window |
RATE_LIMIT_LOGIN_WINDOW_MS |
900000 |
/login window (15 minutes) |
RATE_LIMIT_LOGIN_MAX |
20 |
Max login attempts per window |
RATE_LIMIT_MARKETPLACE_WINDOW_MS |
60000 |
Window for /marketplace routes |
RATE_LIMIT_MARKETPLACE_MAX |
120 |
Max /marketplace requests per window |
RATE_LIMIT_EVENTS_WINDOW_MS |
60000 |
Window for /events endpoints |
RATE_LIMIT_EVENTS_MAX |
120 |
Max /events requests per window |
RATE_LIMIT_ADMIN_WINDOW_MS |
60000 |
Window for /admin routes |
RATE_LIMIT_ADMIN_MAX |
60 |
Max /admin requests per window |
RATE_LIMIT_HEALTH_WINDOW_MS |
10000 |
Window for health endpoints |
RATE_LIMIT_HEALTH_MAX |
120 |
Max health checks per window |
This service uses small JSON files under src/data/ which you should persist in production (bind mount / volume):
-
titles.json— alias → TitleId mapping. Example:{ "production": { "id": "20CA2", "notes": "Production title" }, "staging": { "id": "AB123", "notes": "Staging" } } -
creators.json— creator registry used for search resolution:[ { "id": "creator-uuid", "creatorName": "SomeCreator", "displayName": "Some Creator" } ] -
webhooks.json— persisted webhook registrations (auto-managed).
If these files are missing, the API will warn (e.g., creators) or initialize to empty structures.
Client ──► /login ──► JWT ─┐
│ ┌───────────────┐
(Bearer) ───────────────────┼──────► │ Express API │
│ │ • Routes │
│ │ • Middleware │
│ └─────┬─────────┘
│ │
│ ▼
│ ┌───────────────┐
│ │ Services │
│ │ • marketplace│
│ │ • advanced │
│ │ • watchers │
│ └─────┬─────────┘
│ │
│ ▼
│ ┌───────────────┐
│ │ utils/playfab │──► PlayFab API
│ │ (Axios, Retry │
│ │ Keep-Alive) │
│ └───────────────┘
│
SSE ◄────────────────────────── EventBus + Watchers (sales/items/prices/trending)
Webhooks ◄───────────────────── WebhookService (signature, retry/backoff)
Cache ◄─────────────────────── LRU (sessions/data) + getOrSetAsync de-dup
Highlights
- Central
sendPlayFabRequest()adds retries, jittered backoff, and 429 handling usingRetry-Afterwhen present. - LRU session cache ensures minimal auth churn (
SessionTicket+EntityToken). - ETag middleware serializes handler results and computes weak ETags (
W/"<hex>-<sha1-16>").
-
Auth: All routes require
Authorization: Bearer <jwt>except/login,/openapi.json, and/docs(when enabled). -
Roles:
role=adminis required for:GET /session/:aliasPOST /admin/webhooks
-
Input validation:
express-validatoron most routes. -
Helmet: baseline HTTP protection; CSP disabled for Swagger UI compatibility.
-
CORS: disabled by default; enable via
CORS_ORIGINS(comma-separated).
POST /login
Body:
{ "username": "<ADMIN_USER>", "password": "<ADMIN_PASS>" }Response:
{ "token": "<jwt>" }The token encodes { role: "admin" } for admin credentials. Use it via Authorization: Bearer <jwt>.
-
Content-Type: application/json; charset=utf-8 -
ETags for GETs; send
If-None-Matchto leverage304 Not Modified. -
Pagination opt-in:
page(≥1),pageSize(1..100)- or
skipandlimit(1..1000)
-
Response pagination headers:
X-Total-CountContent-Range: items <start>-<end>/<total>
| Method | Path | Description | Auth |
|---|---|---|---|
| GET | / |
Service banner/health | ✅ |
| GET | /openapi.json |
OpenAPI spec | ❌ |
| GET | /docs |
Swagger UI (when enabled) | ❌ |
| Method | Path | Description | Role |
|---|---|---|---|
| POST | /login |
Issue JWT | — |
| GET | /session/:alias |
Show PlayFab session | admin |
| POST | /admin/webhooks |
Register webhooks | admin |
| Method | Path | Description |
|---|---|---|
| GET | /titles |
All alias→TitleId entries |
| POST | /titles |
Create alias { alias, id, notes? } |
| DELETE | /titles/:alias |
Remove alias |
| GET | /creators |
List creatorName, displayName |
| Method | Path | Description |
|---|---|---|
| GET | /marketplace/all/:alias |
Entire catalog (optionally ?tag=...) |
| GET | /marketplace/latest/:alias |
Latest items (?count<=50) |
| GET | /marketplace/popular/:alias |
Popular by rating/totalcount |
| GET | /marketplace/free/:alias |
Free items |
| GET | /marketplace/tag/:alias/:tag |
Filter by tag |
| GET | /marketplace/search/:alias |
Creator + keyword search |
| GET | /marketplace/details/:alias/:itemId |
Item details (optional enrichments) |
| GET | /marketplace/friendly/:alias/:friendlyId |
Resolve by FriendlyId |
| GET | /marketplace/resolve/:alias/:itemId |
Resolve by ItemId (with references) |
| GET | /marketplace/resolve/friendly/:alias/:friendlyId |
Resolve FriendlyId → item (+refs) |
| GET | /marketplace/summary/:alias |
Compact list (id/title/links) |
| GET | /marketplace/compare/:creatorName |
Compare a creator across titles |
| GET | /marketplace/featured-servers |
Curated featured servers |
| GET | /marketplace/sales |
Aggregated sales across aliases |
| GET | /marketplace/sales/:alias |
Aggregated sales for one alias |
| POST | /marketplace/search-advanced/:alias |
Advanced search + facets |
Below, TOKEN refers to the JWT from /login.
Returns all known aliases with notes.
curl -s "http://localhost:3000/titles" -H "Authorization: Bearer $TOKEN"Create/update an alias → TitleId.
curl -s -X POST "http://localhost:3000/titles" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "alias":"my-title", "id":"20CA2", "notes":"Production" }'Remove an alias mapping.
Paginated list of creators (from src/data/creators.json).
All catalog items for an alias, optional ?tag=.... Supports pagination.
Latest items (max 50).
Sorted by rating/totalcount desc.
Items with displayProperties/price = 0.
Filter by tag; fully enriched items (optionally with references).
Creator is resolved via creators.json (matches creatorName or displayName, whitespace-insensitive).
prices: fetch store prices for the item (bounded bySTORE_MAX_FOR_PRICE_ENRICH).reviews: includeGetItemReviewSummary+GetItemReviewssample (REVIEWS_FETCH_COUNT).refs: resolveItemReferencesto full items and return underResolvedReferences.
Get a single item by its FriendlyId (via a search), then fully resolve + references.
Resolve an item by Id; will include ResolvedReferences.
Same as friendly, explicit route for clarity.
Compact list for link rendering:
{ "id": "ItemId", "title": "Neutral Title", "detailsUrl": "...", "clientUrl": "..." }Compare one creator across all configured titles in titles.json. Returns { [alias]: items[] }.
Returns a curated list from src/config/featuredServers.js, each resolved to a live item (if present).
Aggregate store sales (from SearchStores + GetStoreItems) with items resolved. Optional ?creator=<displayName> filters to a single creator. Returns:
{
"totalItems": 123,
"itemsPerCreator": { "Creator A": 10, "Creator B": 5 },
"sales": {
"storeId": {
"id": "storeId",
"title": "Sale Title",
"discountPercent": 30,
"startDate": "2024-10-01T00:00:00Z",
"endDate": "2024-10-08T00:00:00Z",
"items": [ { "id": "...", "rawItem": { /* full item */ } } ]
}
}
}Body supports query + filters + sorting; response includes facets.
{
"query": "castle",
"filters": {
"tags": ["adventure","survival"],
"creatorIds": ["uuid-1","uuid-2"],
"creatorName": "SomeCreator",
"priceMin": 0,
"priceMax": 1990,
"createdFrom": "2024-01-01T00:00:00Z",
"createdTo": "2025-01-01T00:00:00Z",
"contentTypes": ["bundle","skinpack"]
},
"sort": [{ "field": "creationDate", "dir": "desc" }]
}/events/items/stream:item.snapshot,item.created,item.updated/events/sales/stream:sale.snapshot,sale.update/events/prices/stream:price.changed/events/trending/stream:creator.trending
POST /admin/webhooks— register{ event, url, secret? }
Login
curl -sS -X POST http://localhost:3000/login \
-H "Content-Type: application/json" \
-d '{"username":"'"$ADMIN_USER"'","password":"'"$ADMIN_PASS"'"}'Featured servers
TOKEN="<jwt>"
curl -sS "http://localhost:3000/marketplace/featured-servers" \
-H "Authorization: Bearer $TOKEN"Compare a creator across titles
curl -sS "http://localhost:3000/marketplace/compare/SomeCreator" \
-H "Authorization: Bearer $TOKEN"Resolve by FriendlyId
curl -sS "http://localhost:3000/marketplace/resolve/friendly/my-alias/FRIENDLY_ID" \
-H "Authorization: Bearer $TOKEN"Sales (filtered by creator display name)
curl -sS "http://localhost:3000/marketplace/sales?creator=Some%20Creator" \
-H "Authorization: Bearer $TOKEN"SSE subscription (Node)
import EventSource from "eventsource";
const es = new EventSource("http://localhost:3000/events/trending/stream", {
headers: { Authorization: `Bearer ${token}` }
});
es.addEventListener("creator.trending", e => console.log(JSON.parse(e.data)));
es.addEventListener("ping", () => {});-
Opt-in pagination: pass
page/pageSizeorskip/limit. -
Headers:
X-Total-CountContent-Range: items <start>-<end>/<total>
-
ETag: Responses wrapped by
withETag()set a weak ETag; ifIf-None-Matchmatches the current tag, the server returns 304 without a body. -
Cache-Control on most GET routes:
- Example:
public, max-age=60, s-maxage=300, stale-while-revalidate=600
- Example:
{
"error": {
"type": "bad_request | unauthorized | forbidden | not_found | internal_error",
"message": "Human readable message",
"details": [ /* express-validator issues, optional */ ],
"traceId": "request correlation id"
}
}- The
traceIdmirrorsX-Request-Idif provided, else a short random id. - 4xx/5xx are normalized; in production 5xx messages become
Internal server error.
Rate limiting is controlled via the RATE_LIMIT_* environment variables.
-
When
RATE_LIMIT_ENABLED=true:/loginuses anexpress-rate-limitinstance that, by default, allows 20 requests per 15 minutes (configurable viaRATE_LIMIT_LOGIN_WINDOW_MSandRATE_LIMIT_LOGIN_MAX).- Additional per-group limiters apply to marketplace, events, admin and health endpoints, using their respective
RATE_LIMIT_*envs.
-
Violations return
429 Too Many Requestswith a short JSON error.
-
Each stream sets required headers, flushes, and pings every
SSE_HEARTBEAT_MS(min 5000). -
Reconnect strategy: clients should auto-reconnect; no
Last-Event-IDis used. -
Backpressure: events are small JSON payloads; consumers should be idempotent.
-
Watchers are toggled with:
ENABLE_ITEM_WATCHER,ENABLE_PRICE_WATCHER,ENABLE_SALES_WATCHER,ENABLE_TRENDING_WATCHER.
Register
curl -sS -X POST http://localhost:3000/admin/webhooks \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"event":"price.changed","url":"https://example.com/hook","secret":"<optional-256-max>"}'Events: sale.update, item.snapshot, item.created, item.updated, price.changed, creator.trending
Delivery
- JSON body:
{ "event": "<name>", "ts": 1730000000000, "payload": { ... } } - Header:
X-Webhook-Signature: <sha1>where the value issha1(stableStringify({ body, secret })) - Retries: up to
WEBHOOK_MAX_RETRIESwith exponential backoff + jitter - Concurrency:
WEBHOOK_CONCURRENCY(default 4) - Timeout per delivery:
WEBHOOK_TIMEOUT_MS(default 5000 ms)
Verify signature (Node)
import crypto from "crypto";
function stableStringify(obj){ if(obj===null||typeof obj!=="object") return JSON.stringify(obj);
if(Array.isArray(obj)) return "["+obj.map(stableStringify).join(",")+"]";
const keys=Object.keys(obj).sort(); return "{"+keys.map(k=>JSON.stringify(k)+":"+stableStringify(obj[k])).join(",")+"}";
}
function verify(sig, body, secret){
const expect = crypto.createHash("sha1").update(stableStringify({ body, secret })).digest("hex");
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expect));
}Verify signature (Python)
import hashlib, hmac, json
def stable(o):
if isinstance(o, dict):
return "{" + ",".join(f"{json.dumps(k)}:{stable(o[k])}" for k in sorted(o)) + "}"
if isinstance(o, list):
return "[" + ",".join(stable(x) for x in o) + "]"
return json.dumps(o)
def verify(sig, body, secret):
expect = hashlib.sha1(stable({"body": body, "secret": secret}).encode()).hexdigest()
return hmac.compare_digest(sig, expect)- Spec:
GET /openapi.json(always available) - Swagger UI:
GET /docs(whenENABLE_DOCS=true)
Spec composition
- Base:
src/docs/openapi-base.yaml - Schemas:
src/docs/schemas/**/*.yaml - Paths:
src/docs/paths/**/*.yaml - Builder:
config/swagger.jsmerges all YAML parts
Validator
express-openapi-validatorplugged whenVALIDATE_REQUESTS=true(and optionallyVALIDATE_RESPONSES=true).- Custom BearerAuth handler verifies JWT or throws standardized 401/403 early.
- Winston logger with colorized console (chalk).
middleware/requestLoggerprints→and←lines atdebuglevel including latency.- Upstream calls log store counts, discount percents, item totals under
debugto aid diagnosis.
- LRU (sessionCache) — PlayFab login session (
SessionTicket,EntityToken), soft TTL with refresh viagetSession(). - LRU (dataCache) — Generic results via
getOrSetAsync(key, fn, ttlOverride?)to deduplicate in-flight calls. - ETag — Response entity tagging on controllers using
withETag(handler). - Cache headers — Per-route
Cache-Controlhints viacacheHeaders(seconds, smax)inindex.js.
Items are normalized by utils/playfab.transformItem():
type TransformedImage = {
Id: string;
Tag: string;
Type: "thumbnail" | "screenshot";
Url: string;
};
type TransformedItem = {
Id: string;
Title: Record<string,string>;
Description?: Record<string,string>;
ContentType?: string;
Tags?: string[];
Platforms?: string[];
Images: TransformedImage[]; // thumbnails first
StartDate?: string; // normalized from start/creation date
DisplayProperties?: any;
ResolvedReferences?: TransformedItem[]; // when expanded
StorePrices?: Array<{ storeId: string; storeTitle: any; amounts: Array<{ currencyId: string; amount: number }> }>;
Reviews?: { summary: any; reviews: any[] };
};src/
config/ # caches, logger, rate limiter, swagger builder
controllers/ # http handlers (marketplace, events, admin, etc.)
middleware/ # etag, pagination, validators, request logging, sse heartbeat
routes/ # express routers per domain
services/ # business logic, watchers, webhook service, event bus
utils/ # playfab client, titles/creators db, filter, pagination, hashing, storage
scripts/ # OpenAPI ref fixer
index.js # Express bootstrap
Normalize schema refs / nullable patterns across docs/:
node src/scripts/fix-openapi-refs.jsVALIDATE_REQUESTS=true ENABLE_DOCS=true node src/index.jsLOG_LEVEL=debug node src/index.jsPORT=3000
NODE_ENV=production
JWT_SECRET=please-change-me-to-a-very-long-random-string-32plus
ADMIN_USER=admin
ADMIN_PASS=change-me
DEFAULT_ALIAS=prod
TITLE_ID=20CA2
OS=iOS
TRUST_PROXY=1
LOG_LEVEL=info
CORS_ORIGINS=
HTTP_MAX_SOCKETS=512
HTTPS_MAX_SOCKETS=512
UPSTREAM_TIMEOUT_MS=20000
RETRY_BUDGET=3
SESSION_TTL_MS=1800000
SESSION_CACHE_MAX=1000
DATA_CACHE_MAX=20000
DATA_TTL_MS=300000
FEATURED_PRIMARY_ALIAS=prod
MULTILANG_ALL=true
MULTILANG_ENRICH_BATCH=100
MULTILANG_ENRICH_CONCURRENCY=5
STORE_CONCURRENCY=6
STORE_MAX_FOR_PRICE_ENRICH=500
VALIDATE_REQUESTS=false
VALIDATE_RESPONSES=false
ENABLE_DOCS=false
PAGE_SIZE=100
REVIEWS_ENABLED=true
REVIEWS_FETCH_COUNT=20
ENABLE_SALES_WATCHER=true
SALES_WATCH_INTERVAL_MS=30000
ENABLE_ITEM_WATCHER=true
ITEM_WATCH_INTERVAL_MS=30000
ITEM_WATCH_TOP=150
ITEM_WATCH_PAGES=3
ENABLE_PRICE_WATCHER=true
PRICE_WATCH_INTERVAL_MS=30000
PRICE_WATCH_MAX_STORES=50
ENABLE_TRENDING_WATCHER=true
TRENDING_INTERVAL_MS=60000
TRENDING_WINDOW_HOURS=24
TRENDING_PAGE_TOP=200
TRENDING_PAGES=3
TRENDING_TOP_N=20
SSE_HEARTBEAT_MS=15000
ADV_SEARCH_TTL_MS=60000
ADV_SEARCH_BATCH=300
ADV_SEARCH_MAX_BATCHES=10
WEBHOOK_TIMEOUT_MS=5000
WEBHOOK_MAX_RETRIES=3
WEBHOOK_CONCURRENCY=4
MAX_SEARCH_BATCHES=10
MAX_FETCH_BATCHES=20
PLAYFAB_CONCURRENCY=12
PLAYFAB_BATCH=600
RATE_LIMIT_ENABLED=true
RATE_LIMIT_DEFAULT_WINDOW_MS=60000
RATE_LIMIT_DEFAULT_MAX=60
RATE_LIMIT_LOGIN_WINDOW_MS=900000
RATE_LIMIT_LOGIN_MAX=20
RATE_LIMIT_MARKETPLACE_WINDOW_MS=60000
RATE_LIMIT_MARKETPLACE_MAX=120
RATE_LIMIT_EVENTS_WINDOW_MS=60000
RATE_LIMIT_EVENTS_MAX=120
RATE_LIMIT_ADMIN_WINDOW_MS=60000
RATE_LIMIT_ADMIN_MAX=60
RATE_LIMIT_HEALTH_WINDOW_MS=10000
RATE_LIMIT_HEALTH_MAX=120- Unit: isolate pure helpers (
utils/pagination,utils/hash,services/advancedSearchService). - Contract: with
VALIDATE_REQUESTS=true, focus on validator test cases and unified error shapes. - SSE: simulate via
eventsourceand assert event names/payload shapes. - Webhooks: spin up a local receiver and validate signature & retry semantics.
- Increase
HTTP_MAX_SOCKETS/HTTPS_MAX_SOCKETSfor higher parallelism. - Adjust
STORE_CONCURRENCY,ADV_SEARCH_BATCH, and watcher intervals to respect upstream quotas. - Use CDN in front; the API already sends ETags and cache directives.
- Prefer
page/pageSizefor stable pagination;skip/limitis supported for flexibility. - Enable
LOG_LEVEL=debugtemporarily to inspect store/item volumes and discounts.
Dockerfile (example)
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node","src/index.js"]version: "3.8"
services:
api:
build: .
ports: ["3000:3000"]
environment:
PORT: 3000
JWT_SECRET: ${JWT_SECRET}
ADMIN_USER: ${ADMIN_USER}
ADMIN_PASS: ${ADMIN_PASS}
ENABLE_DOCS: "true"
LOG_LEVEL: "info"
volumes:
- ./src/data:/app/src/dataapiVersion: apps/v1
kind: Deployment
metadata: { name: playfab-catalog-api }
spec:
replicas: 2
selector: { matchLabels: { app: playfab-catalog-api } }
template:
metadata: { labels: { app: playfab-catalog-api } }
spec:
containers:
- name: api
image: ghcr.io/org/playfab-catalog-api:latest
ports: [{ containerPort: 3000 }]
envFrom: [{ secretRef: { name: api-secrets } }]
readinessProbe: { httpGet: { path: "/", port: 3000 }, initialDelaySeconds: 5, periodSeconds: 10 }
livenessProbe: { httpGet: { path: "/", port: 3000 }, initialDelaySeconds: 10, periodSeconds: 20 }
---
apiVersion: v1
kind: Service
metadata: { name: playfab-catalog-api }
spec:
selector: { app: playfab-catalog-api }
ports: [{ port: 80, targetPort: 3000 }]server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s; # allow SSE
}
}- Traceability: include
X-Request-Idon requests; error payloads echotraceId. - SSE stability: ensure proxy timeouts ≥ heartbeat; enable TCP keep-alive.
- Disk: persist
src/data/*.jsonto retain titles, creators, and webhook registrations. - Metrics: wrap winston logs with your log shipping/metrics pipeline.
-
401/403: Missing/invalid JWT or insufficient role.
-
404:
Alias '<alias>' not found.→ add totitles.json.Creator '<name>' not found.→ add to/verifycreators.json.- Item not found → ensure correct alias/title and item id/friendly id.
-
Empty sales: no stores returned or store items not resolvable; turn on
LOG_LEVEL=debug. -
429/5xx upstream: the client already retries with jitter; consider lowering concurrency/intervals.
-
SSE disconnects: check proxy idle timeout; heartbeats are at least 5s.
How do I map aliases to TitleIds?
Use POST /titles or edit src/data/titles.json and restart (hot reload reads mtime).
What’s the difference between resolve and friendly routes?
Both end up returning a fully enriched item; friendly uses a search by alternateIds first.
Can I get facets for tags/creators?
Yes — use advanced search; the response adds facets.tags, facets.creators, facets.contentTypes, and price buckets.
Are images always present?
Items are filtered by isValidItem() to ensure at least one image; thumbnails are prioritized in Images.
- Fork and create a feature branch.
- Add tests for your logic where applicable.
- Keep code style consistent; prefer small, readable modules.
- Update README/OpenAPI when you change public behavior.
- Open a PR with a clear description and screenshots/logs for UX/API changes.
This project integrates third-party services (PlayFab). Ensure compliance with your internal policies and the provider’s terms of use.