Repository navigation
Launch prep: production-ready backend, security fixes, truthful web app, university admin service - #8
NiksheyYadav wants to merge 106 commits into
Conversation
Adds the missing resume-optimization step from the primary flow (docs/01 step 5) as the smallest safe version: deterministic, advisory, and stateless. - @atlas/ai: analyzeResumeForJob compares a parsed resume with one job's keywords (explicit skills plus vocabulary terms in title/description), flags weak bullets (duty-style openers, no measurable result, too short/long), and folds in the resume-level ATS hints. It never writes resume text or asserts a skill/metric the candidate did not state. - @atlas/ai: expose containsKeyword so non-vocabulary job skills get the same boundary-aware match as extractSkillKeywords (now built on it). - @atlas/types: report schema and optimize request/response contracts. - profile-service: POST /v1/resumes/:id/optimize. Owner-scoped (404 for other users' resumes), 409 for unparsed resumes, 404 for unknown jobs, 400 on malformed ids/bodies. Reads jobs read-only, as applications-service and matching-service already do. - docs: document the endpoint (docs/04) and add docs/15 progress report with the verified baseline and known gaps. - api/: regenerate committed bundles (profile gains the route; the rest gain only the new shared schemas; jobs picks up the skills refactor). Verified: typecheck and lint clean; 183 tests pass (18 new) with the DB-backed suites run against Postgres 16 + pgvector. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
createVercelHandler(app) is the dispatch logic services/auth/src/index.ts inlines today: await Fastify's ready(), then emit the Node request/response pair onto app.server. It takes a structural type so @atlas/utils does not gain a fastify dependency, starts plugin boot eagerly so it overlaps cold-start init, and guards that eager promise so a boot failure surfaces on each invocation rather than as an unhandled rejection at cold start. withServerlessEnvDefaults() feeds the services' env loaders a placeholder PORT when VERCEL is set. packages/config's httpEnvSchema requires PORT with no default, but a function never binds a port, so without it every module throws a ConfigError at load. Outside Vercel the source is returned untouched, so local dev and tests keep strict validation, and nothing but PORT is defaulted. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
These entrypoints wrapped startup in main() and only ever called app.listen(), with no default export, so as Vercel functions (see e886019 for the failure mode on auth) they could not serve a request. Convert them to the auth pattern: construct env/logger/db/broker at module scope, export `app`, export the default handler from createVercelHandler, and install shutdown handlers and listen() only when VERCEL is unset, so `pnpm dev:<service>` behaves as before. Env loading goes through withServerlessEnvDefaults() so a missing PORT does not fail module load on Vercel. Each service gets an entrypoint test that loads the module with VERCEL=1 and no PORT, asserts the default export is a function and that nothing binds a socket, serves /health through the handler (503 with an unreachable database, 200 with ATLAS_TEST_DATABASE_URL), checks that other missing config still fails fast, and checks that local mode still listens. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
…andler Same conversion as the previous commit for the services that also run an event consumer. registerConsumers() still runs in every runtime, now via top-level await after the app is built, so a failed subscription fails module load loudly like a bad config does; subscriptions are kept for the local shutdown path, which stops them before closing the app, as before. The entrypoint tests additionally assert the consumers are still registered when the module is loaded with VERCEL=1. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
The DB-backed integration suites need more than a bare Postgres: the tests insert into Supabase's auth.users, which fires handle_new_user from packages/db/supabase/auth-hooks.sql, and that file references the supabase_auth_admin, authenticated and anon roles. infra/ci/supabase-stub.sql provides just those pieces (NOLOGIN roles, schema auth, and an auth.users table with the columns the tests and the trigger use). It is idempotent and refuses to run if the database looks like a real Supabase project (auth.schema_migrations or auth.identities present). scripts/ci/setup-test-db.sh runs the whole recipe against DATABASE_URL: the stub, pnpm db:migrate, then auth-hooks.sql. It is re-runnable: auth-hooks.sql adds a foreign key without IF NOT EXISTS, so the script drops that constraint first. CI and developers use the same script. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
The backend job had no Postgres, so 10 test files (58 tests) self-skipped and still reported green. Add a pgvector/pgvector:pg17 service container (same image as docker-compose.yml) with a TCP health check, prepare it with scripts/ci/setup-test-db.sh, and set ATLAS_TEST_DATABASE_URL so the suites actually run. The test step is renamed and the stale 'skipped' comment removed. Also set workflow-level permissions to contents: read and a 20 minute job timeout. The database credentials are throwaway values for an ephemeral service container, not secrets. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
Vercel deploys the committed api/*/index.js bundles, not the output of a build step (see the header of scripts/bundle-backend.mjs), so a source or packages/* change that is merged without re-bundling ships old code. Rebuild in CI and fail if the result differs from what is committed. New bundles are registered with git add --intent-to-add first: a plain git diff --exit-code does not see untracked files, so a newly added service would pass unnoticed. The failure message says to run node scripts/bundle-backend.mjs and commit api/. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
Dependabot: weekly npm (pnpm workspace root) and github-actions updates. Minor and patch bumps are grouped per dependency type; majors open their own PRs. Open-PR limits are 5 and 3, and commit messages use conventional prefixes. Audit: a separate job runs pnpm audit --prod --audit-level=high with continue-on-error, so it reports without gating. It reads only the lockfile and needs no install. It does exit non-zero today (3 critical and 5 high advisories on production dependencies), which is why it must not block the build. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
… first run) Update the Verified baseline bullets and the Known gaps CI bullet: CI now provisions Postgres and sets ATLAS_TEST_DATABASE_URL, and also checks bundle freshness and runs an advisory audit. All of it is marked pending first CI run because it was rehearsed locally on Postgres 16 and has not been observed in GitHub Actions. Skip counts corrected to the measured 10 files / 58 tests. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
services/auth called loadAuthServiceEnv() with no serverless default, so on Vercel (which provides no PORT) it threw a ConfigError at module load unless PORT was set by hand in the project env. Use withServerlessEnvDefaults() like the other services and replace the inline handler with the shared createVercelHandler, which also guards a failed boot against an unhandled rejection at cold start. Adds the service's first test, an entrypoint test mirroring the other services (loads without PORT, exports a handler, serves /health, local mode still listens). Removing the PORT default fails 4 of its 5 cases. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
Rebuild api/*/index.js with scripts/bundle-backend.mjs so the deployed functions carry the new Vercel handler exports (createVercelHandler, withServerlessEnvDefaults) for all nine services. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
The job page needs to know whether the signed-in user already has an application for a job; without a filter it had to walk up to five pages of 100. GET /v1/applications?job_id=<uuid> restricts the caller's own list. It is rejected (400) with scope=organization, because a per-job filter would let university staff ask who applied to a given job, which is exactly the job-level detail staff must not see. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
…ages page Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
…anagement POST|GET /internal/jobs/ingest-sources, secured by a CRON_SECRET bearer exactly like the drain routes (constant-time compare, 503 when unset) and registered outside the rate limiter. It reads enabled job_sources least-recently-fetched first, a few at a time, inside a wall-clock budget (default 20s of the 30s function limit). A source that fails is recorded (short, fixed, scrubbed message in last_error) and skipped; the run carries on. Known listings are settled with a statement per few hundred postings, so a steady-state re-read of a 700-posting board is ~300ms locally and a second run inserts nothing. Postings a successfully fetched source no longer lists are marked inactive, scoped to that provider and company and judged by last_seen_at against the database clock. Nothing is deactivated for an empty result, an incomplete run, a run with postings that could not be stored, or a sharp drop from a sizeable board (applied one run later if it is real). A failed fetch never reaches the sweep. GET and POST /v1/jobs/sources let platform administrators list, add, enable and disable boards; board tokens are validated to a URL-safe charset, and two boards of one provider cannot share a company name. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
…, funnel and paging helpers - api.ts: ApiError carries request id, details and Retry-After; non-JSON and empty error bodies no longer throw; network failures become a typed error. - errors.ts: describeApiError maps every status the roster API can answer (401/403/404/409/413/415/429/5xx, network, bad response) to vetted copy and a recovery action, never echoing server text. - contracts.ts: runtime mirror of packages/types/src/admin.ts. Turbopack cannot resolve @atlas/types' NodeNext .js specifiers at runtime, so the schemas are copied here, guarded by compile-time assignability checks and a parity test. - csv.ts/import-csv.ts: client pre-flight of a roster file (size, type, UTF-8, header, 5000-row cap) using a port of the server's reader, the template, row error wording and an honest import summary. csv-parity.test.ts runs the client and server readers over the same inputs. - Pagination cursor stack, roster/funnel query builders, safe next-path, filename sanitising. 209 tests under apps/admin/test. - Drop the analytics and org-wide applications service modules: the new console does not use them (see the dashboard commit). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
…ata moved out of the console - Guard sends signed-out visitors to /login?next=<path> and returns them there through a same-origin-only redirect check. - /dashboard resolves the organization from the sign-in claim (the user's oldest membership, the only thing /v1/auth/me exposes); an owner/admin claim opens it, anything else lands on a chooser that says plainly when the account is not a member of a university organization. An organization id can also be pasted; the API still authorizes it (404 non-member, 403 wrong role/type). - Matching-model health (learning-service, platform-admin only) is now a labelled platform-wide page that degrades to an explanation on 403 instead of sitting in the university console. The old dashboard's raw application list and event counts are gone: they were keyed to the token's organization, not the selected one, and listed opaque ids. - Privacy hardening: no-store/no-frame/no-referrer headers on pages, noindex, and the error boundary logs only the error name and digest. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
…and CSV export Search and filters live in component state only (never the URL or browser storage: a search term is often an email). Pagination is forward-only as the API is; a cursor stack provides Previous. Students who are not members of the organization show a dash plus screen-reader text rather than zeros. Export fetches the roster with the bearer token and saves it as a blob under a sanitised server-provided file name. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
… and a truthful summary File type, size, UTF-8 and structure (header, 5000-row cap, quoting) are checked in the browser before upload; the file is sent as text/csv with an Idempotency-Key that is reused only for retries of the same file. The result lists every outcome bucket with the API's own counts, groups and lists row errors without echoing submitted values, and states plainly that rows without an Atlas account are skipped and nobody is emailed or invited. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
The funnel lists the API's counts per stage as an accessible table with decorative bars, with the activity window and the as-of-now/in-window basis stated per row; no percentages or other numbers are derived. The audit log page says it is unavailable (admin-service writes entries but exposes no read endpoint) and describes only what is recorded today. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
… and outreach honesty Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
|
Deployment failed for project atlas-analytics with the following error: Learn More: https://vercel.com/niksheyyadavs-projects?upgradeToPro=build-rate-limit |
… and docs .github/workflows/ingest-jobs.yml calls POST /internal/jobs/ingest-sources hourly (and on workflow_dispatch, optionally for one source_id). Like drain-events.yml it is inert until the ATLAS_API_BASE_URL variable and the CRON_SECRET secret exist; an optional ATLAS_JOBS_URL variable overrides the base URL when the jobs service is its own Vercel project. The route itself is served by the jobs Fastify app, and vercel.json gains the /internal/jobs/:path* rewrite for the single-project layout. packages/db/seed/job-sources.example.sql seeds a handful of real public boards (tokens verified against the live endpoints on 2026-10-27) with ON CONFLICT DO NOTHING; a test pins that it applies cleanly, twice, and only inserts. docs/04 documents the endpoints, what is read, and exactly when postings are deactivated. .env.example gains JOB_SOURCE_CONTACT and the CRON_SECRET note. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
…r deleting a user Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
…nt export/delete Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
The connectors service is not deployed and is optional per project rules, so the nav no longer links to a page that only says it is being surveyed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
The company contacts route exists in the referrals service but the single-project layout had no rewrite for it, so it answered 404. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
…/delete Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
|
The twelve Each one reports
Generated by Claude Code |
…and the HTML stripper Recommendations read precomputed scores joined to jobs, so a posting its source stopped listing (now marked inactive by scheduled ingestion) would have kept appearing in people's recommendations. listForUser now excludes inactive jobs, with a test. Adds direct tests for the shared ingestPosting(): outcomes, a burst of identical postings settling to one insert with no error (the unique-violation race handler moved here from the route and had no test), the edit-in-place opt-in, and the caller-resolved company. Mutation checks showed the HTML stripper's linear-time tests were too small to fail a quadratic version, so the hostile inputs are now sized to take seconds if the cutoffs regress. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
Resolves docs/04_api_design.md (outreach and job-source sections kept) and documents the assisted-apply application endpoints. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
The catalog now fills from job boards, so a job card with only a title and no employer is not usable. company_name is optional in the shared Job type; the jobs service fills it on every read, and the web list and detail pages show it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
…ngest route; rebundle Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
|
Deployment failed for project atlas-jobs with the following error: Learn More: https://vercel.com/niksheyyadavs-projects?upgradeToPro=build-rate-limit |
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1 # Conflicts: # apps/web/src/app/register/page.tsx # apps/web/src/lib/auth-context.tsx
…ng correctly The smoke script ran against the Vercel preview of 02e25b9: 15 passed, 5 warnings (CRON_SECRET unset), 0 failed. The progress report now says what that does and does not prove, and notes the atlas-jobs per-service failure. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
|
Generated by Claude Code |
Assisted apply is the launch model, confirmed by the owner. Automatic submission stays off until an employer-supplied ATS key and a submission worker exist. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1
Summary
Integration branch for the 2026-11-17 launch (individuals plus one university pilot). It started as job-aware resume optimization and grew into the launch-readiness work. Owner-only steps are in
docs/16_launch_checklist.md; status is indocs/15_progress.md.Works on Vercel now
createVercelHandler); only auth did before.MESSAGING_DRIVER=postgres, the default on Vercel) with drain endpoints protected byCRON_SECRET, driven by a scheduled GitHub workflow (Vercel Hobby has no sub-daily cron). Consumers are idempotent under at-least-once delivery.next,sharp,zodandsource-map-jsare patched.Jobs
job_sourcestable, migration0004, platform-admin/v1/jobs/sources, hourlyingest-jobs.yml). Postings a board stops listing are deactivated and no longer recommended.Security
PLATFORM_ADMIN_USER_IDSallowlist.rls.sqlcloses the Supabase Data API: RLS on every public table and noanon/authenticatedgrants.Product honesty
mode: autois rejected unlessAPPLICATIONS_AUTO_MODE_ENABLED=true. The web app has a job page, a resume check against the job, and an assisted-apply panel ("Atlas does not submit applications for you").OUTREACH_DELIVERY_ENABLED=true(nothing delivers email); users copy a draft, send it themselves and press "I sent this".Privacy
GET /v1/auth/me/exportdownloads everything held about the user;DELETE /v1/auth/me({"confirm":"DELETE"}) deletes the account and its data in one transaction and then removes resume files. Both are in the web app under "Your data".University pilot
admin-service: org-scoped roster list and CSV import, funnel, CSV export (formula-injection safe), audit log; authorization comes fromorganization_membersin the database, non-members get 404.apps/adminis now the staff console: organization chooser, roster with search and export, CSV import with per-row results, funnel.Resume optimization (the original change):
POST /v1/resumes/:id/optimize, advisory and stateless; it never rewrites resume content.Verification
pnpm typecheckandeslintclean; 118 test files / 1,704 tests pass against Postgres 16 with the real migrations,auth-hooks.sqlandrls.sql.mainwas merged into the branch (it had gained a sign-in error fix and a progress report); the two web files that conflicted were resolved keeping this branch's confirm-email and password-reset flows plusmain's friendlier error messages.43c5972: backend (typecheck, lint, DB tests, bundle freshness), web and admin builds, and the dependency audit are all green. The admin build failed once on this commit withnext/font/googlefailing to resolve (a Google Fonts fetch duringnext build); the commit only changes a doc and the smoke script, and the single re-run passed with no change. The earlier merge commit02e25b9was green first time. If this recurs, make the admin and web builds independent of Google Fonts (next/font/local).next buildforapps/webandapps/adminverified locally.02e25b9built.scripts/smoke.mjsagainst theatlas-backendandatlas-webpreviews: 15 passed, 5 warnings, 0 failed. All nine/v1services answer401from the service itself (the committed bundles boot and the rewrites route), the web pages render, and CORS allows the web origin. The five warnings areCRON_SECRETnot being set on that preview.401comes before any database or Supabase call, so sign-in, uploads, matching, ingestion and account deletion are still unverified against real Supabase (the project was paused when last checked).atlas-backend,atlas-admin,atlas-learning,atlas-profileandatlas-referralsdeployed on02e25b9.atlas-jobs(a superseded per-service project) failed to build there; its log is not reachable from the build session (npx vercel inspect dpl_95uCaJRPMJ5m7CJjnxxuVpBXFjpQ --logs), and the same jobs code is deployed and answering inatlas-backend. Each push builds twelve Vercel projects, so the daily quota is exhausted again within a few pushes (atlas-applicationsandatlas-profilewere rate limited on43c5972).Needs the owner before launch
auth-hooks.sql(re-apply) thenrls.sql; runpnpm db:migratefor0004.CRON_SECRETon jobs too,PLATFORM_ADMIN_USER_IDS, storage keys on profile and auth) and the GitHub variables and secret for the drain and job-ingest workflows.docs/15_progress_report.mdagree); add the first job-board sources and the pilot organization.🤖 Generated with Claude Code
https://claude.ai/code/session_01BdqVHQRXj76uudaef6gTD1