# apps/api — Public Auth + Media API

The public-facing Bigscreen API. Everything the VR app and the website need to authenticate users, manage accounts, buy / play media, report content, subscribe to topics, and ingest analytics events lands here. It also terminates the Shopify webhook and serves the eye-tracking endpoints used by the Beyond headset.

**Entry:** [apps/api/api.ts](../../apps/api/api.ts):19 — `api.set("port", 3009)`.
**Package:** `bigscreen_api` (yarn workspace).
**Depends on:** [`@bigscreen/lib`](../libraries/lib.md), [`@bigscreen/auth`](../libraries/auth.md), [`@bigscreen/api`](../libraries/api-handlers.md).

## Request Pipeline

Every request is processed through the same middleware stack (with a couple of path-specific skips):

```mermaid
flowchart LR
    REQ[HTTP request] --> C1[compression]
    C1 --> C2[helmet]
    C2 --> C3{path == /shopify/webhook?}
    C3 -- yes --> SK1[skip bodyParser<br/>raw body]
    C3 -- no --> C4[bodyParser.json<br/>limit=1mb]
    SK1 --> C5[CORS handler]
    C4 --> C5
    C5 --> C6{origin in<br/>bigscreencloud.com /<br/>bigscreenvr.com?}
    C6 -- no, no auth --> R401[CorsError 401]
    C6 -- yes, or has Authorization --> C7[requiresAuthorizedRequest]
    C7 --> C8{bypass path?<br/>/shopify/webhook,<br/>/eye-tracking/*,<br/>/oauth/token,<br/>/.well-known/*}
    C8 -- yes --> R[route handler]
    C8 -- no --> C9[Auth.AuthHandler<br/>authorizeHttpRequest]
    C9 -- AuthError --> R401b[401/403]
    C9 -- ok --> R[route handler]
```

Notable details:

- `/shopify/webhook` bypasses both JSON parsing (we want raw body for HMAC) and app-key auth (Shopify uses HMAC signing instead). See [data-flows.md#5-shopify-webhook--order](../data-flows.md#5-shopify-webhook--order).
- `/eye-tracking/*` uses its own JWT scheme (`requiresEyeTrackingToken`) rather than the standard access-token flow.
- `/oauth/token`, `/.well-known/jwks.json`, and `/.well-known/oauth-authorization-server` bypass `requiresAuthorizedRequest` because external OAuth clients cannot be in `allowed_apps.json` — they authenticate via `client_id` + `client_secret` inside the token endpoint (and the well-known endpoints are public per RFC). All three are gated on `OAUTH_ENABLED=true`; otherwise they return 501. See [services/oauth.md](./oauth.md).
- Rate limiting is currently **disabled** — [api.ts:91,94](../../apps/api/api.ts) shows commented-out imports. Infrastructure lives in [api/src/RateLimiter.ts](../../api/src/RateLimiter.ts) if / when we re-enable.
- CORS allows null-origin (Unity / native clients) only when an Authorization header is present.

## Endpoint Groups

Endpoint registration is done inline in [api.ts](../../apps/api/api.ts) — there is no per-domain router file. The groups below reflect the main path prefixes.

| Prefix | Purpose | Handler origin |
|--------|---------|----------------|
| `/auth/*` | Account creation, login, password reset, token verify/renew, Steam/Oculus auth | `@bigscreen/auth` (`AuthApi`) |
| `/auth/account`, `/auth/profile` | Logged-in user profile management | `@bigscreen/auth` |
| `/media/*` | Media products, entitlements, playback, orders | `@bigscreen/api` (`Media`) |
| `/channel_groups`, `/channel/:id`, `/topic/*` | Channels, channel groups, topic subscriptions | `@bigscreen/api` (`Channels`, `Topics`) |
| `/beyond/*` | Face scan requests, Beyond big orders, Multipass | `@bigscreen/api` (`Beyond`) |
| `/info/*` | Username validation, server time, geoip / region, currency | `@bigscreen/api` (`PublicApi`) |
| `/event` | Analytics event ingestion (auth optional) | `@bigscreen/api` (`Analytics`) |
| `/report/user`, `/report/room` | User / room reporting (cloud-api uses this too) | `@bigscreen/api` (`ReportingApi`) |
| `/eye-tracking/*` | Beyond headset eye-tracking model + files (custom JWT) | `@bigscreen/api` (`EyeTracking`) |
| `/shopify/webhook` | Shopify-initiated order events | `@bigscreen/api` (`Shopify.ShopifyAdminApi`) |
| `/oauth/token` | OAuth 2.0 `authorization_code` + `refresh_token` grants | `@bigscreen/api` (`OAuthApi`) |
| `/.well-known/jwks.json` | Access/refresh-token signing keys (RFC 7517) | `@bigscreen/api` (`OAuthApi`) |
| `/.well-known/oauth-authorization-server` | Authorization-server discovery metadata (RFC 8414) | `@bigscreen/api` (`OAuthApi`) |

`apps/api` does **not** host admin endpoints. Those live on the IP-restricted [admin-api](./admin-api.md). OAuth *administration* (client CRUD, grants, audit) also lives there — `apps/api` only hosts the public-facing token endpoint and discovery.

## Auth Posture

Every non-webhook route goes through a single `standardAuthHandler` created at [api.ts:98](../../apps/api/api.ts):

```ts
const standardAuthHandler = Auth.AuthHandler.createAuthHandler({ apiName: "api" });
```

Access tokens are enforced on a per-route basis with `AuthApi.requiresValidAccessToken`. Access-policy checks (e.g. requiring `Moderator` for content reporting admin endpoints) use `AuthApi.getAccessPolicyHandler([...])`. See [libraries/auth.md](../libraries/auth.md) for how those middleware pieces work.

## Startup

```mermaid
sequenceDiagram
    participant P as Node process
    participant ENV as dotenv
    participant LIB as @bigscreen/lib
    participant SRV as Express

    P->>ENV: dotenv.config()
    P->>LIB: Logger.initialize()<br/>Postgres pool<br/>Redis + Redlock
    P->>SRV: express()<br/>compression, helmet, bodyParser, CORS
    P->>SRV: standardAuthHandler<br/>requiresAuthorizedRequest
    P->>SRV: register ~150 routes
    P->>SRV: api.listen(3009)
```

## Conventions

- **JSON-first**: every response is JSON except the Shopify webhook (plain 200 OK) and a handful of HTML redirects.
- **Custom headers**: tokens travel in headers (`x-access-token`, `x-refresh-token`, `x-timestamp`, `x-nonce`), listed in [AuthSchemas.HttpHeaders](../../auth/Schemas.ts).
- **Status codes**: 200 OK, 400 validation, 401 auth, 403 policy, 404 missing, 500 internal. Rate-limit 429s will return once the rate limiter is re-enabled.
- **Error surface**: Express `err` handler catches `Auth.AuthError` / `Auth.CorsError` and returns the attached HTTP code. Unexpected errors yield a 500 with a logged stack trace.

## Further reading

- Per-domain handler detail (what `Media`, `Beyond`, `Channels`, etc. actually do) → [libraries/api-handlers.md](../libraries/api-handlers.md)
- Auth token format, refresh, access policies → [libraries/auth.md](../libraries/auth.md)
- OAuth 2.0 provider (token endpoint on this service, client CRUD on admin-api, consent UI on arda) → [services/oauth.md](./oauth.md)
- How Shopify webhooks end up in the factory queue → [data-flows.md#5-shopify-webhook--order](../data-flows.md#5-shopify-webhook--order)
