May 21, 2026
Plan 024 Phases 0–3 (production hardening)
Security and hardening
- BYOK API key removed from Inngest event payload:
scan/requestedandplatform-scan/requestedno longer ship theOPENROUTER_API_KEYthrough the durable event store. Worker now fetches the key fresh viaresolveOpenRouterKey(organizationId)insidestep.run. Stops the key from sitting in Inngest's UI, logs, or retry payloads. - Fake RAG retired:
retrieveContext(whichembedMany'd a query embedding and then did a no-opexists()subquery) renamed togetRecentMentionsand now honestly returns chronologically-recent rows. System prompt updated to say "RECENT BRAND MENTIONS (chronological, not ranked)". - Bulk-scan failure recovery: missing
.catchreattached torunScanInBackgroundinbulk-execute.tsso a single failing worker no longer leaves ascan_sessionrow stuck inpendingforever. - Typed environment validation: new
@repo/envpackage built on@t3-oss/env-nextjs. Allprocess.env.*reads now flow through a Zod-validated schema (emptyStringAsUndefined: true, most fields.optional()to avoid breaking provider-gated deployments). Wired intoapps/saas/config.tsandapps/marketing/config.ts. - Server-side password schema enforcement: Better Auth's
beforehook now runspasswordSchema.safeParse()against/sign-up/email,/change-password,/reset-password,/set-password. A forged request that skips the React form is rejected with aBAD_REQUESTinstead of writing a weak hash to disk. - Impersonation banner: when
session.impersonatedByis set, a sticky red banner with a one-click stop button is rendered on every authenticated page via<ImpersonationBanner />mounted in the authenticated layout. - Content-Security-Policy with per-request nonce on both proxies (
apps/saas/proxy.ts+apps/marketing/proxy.ts).crypto.getRandomValues→ 16-byte base64 nonce per request, propagated to layouts viax-nonceheader. Default Report-Only (setCSP_REPORT_ONLY=falseto enforce) so violations are observable before they break the app. - AuditEvent table + `@repo/audit-log`: append-only
audit_eventschema (actor, org, type, target, JSON metadata, IP, UA, createdAt) with indexes on actor/org/type/createdAt.recordAudit()swallows write failures (compliance prefers a missing log line to a missed login). Better Authafterhook records sign-in/sign-up, password reset/change, admin impersonate/ban/role-change/delete, two-factor enable/disable, email change. Migration0014_shiny_prima.sql.
Performance and DX
- `reportWebVitals` wired:
<WebVitals />mounted in root layout usesnext/web-vitals. Logs to console in dev; in production beacons toNEXT_PUBLIC_WEB_VITALS_URL(no-op if unset). - Bundle analyzer behind `ANALYZE=true`:
@next/bundle-analyzerwrappingnext.config.ts. SetANALYZE=true pnpm --filter saas buildto inspect chunk sizes.
Conformance and quality
- Fumadocs version pinned correctly:
apps/docs/fumadocs-mdxset to15.0.7(latest 15.x; v16 doesn't exist) so thewithMDXtypegen + postinstall regenerate the.source/server.tscorrectly. - `proxy.ts` export style aligned: marketing's default export switched to a named
export async function proxyto match the SaaS app + agents.md "avoid default exports" guidance. - `types` glob fixes in three more packages (
@repo/logs,@repo/storage,@repo/utils) — malformed"./**/.ts"patterns replaced with"./index.ts". - `type` → `interface` for the prop type alias on the
@repo/uicomponents that needed it (button,badge,chart). - Floating-promise sweep: five
queryClient.invalidateQueries(...)call sites that were warning undertypescript/no-floating-promisesnow properly prefixed withvoid.