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.
Important
Project Disclaimer
"PlayFab Catalog Service Bedrock" by SpindexGFX is an independent project. It is not affiliated with, endorsed by, sponsored by, or otherwise connected to Mojang AB, Microsoft Corporation, or any of their subsidiaries or affiliates. No partnership, approval, or official relationship with Mojang AB or Microsoft is implied.
All names, logos, brands, trademarks, service marks, and registered trademarks are the property of their respective owners and are used strictly for identification and reference purposes only. This project does not claim ownership of third-party intellectual property and does not grant any license to use it.
- 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, featured content updates, 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,featured.content.added,featured.content.removed,featured.content.updated. - 🔔 Webhooks with HMAC signature (
sha256=<hex>over JSON body) 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
Warning
This service can generate sustained upstream traffic through watchers, enrichment, retries, and webhook delivery. Before using it against a public or production-like environment, review polling intervals, concurrency limits, cache settings, and timeout values to avoid unnecessary load on upstream services.
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 |
http://localhost:5173,https://view-marketplace.net |
Comma-separated list of allowed browser origins |
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 |
|---|---|---|
UPDATE_CHECK_ENABLED |
true |
Enable GitHub-backed version checks for GET /version |
UPDATE_CHECK_REPOSITORY |
Git remote/package repository | GitHub repository in owner/name or GitHub URL format |
UPDATE_CHECK_TTL_MS |
900000 |
Cache TTL for GitHub version metadata |
UPDATE_CHECK_TIMEOUT_MS |
5000 |
GitHub API request timeout |
UPDATE_CHECK_STARTUP_LOG |
true |
Log the public GitHub update status after server startup |
When UPDATE_CHECK_STARTUP_LOG=true, the listener is opened first and the banner is printed with a version block above the attribution line. The checker uses the public GitHub API without a token. Local SemVer Git tags at the current commit are preferred over package.json; if no local tag is available, a matching local commit and latest GitHub release/tag commit still counts as current.
VERSION CHECK Update available
Version Current 1.0.0 (package.json) -> Latest 1.1.0 (GitHub release, ok, 900s cache)
Commit Local b2b951b3bdbe on main -> Remote 2f1c4d8a91bd latest ref
Repo Daniel-Ric/PlayFab-Catalog-Service-Bedrock
Release https://github.com/Daniel-Ric/PlayFab-Catalog-Service-Bedrock/releases/tag/v1.1.0
The Catalog Bridge is disabled by default and adds no routes, handshakes, CORS rules, CSRF checks, or rate limiters unless CATALOG_BRIDGE_ENABLED=true.
| Variable | Default | Description |
|---|---|---|
CATALOG_BRIDGE_ENABLED |
false |
Enable optional /api/... bridge routes |
CATALOG_BRIDGE_PROXY_ENABLED |
follows bridge | Enable POST /api/catalog/proxy |
CATALOG_BRIDGE_SECURE_ENABLED |
follows bridge | Enable handshake and POST /api/catalog/secure |
CATALOG_BRIDGE_CSRF_ENABLED |
follows bridge | Require CSRF for plain proxy and secure payloads |
CATALOG_BRIDGE_CORS_ENABLED |
false |
Add bridge-specific CORS headers |
CATALOG_BRIDGE_RATE_LIMIT_ENABLED |
follows bridge | Add bridge-specific rate limiting |
ALLOWED_WEB_ORIGINS |
empty | Comma-separated origins for bridge CORS |
CATALOG_UPSTREAM_ORIGIN |
empty | Upstream origin used for relative /catalog/... proxy targets |
CATALOG_BEARER_TOKEN |
empty | Server-side bearer token injected into upstream catalog requests |
CSRF_COOKIE_NAME |
catalog_bridge_csrf |
CSRF cookie name |
SESSION_COOKIE_NAME |
catalog_bridge_session |
Reserved bridge session cookie name if deployments need one |
CATALOG_BRIDGE_COOKIE_SAMESITE |
Lax |
CSRF cookie SameSite value |
CATALOG_BRIDGE_COOKIE_SECURE |
true in production |
Add Secure to bridge cookies |
CATALOG_BRIDGE_COOKIE_TTL_MS |
1800000 |
CSRF cookie TTL |
CATALOG_BRIDGE_HANDSHAKE_TTL_MS |
120000 |
ECDH handshake TTL |
CATALOG_BRIDGE_RATE_LIMIT_WINDOW_MS |
60000 |
Bridge rate-limit window |
CATALOG_BRIDGE_RATE_LIMIT_MAX |
120 |
Bridge rate-limit max requests per window |
CATALOG_BRIDGE_CORS_METHODS |
GET,POST,OPTIONS |
Bridge CORS methods |
CATALOG_BRIDGE_CORS_ALLOWED_HEADERS |
Content-Type,X-CSRF-Token |
Bridge CORS request headers |
CATALOG_BRIDGE_CORS_CREDENTIALS |
true |
Add bridge CORS credentials support |
CATALOG_BRIDGE_UPSTREAM_TIMEOUT_MS |
60000 |
Bridge upstream timeout; independent from the PlayFab timeout |
CATALOG_BRIDGE_MAX_BODY_BYTES |
1048576 |
Bridge JSON body limit |
POST /api/catalog/proxy accepts only relative url values under /catalog/, blocks /catalog/login, ignores client-supplied Authorization, and injects the configured server-side bearer token. GET /api/security/catalog-handshake creates short-lived P-256 ECDH handshakes for POST /api/catalog/secure; the encrypted payload is the same proxy payload plus the handshake csrfToken when CSRF is enabled.
If CATALOG_BEARER_TOKEN should authenticate against this API's JWT middleware, generate a deployment token locally and store it only in environment/secret storage:
CATALOG_BRIDGE_TOKEN_EXPIRES_IN=365d npm run bridge:tokenOptional generator inputs are CATALOG_BRIDGE_TOKEN_SUB, CATALOG_BRIDGE_TOKEN_ROLE, and CATALOG_BRIDGE_TOKEN_EXPIRES_IN. Rotate this token like any other server-side credential.
| 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_DEVICE_ID |
generated | Stable anonymous PlayFab device identity |
PLAYFAB_CONCURRENCY |
12 |
Parallel PlayFab requests |
PLAYFAB_BATCH |
600 |
Max batch size for bulk PlayFab calls |
npm run setup generates PLAYFAB_DEVICE_ID once and preserves it on later setup runs. Keep the value stable per installation and outside version control so PlayFab reuses the linked anonymous account. Without it, the service uses a process-local fallback that changes after a restart.
| Variable | Default | Description |
|---|---|---|
SESSION_TTL_MS |
1800000 |
Fallback TTL when PlayFab omits token expiration |
SESSION_EXPIRY_SKEW_MS |
60000 |
Refresh token this long before expiration |
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 |
ENABLE_CREATOR_PARTNER_WATCHER |
true |
Enable creator/partner registry watcher |
ENABLE_FEATURED_CONTENT_WATCHER |
true |
Enable featured content watcher |
ENABLE_SUBSCRIPTION_WATCHER |
false |
Enable Marketplace Pass / Realms Plus membership 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_CREATED_LOOKBACK_MS |
86400000 |
StartDate/CreationDate window for newly visible catalog items |
FEATURED_CONTENT_WATCH_INTERVAL_MS |
21600000 |
Featured content watcher interval |
SUBSCRIPTION_WATCH_INTERVAL_MS |
300000 |
Marketplace Pass / Realms Plus 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 |
CREATOR_PARTNER_WATCH_INTERVAL_MS |
21600000 |
Creator/partner registry watcher interval |
CREATORNAME_MODE |
nospace |
Creator registry normalization mode (nospace or alnum) |
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 |
FETCH_CONCURRENCY |
5 |
Parallel catalog search pages |
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 |
ADV_SEARCH_CONCURRENCY |
5 |
Parallel advanced search pages |
MARKETPLACE_SEARCH_ITEMS_TTL_MS |
30000 |
Cache TTL for native SearchItems responses |
MARKETPLACE_SEARCH_SUGGEST_TTL_MS |
30000 |
Cache TTL for suggestions |
MARKETPLACE_SEARCH_AUDIT_TTL_MS |
60000 |
Cache TTL for search health audits |
MULTILANG_ALL |
true |
Enrich via GetItems for all results |
MULTILANG_ENRICH_BATCH |
100 |
Requested GetItems batch size; PlayFab calls are capped at 50 IDs |
MULTILANG_ENRICH_CONCURRENCY |
5 |
GetItems concurrency |
STORE_MAX_FOR_PRICE_ENRICH |
500 |
Stores consulted per item (prices) |
STORE_PRICE_TTL_MS |
120000 |
Computed item store-price cache TTL (ms) |
| 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 |
RATE_LIMIT_VERSION_WINDOW_MS |
300000 |
Window for /version checks |
RATE_LIMIT_VERSION_MAX |
200 |
Max /version requests 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/creator partners)
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,/docs(when enabled), andGET /marketplace/mc-token. -
Roles:
role=adminis required for:GET /session/:aliasPOST /webhooks
-
Input validation:
express-validatoron most routes. -
Helmet: baseline HTTP protection; CSP disabled for Swagger UI compatibility.
-
CORS: enabled for
http://localhost:5173andhttps://view-marketplace.netby default; override viaCORS_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>.
GET /marketplace/mc-token
Returns a Minecraft Services token for the configured primary title (FEATURED_PRIMARY_ALIAS → DEFAULT_ALIAS → TITLE_ID). !!!No Auth Token required!!!.
Response:
{ "titleId": "20CA2", "token": "<mc-token>" }-
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 | /health |
Runtime health report | ✅ |
| GET | /version |
GitHub update status | ✅ |
| 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 | /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/marketplace-pass/:alias |
Marketplace Pass items (csb) |
| GET | /marketplace/realms-plus/:alias |
Realms Plus items (realms_plus) |
| GET | /marketplace/tag/:alias/:tag |
Filter by tag |
| GET | /marketplace/search/:alias |
Keyword full-text search |
| POST | /marketplace/search/items/:alias |
Native SearchItems with cursor pagination |
| POST | /marketplace/search/store/:alias |
Store-scoped SearchItems |
| GET | /marketplace/search/suggest/:alias |
Lightweight search suggestions |
| POST | /marketplace/search/localized/:alias |
Multi-language search comparison |
| POST | /marketplace/search/audit/:alias |
Search-based content health scan |
| 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) |
| POST | /marketplace/resolve/batch/:alias |
Resolve up to 50 ids/alternateIds |
| 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/featured-content |
Featured persona items |
| GET | /marketplace/mc-token |
MC token for primary title |
| GET | /marketplace/sales |
Aggregated sales across aliases |
| GET | /marketplace/sales/:alias |
Aggregated sales for one alias |
| POST | /marketplace/search/advanced/:alias |
Advanced search + facets or cursor mode |
Below, TOKEN refers to the JWT from /login.
Returns the detected local version, local Git metadata, configured GitHub repository, latest release or tag metadata, and whether an update is available. Local SemVer Git tags are preferred over package.json; when the deployed commit matches the latest GitHub release/tag commit, the response reports the service as current even if package.json still contains an older fallback version.
curl -s "http://localhost:3000/version" -H "Authorization: Bearer $TOKEN"Optional query parameters:
refresh=true: bypass the local update-check cache and revalidate against GitHub.includePrerelease=true: include GitHub prereleases when checking releases.source=auto|release|tag: choose the remote source.autochecks the latest release and falls back to tags when no release exists.
Example response:
{
"versionCheckerVersion": "v1",
"local": {
"name": "playfab-bedrock-catalog",
"version": "1.0.0",
"git": { "branch": "main", "shortCommit": "abc123def456" }
},
"repository": {
"fullName": "Daniel-Ric/PlayFab-Catalog-Service-Bedrock",
"htmlUrl": "https://github.com/Daniel-Ric/PlayFab-Catalog-Service-Bedrock"
},
"remote": {
"status": "ok",
"source": "release",
"latest": { "version": "1.1.0", "tagName": "v1.1.0" }
},
"update": {
"available": true,
"status": "outdated",
"current": "1.0.0",
"latest": "1.1.0"
}
}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 server-side pagination. ResolvedReferences are only included with ?expand=refs.
Latest items (max 50).
Sorted by rating/totalcount desc.
Items with displayProperties/price = 0.
Items with the csb tag. Supports creator, date, pagination, and orderBy filters.
Items with the realms_plus tag. Supports creator, date, pagination, and orderBy filters.
Filter by tag; fully enriched items (optionally with references).
Runs a PlayFab Catalog full-text search across the selected title alias. creatorName=<name> is optional and, when present, is resolved via creators.json (matches creatorName or displayName, whitespace-insensitive).
Native Catalog/SearchItems wrapper with cursor pagination. Use this for efficient frontend search when you need PlayFab Language, Select, Store, Count, and ContinuationToken support.
{
"search": "dragon",
"filter": "ContentType eq '3PServerContent_V1.2'",
"orderBy": "StartDate desc",
"select": "title,description,images,priceOptions",
"language": "de-DE",
"count": 24,
"continuationToken": ""
}Response shape:
{
"items": [{ "id": "ItemId", "title": "Title", "thumbnail": "https://...", "price": { "amount": 660, "currencyId": "Minecoins" } }],
"pagination": { "count": 24, "requestedCount": 24, "continuationToken": "...", "hasNext": true },
"meta": { "source": "playfab.catalog.searchItems", "language": "de-DE" }
}Same response shape as /marketplace/search/items/:alias, but SearchItems runs in a PlayFab store context. Provide one of:
{ "storeId": "store-id", "search": "", "count": 24 }or:
{ "storeAlternateId": { "type": "FriendlyId", "value": "store-friendly-id" }, "count": 24 }Returns lightweight item, creator, and keyword suggestions. Optional query params: language, filter, count (1..20).
Runs the same SearchItems request for multiple languages and returns one result bucket per language:
{
"search": "dragon",
"languages": ["de-DE", "en-US"],
"select": "title,description,keywords,images",
"count": 10
}Search-based content health scan. It checks the returned SearchItems pages for missing thumbnails, screenshots, contents, platforms, deep links, requested localization texts, malformed URLs, and invalid min/max client version ranges.
{
"filter": "ContentType eq '3PServerContent_V1.2'",
"languages": ["de-DE", "en-US"],
"count": 50,
"maxPages": 3
}Resolves up to 50 item IDs or alternate IDs with Catalog/GetItems in one request:
{
"ids": ["item-id-1", "item-id-2"],
"alternateIds": [{ "type": "FriendlyId", "value": "friendly-id" }]
}Search endpoints accept contentType=<PlayFab ContentType> for exact PlayFab ContentType matches, for example contentType=3PServerContent_V1.2.
Item list/search endpoints also accept ISO date ranges for PlayFab catalog timestamps: creationDateFrom/creationDateTo, lastModifiedDateFrom/lastModifiedDateTo, and startDateFrom/startDateTo.
Date filters are pushed into PlayFab search where possible and can be combined with pagination, creator, tag, free, popular, latest, summary, compare, player marketplace search, recommendations, and sales item queries.
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).
Returns featured persona items from the Dressing Room layout page.
Returns the Minecraft Services authorization token for the configured primary title.
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 */ } } ]
}
}
}Advanced Search uses a strict, fixed request body. Default mode: "page" keeps the existing paged/faceted behavior with query, filters, and sort. mode: "cursor" uses native Catalog/SearchItems and accepts language, select, store, storeId, storeAlternateId, count, and continuationToken.
Unknown fields are rejected with 400 Bad Request.
{
"query": {
"text": "valentine bundle"
},
"filters": {
"id": "6656d5d4-d8ad-49a5-870f-04cb26d3ce26",
"friendlyId": "f5c30f04-e47e-4a2c-88f1-313b868195dc",
"alternateIds": [{ "type": "FriendlyId", "value": "f5c30f04-e47e-4a2c-88f1-313b868195dc" }],
"keywords": ["bundle"],
"isStackable": false,
"platforms": ["android.googleplay"],
"tags": ["hidden_offer", "3P"],
"tagsAny": ["3P", "server"],
"tagsAll": ["marketplace", "hidden_offer"],
"contentType": "3PServerContent_V1.2",
"contentKinds": ["world"],
"creationDate": { "from": "2026-02-01T00:00:00Z", "to": "2026-02-28T23:59:59Z" },
"lastModifiedDate": { "from": "2026-02-01T00:00:00Z", "to": "2026-02-28T23:59:59Z" },
"startDate": { "from": "2026-02-01T00:00:00Z", "to": "2026-02-28T23:59:59Z" },
"priceAmounts": { "min": 1000, "max": 1500, "currencyId": "ecd19d3c-7635-402c-a185-eb11cb6c6946" },
"creatorName": "The Hive",
"offerId": "f5c30f04-e47e-4a2c-88f1-313b868195dc",
"purchasable": true,
"packIdentityType": "durable",
"ratingMin": 4.0
},
"sort": [
{ "field": "startDate", "dir": "desc" },
{ "field": "priceAmount", "dir": "asc" }
]
}Cursor mode example:
{
"mode": "cursor",
"query": { "text": "dragon" },
"filters": { "contentType": "3PServerContent_V1.2" },
"language": "de-DE",
"select": "title,description,images,priceOptions",
"count": 24,
"continuationToken": ""
}Query behavior
query.textis forwarded to PlayFabSearch(fuzzy/full-text; e.g., matches via title/description/indexed fields).
Filter behavior (PlayFab vs API layer)
The following filters are pushed directly to PlayFab OData in Catalog/Search:
idfriendlyId,alternateIdsisStackableplatformstags,tagsAny,tagsAllcontentType,contentTypescontentKinds(skinpack,world,persona,addon,resourcepack,mashup,server)creationDate,lastModifiedDate,startDatepriceAmounts.min/max(viaDisplayProperties/price)ratingMin(viarating/average)creatorNamepurchasable
The following filters are additionally evaluated in the API layer (when PlayFab cannot reliably cover them directly):
keywordsofferIdpackIdentityTypepriceAmounts, including optionalcurrencyIdchecks viaPrice/PriceOptions.
Sorting
Allowed sort fields:
id,type,title,description,keywordscontentType,isStackable,platforms,tagscreationDate,lastModifiedDate,startDatepriceAmount,creatorName,purchasable,packIdentityType
Not allowed (returns 400):
offerIdfriendlyIdalternateIds
Performance note
- The service still uses paginated PlayFab batches (
ADV_SEARCH_BATCH,ADV_SEARCH_MAX_BATCHES) and applies local filtering/sorting only after upstream fetch when required.
/events/stream(optional queryevents=<comma-separated-event-names>):item.snapshot,item.created,item.updated,marketplace.pass.snapshot,marketplace.pass.added,marketplace.pass.removed,marketplace.pass.updated,realms.plus.snapshot,realms.plus.added,realms.plus.removed,realms.plus.updated,sale.snapshot,sale.update,price.changed,creator.trending,featured.content.added,featured.content.removed,featured.content.updated.- Subscription events compare the current
csb/realms_plustag memberships with the last persisted watcher state. Added/removed events include the current item list, updated events includebefore/afterentries, and each item includessubscription.startDate/subscription.endDatefromcsbStartDate,csbEndDate,realmsPlusStartDate, andrealmsPlusEndDate. - Featured content changes emit dedicated
featured.content.added,featured.content.removed, andfeatured.content.updatedevents. Payloads include the matchingaddedItems,removedItems, orchangedItemsplus item details withfeaturedContext(page,row,component,itemIndex);currentItemDetailsandpreviousItemDetailsprovide the full after/before featured item lists. Same-ID layout or metadata changes emitfeatured.content.updated, setcontentChanged=true, and includepreviousContentSignature/currentContentSignature.
POST /webhooks— register{ events, 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"Featured content
TOKEN="<jwt>"
curl -sS "http://localhost:3000/marketplace/featured-content" \
-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/stream?events=creator.trending", {
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. -
Lightweight list responses: pass
full=false(orenrich=false) to skip fullGetItemsenrichment, andrefs=falseto skip resolvingItemReferenceson list endpoints that include them by default. -
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.
-
Endpoints:
/events/stream
-
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,ENABLE_CREATOR_PARTNER_WATCHER,ENABLE_FEATURED_CONTENT_WATCHER,ENABLE_SUBSCRIPTION_WATCHER.
Register
curl -sS -X POST http://localhost:3000/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, marketplace.pass.snapshot, marketplace.pass.added, marketplace.pass.removed, marketplace.pass.updated, realms.plus.snapshot, realms.plus.added, realms.plus.removed, realms.plus.updated, price.changed, creator.trending, featured.content.added, featured.content.removed, featured.content.updated
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)
Note
/openapi.json is always exposed, but the interactive Swagger UI at /docs is disabled by default. If you enable docs in a public environment, make sure the rest of your deployment and authentication settings match your intended exposure model.
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
PLAYFAB_DEVICE_ID=vmc-<unique-random-id>
TRUST_PROXY=1
LOG_LEVEL=info
CORS_ORIGINS=http://localhost:5173,https://view-marketplace.net
HTTP_MAX_SOCKETS=512
HTTPS_MAX_SOCKETS=512
UPSTREAM_TIMEOUT_MS=20000
RETRY_BUDGET=3
SESSION_TTL_MS=1800000
SESSION_EXPIRY_SKEW_MS=60000
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
UPDATE_CHECK_ENABLED=true
UPDATE_CHECK_REPOSITORY=Daniel-Ric/PlayFab-Catalog-Service-Bedrock
UPDATE_CHECK_TTL_MS=900000
UPDATE_CHECK_TIMEOUT_MS=5000
UPDATE_CHECK_STARTUP_LOG=true
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
ENABLE_CREATOR_PARTNER_WATCHER=true
CREATOR_PARTNER_WATCH_INTERVAL_MS=21600000
CREATORNAME_MODE=nospace
ENABLE_FEATURED_CONTENT_WATCHER=true
FEATURED_CONTENT_WATCH_INTERVAL_MS=21600000
ENABLE_SUBSCRIPTION_WATCHER=false
SUBSCRIPTION_WATCH_INTERVAL_MS=300000
SSE_HEARTBEAT_MS=15000
ADV_SEARCH_TTL_MS=60000
ADV_SEARCH_BATCH=300
ADV_SEARCH_MAX_BATCHES=10
ADV_SEARCH_CONCURRENCY=5
WEBHOOK_TIMEOUT_MS=5000
WEBHOOK_MAX_RETRIES=3
WEBHOOK_CONCURRENCY=4
MAX_SEARCH_BATCHES=10
MAX_FETCH_BATCHES=20
FETCH_CONCURRENCY=5
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
RATE_LIMIT_VERSION_WINDOW_MS=300000
RATE_LIMIT_VERSION_MAX=200- 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
}
}If CATALOG_BRIDGE_ENABLED=true, a reverse proxy can additionally route these paths to the same Node service:
/api/security/csrf
/api/security/catalog-handshake
/api/catalog/proxy
/api/catalog/secure
Existing public /catalog/... proxy rules can remain unchanged. The bridge accepts only relative /catalog/... targets and forwards them to the configured CATALOG_UPSTREAM_ORIGIN.
- 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.