diff --git a/.github/workflows/production-smoke.yml b/.github/workflows/production-smoke.yml new file mode 100644 index 0000000..8cf9316 --- /dev/null +++ b/.github/workflows/production-smoke.yml @@ -0,0 +1,73 @@ +name: Production smoke + +on: + schedule: + - cron: "*/15 * * * *" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: interviewthread-production-smoke + cancel-in-progress: true + +jobs: + smoke: + runs-on: ubuntu-latest + timeout-minutes: 5 + env: + SITE_ORIGIN: https://interviewthreadai.com + steps: + - name: Check production homepage and health endpoint + shell: bash + run: | + set -euo pipefail + + response_dir="$(mktemp -d)" + trap 'rm -rf "$response_dir"' EXIT + + request() { + local name="$1" + local url="$2" + curl \ + --silent \ + --show-error \ + --location \ + --connect-timeout 10 \ + --max-time 30 \ + --retry 2 \ + --retry-delay 2 \ + --retry-all-errors \ + --header "User-Agent: InterviewThread production smoke/1.0" \ + --dump-header "$response_dir/$name.headers" \ + --output "$response_dir/$name.body" \ + --write-out "%{http_code}" \ + "$url" + } + + home_status="$(request home "$SITE_ORIGIN/en")" + if [[ "$home_status" != "200" ]]; then + echo "Production homepage returned HTTP $home_status" + exit 1 + fi + if ! grep --quiet --ignore-case "InterviewThread" "$response_dir/home.body"; then + echo "Production homepage did not contain the expected product marker" + exit 1 + fi + + health_status="$(request health "$SITE_ORIGIN/api/healthz")" + if [[ "$health_status" != "200" ]]; then + echo "Production health endpoint returned HTTP $health_status" + exit 1 + fi + if ! jq --exit-status '.status == "ok" and (keys == ["status"])' \ + "$response_dir/health.body" >/dev/null; then + echo "Production health endpoint returned an unexpected payload" + exit 1 + fi + if ! grep --quiet --ignore-case '^cache-control:.*no-store' \ + "$response_dir/health.headers"; then + echo "Production health endpoint is missing no-store caching" + exit 1 + fi diff --git a/README.md b/README.md index 9b12bea..d414409 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ practice while keeping genuine gaps visible. Beta testers are welcome. **[Try InterviewThread](https://interviewthreadai.com/en)** · **[Join the founding beta](https://interviewthreadai.com/en/beta)** · **[Report a reproducible beta issue](https://github.com/weiyu1029/Interview_Thread_AI/issues/new?template=beta_feedback.yml)** -[Watch the 60-second walkthrough](https://interviewthreadai.com/interviewthread-60-second-walkthrough.mp4) · [Read the beta guide](docs/BETA_TESTING.md) · [Preview the first beta release](docs/releases/v0.1.0-beta.1.md) · [Contribute](CONTRIBUTING.md) · [Support](SUPPORT.md) · [Security](SECURITY.md) +[Watch the 60-second walkthrough](https://interviewthreadai.com/interviewthread-60-second-walkthrough.mp4) · [Read the beta guide](docs/BETA_TESTING.md) · [Production architecture](docs/platform_architecture.md) · [Operations runbook](docs/production_operations.md) · [Contribute](CONTRIBUTING.md) · [Security](SECURITY.md) > **Founding beta testers wanted.** We are inviting new graduates, career > changers, non-native English speakers, and candidates interviewing in the @@ -95,32 +95,37 @@ and feature experiments. It supports: ## Production web platform -The `platform/` directory is the production-oriented evolution path for free -accounts, permanent tracking, and open-source collaboration: +`platform/web` is the formal production full-stack application. Its localized +React interface and same-origin backend routes ship as one Cloudflare Sites +Worker, and Cloudflare D1 stores account-backed records. This keeps the browser, +API and OAuth callbacks on one origin and makes every frontend/backend release +atomic and reversible. -- a professional, responsive, emoji-free React / Next-compatible interface; -- guest analysis, 40 locale choices with eight reviewed end-to-end catalogs - and 32 community-beta catalogs, locale-aware AI output, worldwide recommendation filters, an - interactive Market Insights preview, a device-local tracker, evidence-aware - copilot, and feedback; -- FastAPI endpoints for identity, workspaces, persisted analyses, tracker items, - evidence-ranked job recommendations, market snapshots, application-mode - policies, analysis-linked chat, feedback, model discovery, usage, and plans; -- PostgreSQL-ready multi-tenant data models and role-based workspace access; -- Docker Compose for the web, API, and PostgreSQL services; -- one free, open-source access level with no checkout or paid entitlement. +The production platform includes: -Start the complete local stack: +- responsive web and mobile interfaces with 40 locale choices; +- guest mode plus Google, GitHub and LinkedIn OAuth accounts; +- server-side document parsing, evidence mapping, job adapters and mock + interview APIs; +- authenticated Azure speech routes with device fallback; +- D1-backed activity, beta and feedback records; +- privacy-minimized structured logs, a D1 health endpoint, scheduled smoke + checks and a private aggregate-only operator dashboard; +- one open-source access level with no checkout or paid entitlement. + +Run the production application locally: ```bash -cd platform -cp .env.example .env -# replace the legacy CAREERPROOF_JWT_SECRET compatibility variable before starting -docker compose up --build +cd platform/web +cp .dev.vars.example .dev.vars +npm install +npm run dev ``` -The web client is available at `http://localhost:3000` and the documented API at -`http://localhost:8000/docs`. +The optional `platform/api` FastAPI/PostgreSQL project is retained for +self-hosting experiments. It is not called by `interviewthreadai.com` and is +not a second production backend. See the [production architecture](docs/platform_architecture.md) +and [operations runbook](docs/production_operations.md). ## Open and local model ecosystem @@ -235,9 +240,9 @@ Interview_Thread_AI/ │ └── privacy.py ├── tests/ # deterministic matching and privacy tests ├── platform/ -│ ├── web/ # professional public React interface -│ ├── api/ # FastAPI multi-tenant service -│ └── docker-compose.yml # web + API + PostgreSQL +│ ├── web/ # production Cloudflare full-stack app + D1 +│ ├── api/ # optional FastAPI self-hosting prototype +│ └── docker-compose.yml # local prototype stack only ├── docs/ ├── .github/ # CI, issue forms, dependency updates ├── streamlit_app.py # public web entry point @@ -285,13 +290,13 @@ docker build -t interviewthread . docker run --rm -p 8501:8501 interviewthread ``` -### Multi-user platform +### Production platform -Use `platform/docker-compose.yml` for local evaluation. For public production, -use managed PostgreSQL, reviewed schema migrations, encrypted backups, a -rate-limiting proxy, and an asynchronous document queue before enabling open -registration. The Next.js workspace is the public product. The Streamlit -version remains a legacy reference implementation and feature incubator. +The public product is deployed from `platform/web` to Cloudflare Sites with D1, +reviewed environment configuration, immutable release versions, protected-branch +CI and rollback. `platform/docker-compose.yml` is for optional local prototype +evaluation only. The Streamlit version remains a legacy reference implementation +and feature incubator. ## Community maintenance diff --git a/docs/platform_architecture.md b/docs/platform_architecture.md index 0ec95a9..00241a2 100644 --- a/docs/platform_architecture.md +++ b/docs/platform_architecture.md @@ -1,87 +1,90 @@ -# Platform Architecture +# Production Platform Architecture -InterviewThread uses a progressive architecture: anyone can start without an -account, while people who need permanent history or collaboration can move into -the free account-backed platform without changing the -evidence model. +InterviewThread is a real full-stack application deployed at +`interviewthreadai.com`. The production source of truth is `platform/web`. +Its React interface and same-origin API routes run together as a Cloudflare +Sites Worker, with Cloudflare D1 as the managed relational database. + +The separate `platform/api` FastAPI/PostgreSQL project remains an optional +self-hosting prototype. The public website does not call it, and it must not be +treated as a second production backend. ## System boundary ```text -Next-compatible web client - ├─ guest evidence match and device-local tracker - └─ authenticated API client - │ - ▼ -FastAPI application - ├─ identity and workspace authorization - ├─ document extraction and PII redaction - ├─ canonical keyword and evidence engine - ├─ model-provider router - ├─ story and chat orchestration - ├─ global job-provider adapters and evidence ranking - ├─ market snapshot aggregation and provenance - ├─ application-mode safety policy - ├─ feedback and usage events - └─ open-source feature configuration - │ - ▼ -PostgreSQL - ├─ users, workspaces, and memberships - ├─ analyses and evidence-linked stories - ├─ tracker items and conversations - ├─ jobs, market metrics, and application preferences - ├─ feedback - └─ usage events +Browser + │ HTTPS, Cloudflare TLS, WAF and edge controls + ▼ +Cloudflare Sites Worker (`platform/web`) + ├─ localized React / server-rendered pages + ├─ OAuth callbacks and encrypted session cookies + ├─ same-origin API routes and request validation + ├─ in-memory document parsing and evidence analysis + ├─ aggregate-only operational events + └─ fixed-host outbound provider adapters + │ │ + ▼ └─ Azure Speech / Resend / approved ATS hosts +Cloudflare D1 + ├─ accounts and hashed sessions + ├─ user-requested activity history + ├─ beta status and product feedback + └─ aggregate event counts ``` -The deterministic evidence result is canonical. A language model can improve -organization and phrasing, but cannot create a supported claim or silently -change the underlying score. +This arrangement keeps browser, API and identity on one origin, removes a +public database endpoint, and lets one immutable release contain both frontend +and backend code. Cloudflare version history is the rollback boundary. -Live job data is adapter-based and administrator-configured. The current -implementation includes a fixed-host Adzuna adapter and storage contracts for -imported job postings and market snapshots. Provider coverage is displayed as -provider coverage, not as a census of the global labor market. +## Identity and access -## Identity and tenancy +- Guest mode is supported, but guest interview history is not saved. +- Google, GitHub and LinkedIn use OAuth; provider secrets stay in encrypted + production environment variables. +- Session tokens are random, stored only as hashes in D1 and sent in secure, + HTTP-only cookies. +- Paid speech endpoints require a signed-in account. +- The operator dashboard requires both a valid session and an exact email match + in `ADMIN_EMAILS`; non-operators receive a 404. -- Guest use is available for the first analysis and device-local tracking. -- Registration creates a personal workspace and an owner membership. -- Every persisted record belongs to a workspace. -- Owner, admin, member, and viewer roles provide the basis for team access. -- API authorization checks workspace membership instead of trusting IDs from - the browser. +## Data and privacy boundary -The current alpha accepts email and password credentials. Production should -add verified email, password reset, passkeys or a well-maintained identity -provider, session revocation, audit logging, and abuse controls before public -registration is opened. +Uploaded documents are parsed in memory. Raw resume files, job descriptions, +interview audio and transcripts are not copied into observability logs. +Product tracking is deliberately limited to bounded event names, counts, +status codes, latency, provider category, release ID and random request ID. -## Data lifecycle +The public health endpoint runs `SELECT 1` against D1 and returns only +`{"status":"ok"}` or `{"status":"unavailable"}`. It never reveals schema, +provider errors, account data or infrastructure credentials. -Uploaded files are parsed in memory. The API stores redacted text and derived -analysis only when an authenticated user asks to persist the result. Raw file -storage is deliberately absent from the alpha. If original-file storage is -introduced, it should use encrypted object storage, short-lived upload URLs, -malware scanning, explicit retention controls, and per-workspace deletion. +Original-file object storage is disabled. If it is ever introduced, require a +separate threat model, malware scanning, short-lived upload URLs, retention and +deletion controls, and encryption-at-rest review before production use. -Model keys arrive through `X-Model-Api-Key` and are not written to the database. -Long-lived bring-your-own-key storage should not be added without envelope -encryption, key rotation, access auditing, and a clear deletion flow. +## Abuse and provider boundary -## Scale path +- State-changing JSON routes require an exact same-origin request, the correct + media type and both declared and actual request-size limits. +- Speech-to-text validates authentication, locale, audio type and size. A local + user window is a second layer; global limits belong at the Cloudflare edge. +- Text-to-speech is authenticated and falls back to device speech when the + managed provider is unavailable. +- Contact delivery uses a honeypot, bounded fields, a fixed recipient map and a + server-side Resend key. User input cannot choose arbitrary recipients. +- Job adapters may call only documented, fixed provider hosts. -1. Keep synchronous extraction and analysis while traffic is low. -2. Add a queue for OCR, large documents, and batch analyses. -3. Add Redis only when distributed rate limits, job locks, or short-lived caches - are actually required. -4. Add managed object storage only for features that require original files. -5. Split services by operating need, not by feature count. +## Monitoring and release -## Open-source access boundary +- `/api/healthz` verifies Worker-to-D1 health and disables caching. +- A scheduled GitHub Actions smoke check requests the English landing page and + health endpoint every 15 minutes without credentials. +- Worker logs are structured and privacy-minimized; the private operator page + exposes aggregate counts only. +- Production variables are managed through Sites. Secrets never enter source, + logs, build artifacts or the browser bundle. +- Releases are built from the reviewed Git commit, saved as an immutable Sites + version, deployed, smoke-tested and rolled back to the previous version if a + release gate fails. -The public product exposes one free access level. Workspace roles protect data -and collaboration boundaries, not commercial entitlements. The evidence engine, -self-hosting path, data export, application modes, and safety rules remain open -source and are never restricted by account status. +See [Production Operations](production_operations.md) for the release, +incident and recovery checklist. diff --git a/docs/production_operations.md b/docs/production_operations.md new file mode 100644 index 0000000..3a7928b --- /dev/null +++ b/docs/production_operations.md @@ -0,0 +1,88 @@ +# Production Operations + +This runbook covers the Cloudflare-hosted InterviewThread production system at +`https://interviewthreadai.com`. + +## Ownership map + +| Layer | Production service | Source of truth | +|---|---|---| +| DNS, TLS, WAF, edge controls | Cloudflare | Cloudflare zone settings | +| Frontend and backend | Cloudflare Sites Worker | `platform/web` | +| Relational storage | Cloudflare D1 | reviewed migrations and D1 backups | +| OAuth | Google, GitHub, LinkedIn | provider console + encrypted Sites variables | +| Speech | Azure Speech | encrypted Sites variables | +| Website email | Resend | verified sending subdomain + encrypted Sites key | +| Source and CI | GitHub | protected `main` branch | + +`platform/api` is not in the public production request path. + +## Required production configuration + +Non-secret values: + +- `APP_BASE_URL=https://interviewthreadai.com` +- `NEXT_PUBLIC_SITE_URL=https://interviewthreadai.com` +- `APP_RELEASE=` +- `ADMIN_EMAILS=` +- `EMAIL_FROM=InterviewThread Website ` +- `EMAIL_FEEDBACK_TO=feedback@interviewthreadai.com` +- `EMAIL_PARTNERSHIPS_TO=partnerships@interviewthreadai.com` + +Encrypted secrets: + +- `AUTH_SECRET` +- OAuth client IDs and client secrets for every enabled provider +- `AZURE_SPEECH_KEY` when managed speech is enabled +- `RESEND_API_KEY` when background email is enabled + +Never print or copy secret values into tickets, chat, logs or CI output. + +## Release checklist + +1. Review the exact Git diff and confirm it contains no secrets or unrelated + database changes. +2. Run lint, the full web build and test suite, Python checks and dependency + audit. +3. Push without force to a feature branch and wait for protected-branch CI. +4. Merge through the protected branch. Do not weaken branch protection. +5. Build the exact reviewed `main` commit and save an immutable Sites version. +6. Deploy that saved version without rerunning historical database migrations. +7. Verify `/en`, `/en/account`, `/api/healthz`, security headers and operator + access. Confirm a non-operator cannot open the dashboard. +8. Watch Worker errors and health checks after release. + +## Alert and incident sequence + +Treat health-check failure, repeated OAuth failure, a provider error spike or a +privacy/security report as an incident. + +1. Record the first failing release ID and UTC time. +2. Check the public health endpoint and privacy-safe Worker logs. +3. Determine whether the fault is edge, Worker, D1 or an outbound provider. +4. Disable only the affected optional provider or feature when possible. +5. Roll back to the last known-good immutable Sites version when core use is + affected. Do not run a compensating schema change during rollback. +6. Verify recovery from an unauthenticated browser and a signed-in test account. +7. Document impact, root cause, corrective action and the test that prevents + recurrence. Never paste user content into the incident record. + +## Data recovery + +- D1 is the only production relational source of truth. +- Before any schema change, verify a recent restorable backup or export and test + the migration against a non-production database. +- Prefer additive, backward-compatible changes. Application rollback must not + depend on destructive down-migrations. +- Resume files, audio and transcripts are intentionally not retained as + observability data and therefore are not part of backup recovery. + +## Privacy-safe monitoring + +Allowed fields are random request ID, bounded route/outcome/provider enums, +HTTP status, duration and release ID. Do not log URLs with query strings, +headers, IP addresses, user agents, account identifiers, email addresses, +uploaded evidence, messages, audio, transcripts or raw provider errors. + +The private operator dashboard may show aggregate account, event, beta and +feedback counts. It must not become a content browser. diff --git a/platform/web/.dev.vars.example b/platform/web/.dev.vars.example index ada0168..fe89f56 100644 --- a/platform/web/.dev.vars.example +++ b/platform/web/.dev.vars.example @@ -2,6 +2,8 @@ # AUTH_SECRET must be a cryptographically random value of at least 32 characters. AUTH_SECRET=replace-with-at-least-32-random-characters APP_BASE_URL=http://localhost:3001 +# Immutable release label used only in privacy-safe health and operations logs. +APP_RELEASE=local GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= @@ -12,10 +14,14 @@ GITHUB_CLIENT_SECRET= LINKEDIN_CLIENT_ID= LINKEDIN_CLIENT_SECRET= -# Optional server-side neural read-aloud. Keep the subscription key private. -# Without both values, interview questions fall back to the browser/device voice. +# Optional server-side Microsoft Azure Speech. Keep the subscription key private. +# Neural read-aloud needs the key and region. Signed-in final transcript +# correction needs the key and resource endpoint. Without the required values, +# voice answers keep the browser/device transcript and read-aloud uses the +# browser/device voice. AZURE_SPEECH_KEY= AZURE_SPEECH_REGION= +AZURE_SPEECH_ENDPOINT=https://replace-with-resource.cognitiveservices.azure.com # Transactional website email. Keep the API key secret and verify the sender # domain with the provider before using the production address. @@ -23,3 +29,7 @@ RESEND_API_KEY= EMAIL_FROM=InterviewThread Website EMAIL_FEEDBACK_TO=feedback@interviewthreadai.com EMAIL_PARTNERSHIPS_TO=partnerships@interviewthreadai.com + +# Comma-separated operator accounts allowed to view aggregate-only metrics. +# Use exact email addresses; wildcards are intentionally unsupported. +ADMIN_EMAILS=contact@interviewthreadai.com,wy.alice.chen@gmail.com diff --git a/platform/web/README.md b/platform/web/README.md index c7d3a5c..0b9c725 100644 --- a/platform/web/README.md +++ b/platform/web/README.md @@ -72,8 +72,25 @@ unavailable, the client falls back to the browser or device voice. Read-aloud does not send the resume, job description, interview answer, transcript, or raw voice recording to Azure Speech. Voice recognition remains a -separate browser capability. Keep the public privacy policy and FAQ aligned if -this data flow changes. +separate capability. Keep the public privacy policy and FAQ aligned if this +data flow changes. + +## Two-stage interview voice answers + +Voice answers show provisional captions from the browser or device while the +candidate speaks. For signed-in users, when `AZURE_SPEECH_KEY` and +`AZURE_SPEECH_ENDPOINT` are configured, the completed recording is sent to the +Microsoft Azure Speech Fast Transcription endpoint for a final correction pass. +The request contains the recorded answer audio, one of the 40 supported locale +codes, and at most 80 short vocabulary hints derived from the selected role, +resume, and job description. It never sends the full resume or job description +as transcription context. + +The transcription route is same-origin, sign-in protected, size and media-type +limited, and returns private no-store JSON. InterviewThread does not persist or +log the raw recording. The user can edit the final text before submitting it. +Guest mode remains browser-only, and any unavailable or failed cloud correction +keeps the existing browser/device transcript instead of clearing the answer. Resume text, job descriptions, tracker items, and Story Signal alert settings remain on the device until an authenticated persistence feature explicitly diff --git a/platform/web/app/ContactInboxForms.tsx b/platform/web/app/ContactInboxForms.tsx index d54137c..6a43842 100644 --- a/platform/web/app/ContactInboxForms.tsx +++ b/platform/web/app/ContactInboxForms.tsx @@ -145,8 +145,17 @@ function ContactInboxForm({