StudyBoost Docs

Core systems (frontend & backend)

PURPOSE

This documents the implemented scaffolding aligned with Practical-core-systems.md: centralized logging, config feature flags, a standard API error shape, and frontend data/prefetch helpers. It maps to observability (§1), feature flags (§8), error system (§15), and preparation for data sync / prefetch (§17, §19).


Frontend (frontend/lib/)

Logging — lib/logger.ts

  • API: logger.debug, logger.info, logger.warn, logger.error(message, context?)
  • Behavior: debug / info are no-ops in production; warn / error always run. Optional context is JSON-serialized.
  • Intent: Replace ad-hoc console.* so Sentry or another sink can be wired in one place later.

Feature flags — lib/flags.ts

  • API: getFlag(name), getFlags()
  • Flags: newUI, aiStreaming, debugMode (typed as FeatureFlagName).
  • Env (build-time):
FlagEnvironment variableDefault
newUINEXT_PUBLIC_FF_NEW_UIfalse
aiStreamingNEXT_PUBLIC_FF_AI_STREAMINGfalse
debugModeNEXT_PUBLIC_FF_DEBUG_MODEfalse
  • Truthy values: 1, true, yes, on (case-insensitive). Falsy: 0, false, no, off, empty/unset.
  • Note: Dev-only toggles in the dev overlay are separate from these product flags.

Error system — lib/errors/

  • AppErrorBody: { code: string; message: string; status: number; details?: unknown } — matches Nest error responses after the global filter.
  • AppError: throwable client error with the same fields.
  • isAppErrorBody(value): type guard for JSON.
  • parseApiError(res) / parseApiErrorBody(res): parse a failed fetch Response.
  • throwIfNotOk(res): throws AppError when !res.ok.

Example:

import { throwIfNotOk } from "@/lib/errors"

const res = await fetch(url, options)
await throwIfNotOk(res)

Data layer prep — lib/data/

  • query-keys.ts stable queryKeys tree (auth, users, courses, institutions, documents) for future TanStack Query / SWR.
  • routes.ts appRoutes canonical paths (home, login, onboarding, dashboard, test).
  • use-app-prefetch.ts client hook useAppPrefetch()prefetchHref(href) using Next.js router.prefetch.

Example:

import { appRoutes, useAppPrefetch } from "@/lib/data"

const { prefetchHref } = useAppPrefetch()
// e.g. onMouseEnter={() => prefetchHref(appRoutes.dashboard)}

Dev overlay — components/dev-overlay/

  • Scope: Dev-only debugging panel; mounted in app/layout.tsx behind isDev and never shipped to production. Provides an in-browser inspector (network, errors, routes, storage, cookies, env, perf) plus a saved-user session switcher.
  • Instrumentation (instrumentation.ts): monkey-patches fetch, localStorage/sessionStorage, document.cookie, console.error, and history to record events. All captured data stays in-memory only (devEventStore) and is rendered in the panel — nothing is sent off-device. The only network calls go to the project's own backend (NEXT_PUBLIC_BACKEND_URL) for the dev session switcher.
  • Event store (store.ts) — INVARIANT: push() must never notify subscribers synchronously. The store coalesces and flushes useSyncExternalStore listeners on a microtask. Synchronous notification is unsafe because push() runs inside the patched console.error, which React calls during its commit / insertion-effect phase — a synchronous notify there triggers React's "useInsertionEffect must not schedule updates" warning, which is itself logged via console.error, re-entering push() and looping until the page freezes ("Page Unresponsive"). The console.error patch additionally carries a reentrancy guard so a nested error during recording is dropped rather than amplified.

Backend (backend/src/common/)

Logging — common/logger/app-logger.ts

  • API: appLogger.debug, appLogger.log, appLogger.warn, appLogger.error(message, trace?, context?)
  • Wraps Nest Logger with optional JSON context on messages.

Error envelope — common/filters/app-exception.filter.ts

  • Registered globally in app.module.ts via APP_FILTER provider.
  • All thrown errors are normalized to JSON:
{
  "code": "UNAUTHORIZED",
  "message": "Human-readable message",
  "status": 401,
  "requestId": "uuid-v4-trace-id",
  "details": {}
}
  • requestId: A unique correlation ID attached to every request, used for cross-log tracing.
  • details is omitted when undefined.

Request Tracing & Logging — common/interceptors/logging.interceptor.ts

  • Global Interceptor: Captures every request/response cycle.
  • X-Request-ID: Generates or propagates a correlation ID for tracing.
  • Latency Tracking: Logs execution time for every endpoint.
  • Sanitization: Automatically masks sensitive fields (password, token, secret, key) in logs.

Document Storage Security (#215)

Bucket architecture

  • Private bucket (gs://studyboost.com): Stores all document PDFs. Public IAM (allUsers) removed; Uniform Bucket-Level Access (PAP) enforced post-#215 cutover. Direct object URLs return 403.
  • Public bucket (GCP_PUBLIC_BUCKET_NAME): Stores non-sensitive assets only — avatar images and server-generated preview rasters (previews/{id}/{n}.jpg). Objects in this bucket are intentionally world-readable.

Full-document serving

  • GET /documents/:slug/file for full-access users: access is verified server-side, then GcsStorageService.getSignedUrl(objectPath, 900) issues a V4 signed URL with a 15-minute TTL. The controller responds 302 to the signed URL.
  • Signed URLs expire after 15 minutes. Clients must re-call /file to obtain a fresh URL.
  • Raw cloud_storage_url values are never serialized into API responses; pdfUrl is no longer published to clients.

Preview rasters

  • The server rasterizes the first 3 PDF pages to JPEG using pdfjs-dist (legacy build) + canvas (NodeCanvasFactory) and writes them to previews/{documentId}/{n}.jpg in the public bucket.
  • thumbnail_url on the document row points to page 1 (previews/{documentId}/1.jpg).
  • Rasterization failures produce a synthetic cover fallback; they do not propagate errors to callers.

Deploy cutover order

  1. Ship signed-URL code and verify full-document serving works via signed URLs.
  2. Remove allUsers IAM binding and enforce PAP on gs://studyboost.com. Do not privatize the bucket before signed-URL serving is verified in production.

Security & Protection (backend/src/common/)

Tiered Rate Limiting

Configured via ThrottlerModule with three distinct tiers:

  • auth: 5 requests / 5 minutes (Strict protection for login/signup).
  • ai: 20 requests / 1 hour (Cost control for LLM usage).
  • default: 100 requests / 1 minute (General API protection).

Guards:

  • AuthThrottlerGuard: Tracks by IP or User ID for authentication routes.
  • AiThrottlerGuard: Specific limits for AI inference to manage provider costs.

Abuse Prevention — common/services/abuse-prevention.service.ts

  • Identity Blocking: Persistence-based blocking using the blocked_identities table.
  • Automated "Smart Ban": Automatically blocks IPs/Users for 24 hours if they trigger 5+ suspicious events (e.g., rate limit hits, failed logins) within 10 minutes.
  • Security Audit Log: Every suspicious event is logged to security_events for manual review.
  • Global Shield: GlobalAbuseGuard enforces blocks across all routes before any logic executes.

AI Cost Controls — backend/src/ai/ai.service.ts

  • Real-time Credit Guarding: Every AI request checks the user's subscription_usages.
  • Atomic Enforcement: Prevents credit overruns via atomic database increments.
  • Usage Cycles: Automatically tracks usage within monthly windows aligned with subscriptions.

Authentication (backend/src/auth/)

Secure cookie session flow

  • Login/signup issue two HTTP-only cookies:
    • access_token (short-lived, default 15 minutes)
    • refresh_token (long-lived, default 30 days)
  • Refresh tokens are stored only as hashes in auth_sessions.
  • Every refresh rotates the refresh token hash (replay-resistant rotation).
  • Logout clears cookies and removes active session rows.
  • Frontend auth UX is page-based (/login, /signup) with redirect continuation via next.
  • Unauthenticated protected access redirects to login; there is no in-page auth modal fallback.

Guard behavior

  • CookieAuthGuard validates access token + session row (auth_sessions) for protected routes.
  • If access token is expired and refresh token is valid, the guard auto-refreshes and reissues cookies.
  • If session is missing/expired/revoked, request is rejected as unauthorized.

Structured Notes (backend/src/notes/)

FEAT-59 module behavior

  • Module: NotesModule registered in AppModule.
  • Guarding: all endpoints use CookieAuthGuard; ownership derives from request.user.user_id.
  • Endpoints:
    • GET /notes, POST /notes
    • GET /notes/:id, PATCH /notes/:id, DELETE /notes/:id
    • POST /notes/:id/references
    • PATCH /notes/:id/references/:referenceId
    • DELETE /notes/:id/references/:referenceId
    • PATCH /notes/:id/canvas

Related repo doc

  • Practical-core-systems.md (repo root) — full checklist; this page only describes what is implemented today.

Not implemented here (see Practical-core-systems.md)

  • Sentry & Metrics (OpenTelemetry/Prometheus)
  • Job Queues idempotency (Queues are implemented but idempotency logic is pending)
  • Per-user / remote feature flags
  • Zod (or similar) on every route
  • TanStack Query / SWR (queryKeys and prefetch hook are ready for when you add them)

QA Coverage Expectations

Test IDScenarioExpected Result
SEC-01Correlation ID InjectionAll response headers contain X-Request-ID and logs show the same UUID.
SEC-02Tiered Rate LimitingAuth routes block after 5 attempts; AI routes block after 20.
SEC-03Automated Smart Ban5 rate-limit hits in 10m results in a 24h entry in blocked_identities.
SEC-04Global Abuse ShieldBlocked IP/User receives 403 Forbidden on any endpoint.
SEC-05AI Cost EnforcementUser with 0 credits receives 402 Payment Required on AI endpoints.
SEC-06Data SanitizationLogs for /auth/login do not contain plaintext passwords.
SEC-07Owned-Document Gate (#287)Upload with sourceReference: 'owned-document:<slug>' returns 400; owned-document: resolvers only ever read the caller's own (uploader_id-scoped) content — no cross-user paid/premium text leaks.
SEC-08AI Credit Deduction Integrity (#288)POST /subscriptions/use-ai-credit returns 404 (debug endpoint removed); useAiCredit rejects non-positive/non-integer amounts (400); ai_credits_used can never be driven negative.