# Project export: Lumixia Brief

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: Turn a messy idea into a clear project brief GPT-5.6 actually understands — through a guided interview, a live confidence score, and instant sync to Notion.
- Devpost: https://devpost.com/software/lumixia-brief
- GitHub: https://github.com/Z2ZATL/lumixia-brief.git
- Demo: http://lumixia-brief.vercel.app/
- Video: https://www.youtube.com/embed/Rj-vb5Cclkw?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Z2ZATL (70 commits)

## Devpost submission (written by the team)

### Inspiration

Every founder, PM, and agency team knows the moment: someone drops a one-line idea into a doc — "build me a dashboard for X" — and everyone downstream has to guess what it actually means. AI tools make this worse, not better. Feed a vague prompt to a generator and you get a polished-looking output built entirely on invented assumptions nobody signed off on. Rework follows. We wanted the opposite of "generate first, ask questions never." Lumixia Brief starts from the belief that a good brief isn't written, it's interviewed into existence — one adaptive question at a time, until the team can see exactly what's known, what's assumed, and what still needs a human decision.

### What it does

Lumixia Brief turns a deliberately vague project idea into a reviewable, versioned one-page brief: Enter a rough idea. Answer 5–12 adaptive questions, one at a time — never while typing, only on submit. Watch confidence build across eight dimensions: Problem, Audience, Outcome, Scope, Constraints, Timeline, Risks, and Success criteria. Review a structured brief plus an "Alignment Improvement" summary showing what got clarified along the way. Reject any section for a focused follow-up question, or approve an immutable, versioned snapshot. Sync the approved version to a Notion page, idempotently — retrying never creates a duplicate. The model (GPT-5.6) proposes facts, assumptions, contradictions, and dimension assessments. It never decides. The server independently calculates the confidence score, enforces stop rules, and owns every workflow transition — so the process stays explainable and testable, not a black box.

### How we built it

Frontend: React 19 + Vite, served from Vercel's CDN, with an EN/TH language switch and full desktop/mobile support. Backend: A single Express app running as one Vercel Fluid Compute function, with strict Zod contracts shared between client and server. Auth & data: Supabase Auth (Google OAuth + mandatory TOTP/AAL2) and Supabase Postgres with forced Row-Level Security on every table — RLS checks ownership and MFA independently of the application layer. AI: OpenAI's GPT-5.6 Responses API with Structured Outputs, store:false, and strict retry rules — swappable behind a provider interface so the app can run fully mocked for development. Integrations: Per-user Notion OAuth with AES-256-GCM-encrypted tokens, and an owner-operated Codex MCP server (/api/mcp) that lets us run the entire interview through a Codex session instead of paid API calls, gated by Supabase OAuth 2.1 consent and AAL2. Ops: Sentry with full payload redaction, Docker for local Supabase/portability checks only (not the runtime), GitHub Actions running format/lint/typecheck/unit/UI/RLS/E2E/audit/SBOM/secret-scan gates on every PR. Codex was our pair programmer for nearly the entire build — scaffolding the app, implementing the confidence engine, writing the RLS policies, and generating the test suite — with every milestone logged in a Build Ledger and every architectural decision written up as an ADR.

### Challenges we ran into

No paid OpenAI credits during the build. We solved this by making the model provider fully swappable (disabled / mock / live) and building an owner-operated Codex MCP integration plus a loopback bridge so the live interview could run against a real reasoning model without ever touching OPENAI_API_KEY. Browsers don't trust HTTPS pages calling http://127.0.0.1. Our first Production rehearsal got blocked outright (ERR_BLOCKED_BY_CLIENT). We fixed it by keeping a same-origin loopback pairing popup open as a relay, so the production page never makes a mixed-scheme request directly. Auth migration mid-build. We started on a third-party auth provider and migrated to native Supabase Auth to get first-class AAL2/MFA enforcement baked into both the API and RLS — a full rip-and-replace under deadline pressure. Keeping AI honest. It was tempting to let the model's confidence claims stand on their own. Instead we built a deterministic scoring rubric (sum of dimension points / 24 × 100) entirely in server code, so the number is explainable and unit-tested, not a model's opinion.

### Accomplishments we're proud of

Production fails closed: missing security or provider configuration stops the app from starting at all — no silent fallback to insecure defaults. 85%+ line/function coverage on the server, with stricter 90% gates on security-critical code, backed by unit, API, UI, Supabase RLS, and Playwright E2E suites. A fully working Codex MCP integration that lets the owner run the core product loop without spending a cent on the OpenAI API. A complete, sanitized audit trail — 22 Build Ledger milestones and 9 architecture decision records — documenting exactly what changed, why, and how it was verified. A sub-three-minute path from a vague one-line idea to an approved, Notion-synced brief.

### What we learned

Separating "the model proposes, the server decides" turns AI output from a trust exercise into something you can actually test and reason about. Loopback-to-browser communication under HTTPS has real, non-obvious security constraints — an origin-bound relay window ended up being simpler and safer than trying to fight the browser's mixed-content policy. Supabase RLS plus AAL2-aware JWT claims is a powerful pattern for enforcing MFA at the data layer, not just the UI. Writing ADRs and a build ledger while iterating fast with an AI pair programmer keeps decisions traceable instead of getting lost in commit history.

### What's next

Turn on the live GPT-5.6 Responses API path for all users once API budget is approved (the code path is already contract-tested and ready). Richer Notion sync — mapping brief fields to database properties, not just creating a child page. Multi-brief workspaces with team comments and shared review, not just single-owner approval. Additional languages beyond EN/TH, and a lighter mobile-first interview flow.

## README (from the GitHub repository)

# Lumixia Brief

> Lumixia Brief does not rush to generate work from a vague prompt. It interviews until everyone can see what is known, what is assumed, and what still needs a human decision.

**Build Week track:** Work & Productivity<br>
**Primary demo:** A founder preparing a brief for Codex<br>
**Core Codex Session ID:** `019f614d-cd80-76d3-8151-b8271f575a3f`<br>
**Source:** <https://github.com/Z2ZATL/lumixia-brief><br>
**Demo URL:** <https://brief.z2zs.space><br>
**Contributors:** [Z2ZATL and OpenAI Codex](CONTRIBUTORS.md)

Lumixia Brief is a React web app that turns an unclear project idea into a reviewable, versioned one-page brief. Codex local demo mode or the live GPT-5.6 provider asks one adaptive question per submitted answer, identifies facts, assumptions, and contradictions, and assesses eight clarity dimensions. The server calculates the score and decides when the brief is ready. A human must review and approve an immutable snapshot before Notion receives anything.

## Three-minute product path

1. Enter a deliberately vague idea.
2. Answer 5–12 adaptive questions, one at a time.
3. Watch confidence change across Problem, Audience, Outcome, Scope, Constraints, Timeline, Risks, and Success criteria.
4. Review a structured brief and the Alignment Improvement evidence.
5. Reject a section for a focused follow-up, or approve an immutable version.
6. Select a Notion parent page and sync the approved version idempotently.

## Architecture

```mermaid
flowchart LR
  U["Founder / PM / Agency"] --> C["Supabase Auth\nGoogle OAuth + TOTP"]
  X["Codex desktop / CLI\nowner-operated model"] --> C
  C --> V["Vite React on Vercel CDN"]
  V --> L["Loopback demo bridge\nCodex CLI + ChatGPT plan"]
  L --> V
  V --> E["Express /api function"]
  C --> E
  E --> O["OpenAI Responses API\nGPT-5.6, store:false"]
  E --> S["Supabase Postgres\nNative JWT + owner/AAL2 RLS"]
  E --> N["Notion Public OAuth\nAES-256-GCM tokens"]
  E --> M["Sanitized logs + Sentry\nno content or PII"]
```

- `src/` — React UI, EN/TH switch, protected app flow.
- `api/index.ts` — Vercel Express entrypoint.
- `server/domain/` — deterministic confidence, question priority, stop rules, and workflow invariants.
- `server/providers/` — live/mock OpenAI and Notion adapters.
- `server/mcp/` — authenticated Streamable HTTP tools for an owner-operated Codex session.
- `scripts/codex-bridge/` — loopback-only Codex CLI runner for the no-API-charge video demo.
- `server/store/` — in-memory test adapter and Supabase adapter using the verified Supabase JWT.
- `shared/contracts.ts` — strict Zod contracts shared by client and server.
- `supabase/migrations/` — forward-only schema and forced RLS policies.
- `tests/` — unit, API, Supabase RLS integration, and Playwright demo tests.

Vercel serves the Vite build from its CDN and rewrites `/api/*` to one Express Fluid Compute function. Docker is intentionally limited to local Supabase, integration testing, Linux/amd64 build verification, and portability checks; it is not the Vercel runtime.

## One-command local setup

Requirements: Node `24.16.x`, npm `11+`, Git, and Docker Desktop for Supabase integration tests.

```powershell
npm run setup:local
```

This installs the locked dependencies and creates `.env.local` from `.env.example` if it does not exist. The safe default uses in-memory data, deterministic providers, and a local AAL2 test identity. No third-party data is sent.

```powershell
npm run dev
```

Open `http://127.0.0.1:5173`. For local Supabase instead of memory:

```powershell
npm run supabase:start
npm run supabase:reset
```

Then set `AUTH_MODE=supabase`, `VITE_AUTH_MODE=supabase`, and `DATA_MODE=supabase`, and provide the local Supabase URL/publishable key. Local Google OAuth remains off unless you explicitly add non-production credentials; CI creates synthetic local Auth users without adding a service-role key to the app runtime.

For the owner-operated Codex demo, start a second terminal before opening **Connections**:

```powershell
npm run codex:bridge
```

The worker binds only to `127.0.0.1:8790` and uses the Codex login already stored on this computer. Click **Connect local Codex** in Lumixia and keep the small loopback relay window open during the demo. The pairing token exists only inside that local window's memory: it is never returned to the production page, stored in browser storage, printed, or persisted. Sign-out closes the relay.

## Environment variables

| Variable                                   | Local default  | Preview / production purpose                                   |
| ------------------------------------------ | -------------- | -------------------------------------------------------------- |
| `APP_ENV`                                  | `local`        | `preview` or `production`; controls fail-closed validation     |
| `APP_URL`, `ALLOWED_ORIGIN`                | local URL      | Exact public URL and exact accepted browser origin             |
| `AUTH_MODE`, `VITE_AUTH_MODE`              | `local-demo`   | Must both be `supabase` in Preview/Production                  |
| `MODEL_PROVIDER_MODE`                      | `mock`         | `disabled`, `mock`, or `live`; production forbids `mock`       |
| `NOTION_PROVIDER_MODE`                     | `mock`         | `mock` locally and `live` in preview/production                |
| `DATA_MODE`                                | `memory`       | Must be `supabase` in production                               |
| `VITE_SUPABASE_URL`                        | empty          | Separate staging/production Supabase project URL               |
| `VITE_SUPABASE_PUBLISHABLE_KEY`            | empty          | Public API key; protected requests also carry the active JWT   |
| `OPENAI_API_KEY`                           | empty          | Required only when `MODEL_PROVIDER_MODE=live`                  |
| `OPENAI_MODEL`                             | `gpt-5.6`      | Interview and brief model                                      |
| `CODEX_MCP_MODE`                           | `enabled`      | Enables the authenticated owner-operated Codex MCP endpoint    |
| `CODEX_LOCAL_BRIDGE_MODE`                  | `enabled`      | Allows an AAL2 browser to submit validated local-Codex results |
| `NOTION_CLIENT_ID`, `NOTION_CLIENT_SECRET` | empty          | Notion public integration credentials                          |
| `NOTION_REDIRECT_URI`                      | local callback | Exact OAuth callback registered in Notion                      |
| `TOKEN_ENCRYPTION_KEY`                     | empty          | Base64-encoded 32-byte AES-256-GCM key                         |
| `OAUTH_STATE_SECRET`                       | empty          | At least 32 random characters for signed, expiring OAuth state |
| `SENTRY_DSN`, `VITE_SENTRY_DSN`            | empty          | Optional scrubbed error/tracing destination; Replay stays off  |

Production startup rejects a mock model, mock Notion, memory data, auth bypass, or missing security/provider credentials. Until the paid model smoke test is authorized, production uses `MODEL_PROVIDER_MODE=disabled` and constructs no OpenAI client. When `CODEX_LOCAL_BRIDGE_MODE=enabled`, a paired owner browser can still run the adaptive interview through local Codex; otherwise interview and generation return the explicit `503 MODEL_NOT_CONFIGURED`. Preview uses a deterministic model mock with live Notion and staging Supabase. The protected `GET /api/capabilities` endpoint reports model, Notion, MCP, and local-bridge support.

Vercel Preview derives its exact origin from the stable `VERCEL_BRANCH_URL` system variable (falling back to `VERCEL_URL`), while Production requires an explicit `APP_URL`. This keeps CORS and OAuth callbacks aligned across new commits without hard-coding a changing deployment URL.

## Codex connection without an OpenAI API key

Lumixia Brief also exposes an owner-operated MCP connection at `https://brief.z2zs.space/api/mcp`. It lets the signed

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 173 recognized source files, 778 KB.
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- OpenAI (technology) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- Supabase (technology) — detected in the code
- TypeScript (language) — detected in the code
- Docker (technology) — claimed on Devpost, not found in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- PostgreSQL (technology) — claimed on Devpost, not found in the code
- Vercel (technology) — claimed on Devpost, not found in the code
- AI coding agent: Codex — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (120 of 201)

```
.dockerignore
.env.example
.gitattributes
.github/dependabot.yml
.github/pull_request_template.md
.github/workflows/ci.yml
.github/workflows/deployment-evidence.yml
.github/workflows/uptime.yml
.gitignore
.npmrc
.prettierignore
.prettierrc.json
AGENTS.md
api/index.ts
api/tsconfig.json
CODEX_BUILD_LOG.md
CONTRIBUTORS.md
Dockerfile
docs/codex-build-ledger/BL-001-005-core-implementation.md
docs/codex-build-ledger/BL-006-private-repository-publication.md
docs/codex-build-ledger/BL-007-quality-and-defect-hardening.md
docs/codex-build-ledger/BL-008-modular-refactor.md
docs/codex-build-ledger/BL-009-open-source-publication.md
docs/codex-build-ledger/BL-010-live-integration-and-deployment.md
docs/codex-build-ledger/BL-011-production-runtime-recovery.md
docs/codex-build-ledger/BL-012-provider-runtime-recovery.md
docs/codex-build-ledger/BL-013-production-google-oauth-recovery.md
docs/codex-build-ledger/BL-014-backend-completion-without-openai.md
docs/codex-build-ledger/BL-015-live-integrations-and-release.md
docs/codex-build-ledger/BL-016-supabase-auth-migration.md
docs/codex-build-ledger/BL-017-clerk-decommission.md
docs/codex-build-ledger/BL-018-codex-mcp-integration.md
docs/codex-build-ledger/BL-019-local-codex-video-bridge.md
docs/codex-build-ledger/BL-020-production-loopback-relay.md
docs/codex-build-ledger/BL-021-build-week-demo-video.md
docs/codex-build-ledger/BL-022-codex-contributor-attribution.md
docs/codex-build-ledger/TEMPLATE.md
docs/decisions/0001-vercel-monorepo-shape.md
docs/decisions/0002-server-owned-alignment.md
docs/decisions/0003-mfa-rls-privacy.md
docs/decisions/0004-versioned-notion-handoff.md
docs/decisions/0005-atomic-operations-and-quality-gates.md
docs/decisions/0006-native-supabase-auth.md
docs/decisions/0007-owner-operated-codex-mcp.md
docs/decisions/0008-local-codex-video-bridge.md
docs/decisions/0009-loopback-popup-relay.md
docs/demo-script.md
docs/operations/codex-mcp.md
docs/operations/runbook.md
docs/security/development-advisories.md
docs/security/privacy-model.md
docs/submission-checklist.md
docs/submission/build-week-demo-kinetic.ass
docs/submission/build-week-demo-subtitles.srt
docs/submission/build-week-demo-voiceover.txt
eslint.config.js
index.html
knip.json
LICENSE
NOTICE
package.json
playwright.config.ts
README.md
scripts/auth-residue-policy.mjs
scripts/check-auth-residue.mjs
scripts/check-bundle.mjs
scripts/check-css.mjs
scripts/check-hosted-auth-residue.mjs
scripts/codex-bridge/prompts.ts
scripts/codex-bridge/runner.ts
scripts/codex-bridge/server.ts
scripts/codex-local-bridge.ts
scripts/run-integration.mjs
scripts/seed-founder-example.ts
scripts/setup-local.mjs
scripts/write-ci-evidence.ts
scripts/write-integration-evidence.ts
SECURITY.md
server/app.ts
server/config.ts
server/dev.ts
server/domain/confidence.ts
server/domain/interview.ts
server/domain/workflow.ts
server/errors.ts
server/http.ts
server/mcp/auth.ts
server/mcp/router.ts
server/mcp/tools.ts
server/observability/sentry.ts
server/providers/model.ts
server/providers/notion.ts
server/routes/briefs.ts
server/routes/capabilities.ts
server/routes/health.ts
server/routes/interview.ts
server/routes/notion.ts
server/routes/projects.ts
server/routes/request.ts
server/security/encryption.ts
server/security/headers.ts
server/security/identity.ts
server/services/briefs.ts
server/services/interview.ts
server/services/notion.ts
server/services/projects.ts
server/services/support.ts
server/store/memory.ts
server/store/supabase.ts
server/store/types.ts
shared/confidence.ts
shared/contracts.ts
shared/domain.ts
shared/model-prompts.ts
shared/telemetry.ts
src/App.tsx
src/auth/AuthBoundary.tsx
src/auth/AuthProvider.tsx
src/auth/client.ts
src/auth/index.ts
[81 more files omitted for size]
```

### Dependencies

- package.json: @eslint/js@^10.0.1, @modelcontextprotocol/sdk@1.29.0, @openai/codex@0.144.6, @playwright/test@^1.58.2, @sentry/node@10.65.0, @sentry/react@10.65.0, @supabase/supabase-js@2.110.5, @testing-library/jest-dom@^6.9.1, @testing-library/react@^16.3.2, @testing-library/user-event@^14.6.1, @types/express@^5.0.6, @types/node@^24.10.0, @types/react@^19.2.14, @types/react-dom@^19.2.3, @types/supertest@^6.0.3, @vitejs/plugin-react@^6.0.1, @vitest/coverage-v8@^4.0.18, concurrently@^9.2.1, eslint@^10.0.1, eslint-plugin-react-hooks@^7.0.1, eslint-plugin-react-refresh@^0.5.2, express@5.2.1, globals@^17.4.0, helmet@^8.1.0, jsdom@^28.1.0, knip@6.26.0, openai@6.46.0, postgres@^3.4.8, prettier@^3.8.1, rate-limiter-flexible@^8.0.1, react@19.2.7, react-dom@19.2.7, react-router-dom@^7.13.0, supabase@2.109.1, supertest@^7.2.2, tsx@^4.21.0, typescript@^6.0.2, typescript-eslint@^8.56.1, vite@8.1.4, vitest@^4.0.18, zod@^4.3.6

### Recent commits (newest first)

- docs: credit Codex and record demo (#38)
- docs: record BL-020 production rehearsal
- docs: record BL-020 pull request evidence
- fix: relay local Codex through pairing popup
- docs: link stable BL-019 CI evidence
- docs: record BL-019 review evidence
- feat: add owner-operated Codex interview bridge
- record Codex MCP production evidence
- record Codex MCP staging evidence
- fix MCP grant verification authorization
- test(rls): assert filtered Codex deletion
- fix(auth): bind Codex OAuth to AAL2 consent
- Record passing BL-018 CI
- Record BL-018 publication evidence
- Add owner-operated Codex MCP connection
- Record Supabase-only production release
- Record Clerk OAuth and DNS decommission
- Document legacy auth decommission evidence
- Gate hosted deployments against legacy auth residue
- Reject legacy auth residue in client bundles

## Key source files (fetched from GitHub, selected and truncated for size)

### SECURITY.md

```markdown
# Security policy

Lumixia Brief is a private Build Week prototype. Report vulnerabilities privately to the repository owner with the affected route/version and a sanitized reproduction. Do not include real prompts, answers, briefs, tokens, emails, user IDs, or exploit secrets.

Production release is blocked on zero critical findings, clean production dependencies, MFA/RLS verification, encrypted Notion credentials, and a rollback rehearsal. See `docs/security/privacy-model.md`.

```

### AGENTS.md

```markdown
# Codex working agreement

This repository is an OpenAI Build Week submission. Preserve these invariants:

1. Never log or commit prompts, interview answers, briefs, tokens, emails, user identifiers, secrets, or chain-of-thought.
2. Every material milestone must add or update a `BL-###` entry in `CODEX_BUILD_LOG.md` and, when useful, a detailed file under `docs/codex-build-ledger/`.
3. Commit messages must include `Codex-Session:` and `Build-Ledger:` trailers.
4. Approved brief content is immutable. Editing it creates a new draft version.
5. The server—not the model—calculates confidence and enforces stop rules.
6. Production must fail closed unless Supabase Auth AAL2, Supabase RLS, token encryption, and live Notion OAuth are configured. The model must be explicitly `disabled` or `live`; production mocks are forbidden.
7. Database changes are forward-only expand-and-contract migrations through the submission deadline.
8. Update tests and user-facing documentation with behavior changes.

Before handoff, run `npm run format:check`, `npm run lint`, `npm run typecheck`, `npm run test`, `npm run build`, and `npm run audit:prod`.

```

### Dockerfile

```
# syntax=docker/dockerfile:1.7
FROM --platform=$BUILDPLATFORM node:24.16.0-bookworm-slim AS build
ENV NPM_CONFIG_UPDATE_NOTIFIER=false
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts --no-audit --no-fund --loglevel=error
COPY . .
RUN npm run typecheck && npm run test && npm run build

FROM --platform=$TARGETPLATFORM node:24.16.0-bookworm-slim AS runtime
ENV NODE_ENV=production PORT=8787 NPM_CONFIG_UPDATE_NOTIFIER=false
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev --ignore-scripts --no-audit --no-fund --loglevel=error \
  && npm cache clean --force --loglevel=error
COPY --from=build /app/server ./server
COPY --from=build /app/shared ./shared
COPY --from=build /app/api ./api
USER node
EXPOSE 8787
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD node -e "fetch('http://127.0.0.1:8787/api/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"
CMD ["npm", "start"]

```

### package.json

```
{
  "name": "lumixia-brief",
  "version": "0.1.0",
  "description": "Turn vague project ideas into evidence-backed, human-approved briefs through an adaptive GPT-5.6 interview.",
  "private": true,
  "license": "Apache-2.0",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/Z2ZATL/lumixia-brief.git"
  },
  "bugs": {
    "url": "https://github.com/Z2ZATL/lumixia-brief/issues"
  },
  "homepage": "https://github.com/Z2ZATL/lumixia-brief#readme",
  "type": "module",
  "engines": {
    "node": "24.x",
    "npm": ">=11"
  },
  "scripts": {
    "dev": "concurrently -n web,api -c cyan,magenta \"vite\" \"npm:dev:api\"",
    "dev:web": "vite",
    "dev:api": "tsx watch --env-file-if-exists=.env.local server/dev.ts",
    "setup:local": "node scripts/setup-local.mjs",
    "build": "tsc -b && vite build",
    "bundle:check": "node scripts/check-bundle.mjs",
    "hosted:check": "node scripts/check-hosted-auth-residue.mjs",
    "preview": "vite preview",
    "start": "tsx --env-file-if-exists=.env.local server/dev.ts",
    "start:test:api": "tsx server/dev.ts",
    "format": "prettier --write .",
    "format:check": "prettier --check .",
    "lint": "eslint . --max-warnings=0",
    "knip": "knip",
    "css:check": "node scripts/check-css.mjs",
    "auth:check": "node scripts/check-auth-residue.mjs",
    "typecheck": "tsc -b --pretty false",
    "test": "vitest run",
    "test:watch": "vitest",
    "test:coverage": "vitest run --coverage",
    "test:ui": "vitest run --config vitest.ui.config.ts",
    "test:integration": "node scripts/run-integration.mjs",
    "test:e2e": "playwright test",
    "test:e2e:ui": "playwright test --ui",
    "supabase:start": "supabase start",
    "supabase:stop": "supabase stop",
    "supabase:reset": "supabase db reset",
    "seed:founder": "tsx --env-file-if-exists=.env.local scripts/seed-founder-example.ts",
    "codex:bridge": "tsx --env-file-if-exists=.env.local scripts/codex-local-bridge.ts",
    "vercel:dev": "npx --yes --loglevel=error vercel@56.1.0 dev",
    "audit:all": "npm audit --audit-level=high",
    "audit:prod": "npm audit --omit=dev --audit-level=high",
    "evidence": "tsx scripts/write-ci-evidence.ts",
    "evidence:integration": "tsx scripts/write-integration-evidence.ts",
    "quality:static": "npm run format:check && npm run lint && npm run typecheck && npm run knip && npm run css:check && npm run auth:check",
    "quality": "npm run quality:static && npm run test:coverage && npm run test:ui && npm run build && npm run bundle:check && npm run audit:all && npm run audit:prod"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "1.29.0",
    "@sentry/node": "10.65.0",
    "@sentry/react": "10.65.0",
    "@supabase/supabase-js": "2.110.5",
    "express": "5.2.1",
    "helmet": "^8.1.0",
    "openai": "6.46.0",
    "rate-limiter-flexible": "^8.0.1",
    "react": "19.2.7",
    "react-dom": "19.2.7",
    "react-router-dom": "^7.13.0",
    "tsx": "^4.21.0",
    "zod": "^4.3.6"
  },
  "devDependencies": {
    "@eslint/js": "^10.0.1",
    "@openai/codex": "0.144.6",
    "@playwright/test": "^1.58.2",
    "@testing-library/jest-dom": "^6.9.1",
    "@testing-library/react": "^16.3.2",
    "@testing-library/user-event": "^14.6.1",
    "@types/express": "^5.0.6",
    "@types/node": "^24.10.0",
    "@types/react": "^19.2.14",
    "@types/react-dom": "^19.2.3",
    "@types/supertest": "^6.0.3",
    "@vitejs/plugin-react": "^6.0.1",
    "@vitest/coverage-v8": "^4.0.18",
    "concurrently": "^9.2.1",
    "eslint": "^10.0.1",
    "eslint-plugin-react-hooks": "^7.0.1",
    "eslint-plugin-react-refresh": "^0.5.2",
    "globals": "^17.4.0",
    "jsdom": "^28.1.0",
    "knip": "6.26.0",
    "postgres": "^3.4.8",
    "prettier": "^3.8.1",
    "supabase": "2.109.1",
    "supertest": "^7.2.2",
    "typescript": "^6.0.2",
    "typescript-eslint": "^8.56.1",
    "vite": "8.1.4",
    "vitest": "^4.0.18"
  },
  "allowScripts": {
    "esbuild@0.28.1": true
  }
}

```

### api/index.ts

```typescript
import { createApp } from '../server/app.js';

export default createApp();

```

### src/main.tsx

```typescript
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { App } from './App';
import { AuthProvider } from './auth';
import { I18nProvider } from './i18n';
import './styles.css';

const sentryDsn = import.meta.env['VITE_SENTRY_DSN'] as string | undefined;
if (sentryDsn) {
  void import('./telemetry')
    .then(({ initializeTelemetry }) => initializeTelemetry(sentryDsn))
    .catch(() => undefined);
}

const content = (
  <BrowserRouter>
    <I18nProvider>
      <AuthProvider>
        <App />
      </AuthProvider>
    </I18nProvider>
  </BrowserRouter>
);
createRoot(document.getElementById('root')!).render(<StrictMode>{content}</StrictMode>);

```

### src/App.tsx

```typescript
import { Route, Routes } from 'react-router-dom';
import { AuthBoundary } from './auth';
import { AppLayout } from './components/Layout';
import { Brief } from './pages/Brief';
import { Interview } from './pages/Interview';
import { Landing } from './pages/Landing';
import { Projects } from './pages/Projects';
import { Settings } from './pages/Settings';
import { Security } from './pages/Security';
import { AuthCallback } from './pages/AuthCallback';
import { NotionCallback } from './pages/NotionCallback';
import { OAuthConsent } from './pages/OAuthConsent';

export function App() {
  return (
    <Routes>
      <Route path="/" element={<Landing />} />
      <Route path="/auth/callback" element={<AuthCallback />} />
      <Route path="/notion/callback" element={<NotionCallback />} />
      <Route
        path="/oauth/consent"
        element={
          <AuthBoundary>
            <OAuthConsent />
          </AuthBoundary>
        }
      />
      <Route
        element={
          <AuthBoundary>
            <AppLayout />
          </AuthBoundary>
        }
      >
        <Route path="/projects" element={<Projects />} />
        <Route path="/projects/:id/interview" element={<Interview />} />
        <Route path="/projects/:id/brief" element={<Brief />} />
        <Route path="/settings" element={<Settings />} />
        <Route path="/security" element={<Security />} />
      </Route>
      <Route path="*" element={<Landing />} />
    </Routes>
  );
}

```

### server/app.ts

```typescript
import express from 'express';
import { type AppConfig, loadConfig } from './config.js';
import { normalizeErrors } from './errors.js';
import {
  errorHandler,
  exactOrigin,
  notFound,
  perUserRateLimit,
  requestContext,
  requestDeadline,
  requireIdentity,
} from './http.js';
import { initializeSentry, mountSentryErrors } from './observability/sentry.js';
import { createMcpMetadataRouter, createMcpRouter } from './mcp/router.js';
import {
  DisabledModelProvider,
  MockModelProvider,
  OpenAIModelProvider,
  type ModelProvider,
} from './providers/model.js';
import { LiveNotionProvider, MockNotionProvider, type NotionProvider } from './providers/notion.js';
import { createBriefRouter } from './routes/briefs.js';
import { createCapabilityRouter } from './routes/capabilities.js';
import { createHealthRouter } from './routes/health.js';
import { createInterviewRouter } from './routes/interview.js';
import { createNotionRouter } from './routes/notion.js';
import { createProjectRouter } from './routes/projects.js';
import { securityHeaders } from './security/headers.js';
import { createIdentityVerifier, type IdentityVerifier } from './security/identity.js';
import { BriefService } from './services/briefs.js';
import { InterviewService } from './services/interview.js';
import { NotionService } from './services/notion.js';
import { ProjectService } from './services/projects.js';
import { MemoryProjectStore } from './store/memory.js';
import { SupabaseProjectStore } from './store/supabase.js';
import type { ProjectStore } from './store/types.js';

export interface AppDependencies {
  config: AppConfig;
  store: ProjectStore;
  model: ModelProvider;
  notion: NotionProvider;
  identity: IdentityVerifier;
}

export function createDependencies(config = loadConfig()): AppDependencies {
  const store =
    config.DATA_MODE === 'supabase'
      ? new SupabaseProjectStore(config.VITE_SUPABASE_URL!, config.VITE_SUPABASE_PUBLISHABLE_KEY!)
      : new MemoryProjectStore();
  const model =
    config.MODEL_PROVIDER_MODE === 'live'
      ? new OpenAIModelProvider(config.OPENAI_API_KEY!, config.OPENAI_MODEL)
      : config.MODEL_PROVIDER_MODE === 'disabled'
        ? new DisabledModelProvider()
        : new MockModelProvider();
  const notion =
    config.NOTION_PROVIDER_MODE === 'live'
      ? new LiveNotionProvider(
          config.NOTION_CLIENT_ID!,
          config.NOTION_CLIENT_SECRET!,
          config.NOTION_REDIRECT_URI!,
          config.OAUTH_STATE_SECRET!,
        )
      : new MockNotionProvider();
  return { config, store, model, notion, identity: createIdentityVerifier(config) };
}

export function createApp(dependencies = createDependencies()) {
  const { config, store, model, notion, identity } = dependencies;
  initializeSentry(config);

  const app = express();
  app.disable('x-powered-by');
  app.set('trust proxy', 1);
  app.use(requestContext(config));
  app.use(requestDeadline());
  app.use(securityHeaders(config));
  app.use(exactOrigin(config));
  app.use(express.json({ limit: '32kb', strict: true }));

  // Liveness and readiness must remain public and independent from identity providers.
  app.use('/api', createHealthRouter(config));
  if (config.codexAvailable) app.use(createMcpMetadataRouter(config));
  app.use('/api', createMcpRouter(config, store, model, identity));

  const protectedApi = express.Router();
  protectedApi.use(requireIdentity(config, identity), perUserRateLimit(config));
  protectedApi.use(createCapabilityRouter(config));
  protectedApi.use(createProjectRouter(new ProjectService(store)));
  protectedApi.use(createInterviewRouter(new InterviewService(store, model), config));
  protectedApi.use(createBriefRouter(new BriefService(store, model), config));
  protectedApi.use(createNotionRouter(new NotionService(store, notion, config), config));
  app.use('/api', protectedApi);

  mountSentryErrors(app, config);
  app.use(notFound, normalizeErrors, errorHandler);
  return app;
}

```

### src/auth/index.ts

```typescript
export { AuthBoundary } from './AuthBoundary';
export { AuthProvider } from './AuthProvider';
export { AuthContext } from './AuthProvider';
export type { AuthContextValue } from './types';

```

### scripts/codex-bridge/server.ts

```typescript
import { randomBytes, timingSafeEqual } from 'node:crypto';
import express, { type NextFunction, type Request, type Response } from 'express';
import { z } from 'zod';
import { projectSchema } from '../../shared/contracts.js';
import { CodexBridgeExecutionError, type CodexLocalRunner } from './runner.js';

const interviewRequestSchema = z
  .object({
    project: projectSchema,
    clientAnswerId: z.string().uuid(),
    answer: z.string().trim().min(1).max(10_000),
  })
  .strict();
const briefRequestSchema = z.object({ project: projectSchema }).strict();

export interface BridgeServerOptions {
  runner: Pick<CodexLocalRunner, 'analyzeInterview' | 'generateBrief' | 'model'>;
  token?: string;
  allowedOrigins: ReadonlySet<string>;
}

export function createCodexBridgeApp(options: BridgeServerOptions) {
  const token = options.token ?? randomBytes(32).toString('base64url');
  const app = express();
  let busy = false;
  app.disable('x-powered-by');
  app.use(responseHeaders);
  app.get('/pair', pairingPage(options.allowedOrigins, token, options.runner.model));
  app.use(cors(options.allowedOrigins));
  app.use(requireToken(token));
  app.use(express.json({ limit: '256kb', strict: true }));
  app.get('/health', (_req, res) => {
    res.json({ ready: true, model: options.runner.model });
  });
  app.post(
    '/v1/interview',
    asyncHandler(async (req, res) => {
      if (busy) return sendBusy(res);
      busy = true;
      const controller = requestController(req);
      try {
        const input = interviewRequestSchema.parse(req.body);
        const result = await options.runner.analyzeInterview(
          input.project,
          input.clientAnswerId,
          input.answer,
          controller.signal,
        );
        return res.json({ result, model: options.runner.model });
      } finally {
        busy = false;
      }
    }),
  );
  app.post(
    '/v1/brief',
    asyncHandler(async (req, res) => {
      if (busy) return sendBusy(res);
      busy = true;
      const controller = requestController(req);
      try {
        const input = briefRequestSchema.parse(req.body);
        const result = await options.runner.generateBrief(input.project, controller.signal);
        return res.json({ result, model: options.runner.model });
      } finally {
        busy = false;
      }
    }),
  );
  app.use(errorHandler);
  return app;
}

function responseHeaders(_req: Request, res: Response, next: NextFunction): void {
  res.set({
    'Cache-Control': 'no-store',
    'Cross-Origin-Resource-Policy': 'cross-origin',
    'Referrer-Policy': 'no-referrer',
    'X-Content-Type-Options': 'nosniff',
  });
  next();
}

function cors(allowedOrigins: ReadonlySet<string>) {
  return (req: Request, res: Response, next: NextFunction): void => {
    const origin = req.get('origin');
    if (!origin || isLoopbackSelfOrigin(req, origin)) {
      next();
      return;
    }
    if (!allowedOrigins.has(origin)) {
      res.status(403).json({ error: { code: 'BRIDGE_ORIGIN_DENIED' } });
      return;
    }
    res.set({
      'Access-Control-Allow-Origin': origin,
      'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
      'Access-Control-Allow-Headers': 'Authorization,Content-Type',
      'Access-Control-Allow-Private-Network': 'true',
      Vary: 'Origin',
    });
    if (req.method === 'OPTIONS') res.status(204).end();
    else next();
  };
}

function isLoopbackSelfOrigin(req: Request, origin: string): boolean {
  const host = req.get('host') ?? '';
  return /^127\.0\.0\.1:\d+$/.test(host) && origin === `http://${host}`;
}

function pairingPage(allowedOrigins: ReadonlySet<string>, token: string, model: string) {
  return (req: Request, res: Response): void => {
    const origin = typeof req.query['origin'] === 'string' ? req.query['origin'] : '';
    if (!allowedOrigins.has(origin)) {
      res.status(403).type('text').send('Pairing origin denied.');
      return;
    }
    const nonce = randomBytes(18).toString('base64');
    res.set(
      'Content-Security-Policy',
      `default-src 'none'; script-src 'nonce-${nonce}'; connect-src 'self'`,
    );
    res.type('html').send(pairingHtml(origin, token, model, nonce));
  };
}

function pairingHtml(origin: string, token: string, model: string, nonce: string): string {
  const readyMessage = JSON.stringify({ type: 'lumixia:codex-bridge:ready', model });
  const openerOrigin = JSON.stringify(origin);
  const bearerToken = JSON.stringify(token);
  return `<!doctype html><html><head><meta charset="utf-8"><title>Lumixia Codex Bridge</title></head><body><p>Local Codex is connected. Keep this window open during the Lumixia demo.</p><script nonce="${nonce}">const openerOrigin=${openerOrigin};const token=${bearerToken};const active=new Map();const reply=(value)=>{if(window.opener){window.opener.postMessage(value,openerOrigin);}};const errorCode=(body)=>body&&typeof body==='object'&&body.error&&typeof body.error.code==='string'?body.error.code:'BRIDGE_OPERATION_FAILED';reply(${readyMessage});window.addEventListener('message',async(event)=>{if(event.origin!==openerOrigin||event.source!==window.opener||!event.data||typeof event.data!=='object'){return;}const message=event.data;if(message.type==='lumixia:codex-bridge:cancel'&&typeof message.id==='string'){active.get(message.id)?.abort();return;}if(message.type!=='lumixia:codex-bridge:request'||typeof message.id!=='string'){return;}const route=message.action==='health'?'/health':message.action==='interview'?'/v1/interview':message.action==='brief'?'/v1/brief':null;if(!route){reply({type:'lumixia:codex-bridge:response',id:message.id,ok:false,code:'BRIDGE_INVALID_REQUEST'});return;}const controller=new AbortController();active.set(message.id,controller);try{const hasBody=message.action!=='health';const response=await fetch(route,{method:hasBody?'POST':'GET',cache:'no-store',credentials:'omit',headers:{Authorization:'Bearer '+token,...(hasBody?{'Content-Type':'application/json'}:{})},...(hasBody?{body:JSON.stringify(mes
[truncated — 1859 more characters]
```

[164 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]