What is the request path?
A browser request never reaches the API directly in the production Compose stack.- The browser calls
/api/backend/*on the web origin, the same origin it signed in to. - The Next.js route handler proxies that request to
API_INTERNAL_URL, which Compose sets tohttp://api:4000. - The API authenticates the session cookie or the
asob_bearer token, authorizes the request and answers.
API_PROXY_TIMEOUT_MS bounds each proxied request, and a request that exceeds it returns a 504 error envelope instead of hanging.
Server rendered pages skip the proxy and call API_INTERNAL_URL directly, which is why the same variable serves both paths.
What are the four surfaces?
Why is store collection isolated?
Every store request goes through theStoreProvider interface in apps/api/src/store-providers/. No other module may import a scraper library.
That single rule contains a parser breakage to one file. When Apple or Google change a response shape, one provider module fails and the rest of the application keeps serving requests. It also keeps rate limiting in one place, and it leaves room for a future deployment to swap in proxies or a data API without touching product code.
Raw scraper payloads are stored in raw JSON columns. Parsers change, so keeping the original response means a corrected parser can reprocess history without collecting it again.
How does work run in queues?
Collection runs in two BullMQ workers, one per store, both at concurrency 1 behind a rate limiter.
A parse failure fails the job. BullMQ retries it with backoff, and request handling is never affected. See The daily pipeline and rate limits.
The component graph
The browser talks only to the Next.js web app, which proxies to the NestJS API. The API reads and writes PostgreSQL for durable state and enqueues work in Redis. The store workers drain those queues and are the only components that reach the App Store and Google Play endpoints.Where do types live?
Every request and response shape the frontend consumes is defined in@asobeast/shared, for example AppListItem, TrackedKeywordItem, AppSummary, RankingSeries and ApiErrorEnvelope.
Prisma generated types never cross the
apps/api boundary, so the web app and the MCP server stay independent of the database. @asobeast/shared is compiled with tsup to CommonJS, ESM and type declarations, because the NestJS CommonJS build cannot consume a just in time TypeScript package cleanly.