Skip to content

docs: add SaaS web app UI design spec and implementation plan#387

Closed
tanmay7264 wants to merge 1 commit intosantifer:mainfrom
tanmay7264:claude/sleepy-lederberg-c83f49
Closed

docs: add SaaS web app UI design spec and implementation plan#387
tanmay7264 wants to merge 1 commit intosantifer:mainfrom
tanmay7264:claude/sleepy-lederberg-c83f49

Conversation

@tanmay7264
Copy link
Copy Markdown

@tanmay7264 tanmay7264 commented Apr 20, 2026

What does this PR do?

Adds a UI design spec and 14-task implementation plan for a career-ops SaaS web application — a multi-tenant browser interface that brings the career-ops CLI pipeline to any user without requiring local setup.

Related issue

Type of change

  • Bug fix
  • New feature
  • Documentation / translation
  • Refactor (no behavior change)

Summary

Two documents added to docs/superpowers/:

specs/2026-04-20-career-ops-saas-ui-design.md — Full UI design spec covering:

  • Tech stack: Next.js 14 App Router · NextAuth.js (Google OAuth) · Prisma + Supabase · Vercel AI SDK · Claude 3.5 Sonnet · shadcn/ui
  • Database schema (8 Prisma models: User, Profile, Application, Report, PipelineItem + NextAuth models)
  • All pages & routes: landing, onboarding wizard (4-step), dashboard, applications tracker, pipeline inbox, evaluate JD, report viewer, settings
  • Auth & multi-tenant data isolation design
  • Streaming AI evaluation flow (Blocks A–G with SCORE/LEGITIMACY sentinels)

plans/2026-04-20-career-ops-saas-build.md — 14-task TDD implementation plan with exact file paths, code snippets, and test commands for each task.

The full implementation has been built and tested following this plan (34 passing tests, clean production build). The web app repo (career-ops-web) is ready for Vercel deployment.

Checklist

  • I have read CONTRIBUTING.md
  • My PR does not include personal data (CV, email, real names)
  • My changes respect the Data Contract (no modifications to user-layer files)
  • My changes align with the project roadmap
  • I ran node test-all.mjs and all tests pass — N/A (docs only, no script changes)

Questions? Join the Discord for faster feedback.

Summary by CodeRabbit

  • Documentation
    • Added comprehensive implementation plan with development roadmap and feature checklist for the Career-Ops SaaS application
    • Added detailed UI design specification covering end-to-end user workflows, data models, API contracts, and deployment strategy

Adds the UI design spec and full implementation plan for the
career-ops SaaS web application — a multi-tenant browser-based
interface for the career-ops CLI pipeline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 20, 2026 10:58
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 20, 2026

📝 Walkthrough

Walkthrough

Two comprehensive documentation files were added defining a complete multi-tenant Career-Ops SaaS web application. The specification covers the tech stack, data models, routes, and UI flows. The implementation plan provides a 14-task checklist with architecture details and code scaffolding guidance.

Changes

Cohort / File(s) Summary
Career-Ops SaaS Documentation
docs/superpowers/plans/2026-04-20-career-ops-saas-build.md, docs/superpowers/specs/2026-04-20-career-ops-saas-ui-design.md
New documentation files defining a multi-tenant SaaS architecture (Next.js 14 App Router, NextAuth, Prisma, Supabase, Anthropic streaming, Tailwind + shadcn/ui) with a 14-task implementation roadmap, data models, authentication flows, onboarding wizard, JD evaluation pipeline, dashboard, applications tracker, pipeline inbox, and deployment instructions.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~8 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding two documentation files (UI design spec and implementation plan) for a SaaS web app.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds documentation describing a proposed SaaS web UI for Career-Ops (design spec + step-by-step build plan) to support discussion and future implementation.

Changes:

  • Added a UI/architecture/design specification for the Career-Ops SaaS web app.
  • Added a detailed 14-task implementation plan (with file maps, commands, and code snippets).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 19 comments.

File Description
docs/superpowers/specs/2026-04-20-career-ops-saas-ui-design.md Defines proposed product scope, pages/routes, data model, and isolation/auth design.
docs/superpowers/plans/2026-04-20-career-ops-saas-build.md Provides a task-by-task build plan including concrete Next.js/NextAuth/Prisma/Supabase implementation steps.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


A multi-tenant SaaS web application that brings the career-ops CLI pipeline into a browser. Any user can sign in with Google, complete a guided onboarding, then use AI-powered job evaluation, an applications tracker, and a pipeline inbox — all from the web.

The system calls the Anthropic API (Claude 3.5 Sonnet) directly for evaluations. Playwright-based portal scanning is deferred to a future worker; for MVP users paste JD URLs or text manually.
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

“Calls the Anthropic API directly” is ambiguous and could be read as client-side calls, which would expose the API key. Clarify that Anthropic requests are made server-side (API route / server actions) and that the browser never sees the key.

Suggested change
The system calls the Anthropic API (Claude 3.5 Sonnet) directly for evaluations. Playwright-based portal scanning is deferred to a future worker; for MVP users paste JD URLs or text manually.
The system performs job evaluations by calling the Anthropic API (Claude 3.5 Sonnet) server-side via Next.js API routes or server actions; the browser never calls Anthropic directly and never sees the API key. Playwright-based portal scanning is deferred to a future worker; for MVP users paste JD URLs or text manually.

Copilot uses AI. Check for mistakes.
].map(f => (
<div key={f.title} className="bg-white rounded-xl border border-gray-200 p-6 shadow-sm">
<div className="text-2xl mb-3">{f.icon}</div>
<div className="text-sm font-700 font-bold mb-1">{f.title}</div>
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

font-700 isn’t a valid Tailwind class (the font weight utilities are font-bold, font-semibold, etc.). As written, this class will be ignored and may confuse readers copying the snippet.

Suggested change
<div className="text-sm font-700 font-bold mb-1">{f.title}</div>
<div className="text-sm font-bold mb-1">{f.title}</div>

Copilot uses AI. Check for mistakes.
const session = await getServerSession(authOptions)
if (!session?.user?.id) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
const { id } = await req.json()
await prisma.pipelineItem.delete({ where: { id, userId: session.user.id } })
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pipelineItem.delete({ where: { id, userId: ... } }) has the same Prisma constraint: delete requires a unique selector, and {id,userId} is not a declared unique key in the provided schema. Use deleteMany({ where: { id, userId } }) or fetch+verify ownership then delete by id.

Suggested change
await prisma.pipelineItem.delete({ where: { id, userId: session.user.id } })
await prisma.pipelineItem.deleteMany({ where: { id, userId: session.user.id } })

Copilot uses AI. Check for mistakes.
Comment on lines +2 to +3
**Date:** 2026-04-20
**Status:** Approved
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec marks this document as "Approved", but the PR description says this is being opened to start discussion (no issue yet). Consider changing the status to "Draft"/"Proposed" (or removing the status) until there’s an explicit approval process recorded.

Copilot uses AI. Check for mistakes.
Comment on lines +147 to +150
2. POST to `/api/evaluate` with `{ url?, jdText, userId }`
3. API route:
- Fetches URL content if provided (WebFetch)
- Loads user's profile + CV from DB
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec suggests POSTing { url?, jdText, userId } to /api/evaluate. Passing userId from the client is insecure (it can be spoofed) and unnecessary if the API derives the user from the session/JWT. Update the spec to omit userId and state that user identity is taken from auth context.

Suggested change
2. POST to `/api/evaluate` with `{ url?, jdText, userId }`
3. API route:
- Fetches URL content if provided (WebFetch)
- Loads user's profile + CV from DB
2. POST to `/api/evaluate` with `{ url?, jdText }`
3. API route:
- Derives the current user from the authenticated session/JWT auth context (do not accept `userId` from the client)
- Fetches URL content if provided (WebFetch)
- Loads the authenticated user's profile + CV from DB

Copilot uses AI. Check for mistakes.
</p>
<SignInButton />
<p className="text-xs text-gray-400 mt-6">
Your data is encrypted and never shared.
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The landing page copy says “Your data is encrypted and never shared.” That’s a strong security/privacy claim that isn’t substantiated elsewhere in the plan (and may be false depending on DB/storage/logging). Either remove/soften the claim or add explicit requirements describing what is encrypted (at rest/in transit) and what “never shared” means.

Suggested change
Your data is encrypted and never shared.
Built with privacy and security in mind.

Copilot uses AI. Check for mistakes.
Comment on lines +1709 to +1731
// Get next application number
const lastApp = await prisma.application.findFirst({
where: { userId: session.user.id },
orderBy: { num: 'desc' },
})
const nextNum = (lastApp?.num ?? 0) + 1

// Extract company/role from content
const companyMatch = finalJdText.match(/(?:at|@|company:?)\s+([A-Z][a-zA-Z\s]+?)(?:\.|,|\s)/i)
const company = companyMatch?.[1]?.trim() ?? 'Unknown'

const application = await prisma.application.create({
data: {
userId: session.user.id,
num: nextNum,
company,
role: 'Evaluated Role',
score,
status: 'Evaluated',
url: url ?? null,
},
})

Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “next application number” logic (findFirst then + 1) is race-prone under concurrent requests and can violate the @@unique([userId, num]) constraint. Suggest generating num atomically (DB sequence / transaction with locking) or implementing a retry on unique constraint failure.

Suggested change
// Get next application number
const lastApp = await prisma.application.findFirst({
where: { userId: session.user.id },
orderBy: { num: 'desc' },
})
const nextNum = (lastApp?.num ?? 0) + 1
// Extract company/role from content
const companyMatch = finalJdText.match(/(?:at|@|company:?)\s+([A-Z][a-zA-Z\s]+?)(?:\.|,|\s)/i)
const company = companyMatch?.[1]?.trim() ?? 'Unknown'
const application = await prisma.application.create({
data: {
userId: session.user.id,
num: nextNum,
company,
role: 'Evaluated Role',
score,
status: 'Evaluated',
url: url ?? null,
},
})
// Extract company/role from content
const companyMatch = finalJdText.match(/(?:at|@|company:?)\s+([A-Z][a-zA-Z\s]+?)(?:\.|,|\s)/i)
const company = companyMatch?.[1]?.trim() ?? 'Unknown'
let application = null
for (let attempt = 0; attempt < 3; attempt++) {
const lastApp = await prisma.application.findFirst({
where: { userId: session.user.id },
orderBy: { num: 'desc' },
})
const nextNum = (lastApp?.num ?? 0) + 1
try {
application = await prisma.application.create({
data: {
userId: session.user.id,
num: nextNum,
company,
role: 'Evaluated Role',
score,
status: 'Evaluated',
url: url ?? null,
},
})
break
} catch (createErr) {
if (
createErr &&
typeof createErr === 'object' &&
'code' in createErr &&
createErr.code === 'P2002' &&
attempt < 2
) {
continue
}
throw createErr
}
}
if (!application) {
throw new Error('Failed to allocate a unique application number')
}

Copilot uses AI. Check for mistakes.
Comment on lines +124 to +129
DATABASE_URL=postgresql://...
DIRECT_URL=postgresql://...
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=
ANTHROPIC_API_KEY=
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.env.example includes SUPABASE_SERVICE_ROLE_KEY without a warning. That key must never be exposed client-side (and generally shouldn’t be needed at all if Prisma connects directly). Consider adding a prominent note that it’s server-only, should not be prefixed with NEXT_PUBLIC_, and ideally avoid requiring it unless absolutely necessary.

Copilot uses AI. Check for mistakes.
Comment on lines +2005 to +2008
const app = await prisma.application.update({
where: { id: params.id, userId: session.user.id },
data: parsed.data,
})
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Prisma, application.update({ where: ... }) requires a unique selector (WhereUniqueInput). With the schema shown later, { id: params.id, userId: session.user.id } is not a valid unique key, so this code won’t typecheck/run. Use updateMany({ where: { id, userId }, data }) or first findFirst({ where: { id, userId } }) then update({ where: { id } }) after verifying ownership (or add a composite unique constraint that matches the selector).

Copilot uses AI. Check for mistakes.
Comment on lines +2012 to +2017
export async function DELETE(req: NextRequest, { params }: { params: { id: string } }) {
const session = await getServerSession(authOptions)
if (!session?.user?.id) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

await prisma.application.delete({ where: { id: params.id, userId: session.user.id } })
return NextResponse.json({ ok: true })
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as the PUT handler: application.delete({ where: { id: params.id, userId: ... } }) requires a unique selector and {id,userId} is not unique in the provided schema. Use deleteMany({ where: { id, userId } }) or verify ownership via findFirst before deleting by id.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/superpowers/plans/2026-04-20-career-ops-saas-build.md`:
- Around line 169-172: The fenced JSON block containing the "test": "vitest run"
and "test:watch": "vitest" entries is missing a blank line before the opening
```json; insert one blank line immediately before the ```json fence so the
snippet is separated from the preceding paragraph and satisfies MD031 (Markdown
best practices).
- Around line 17-72: The file map under the career-ops-web/ heading is helpful
but can go stale; update the
docs/superpowers/plans/2026-04-20-career-ops-saas-build.md file to include a
short maintenance note adjacent to the career-ops-web/ file map that states:
whether the map must be kept in sync with the repo, whether it represents a
snapshot or the intended final layout, and the recommended way to regenerate it
(e.g., a named script or command to auto-generate a fresh tree). Mention the
file map title "career-ops-web/" and a suggested script name (e.g.,
generate-file-map) or tooling approach so maintainers know how to refresh the
map.
- Around line 234-351: The Application model in the Prisma schema is
inconsistent with the UI spec: the schema defines createdAt and url while the
spec expects date and pdfUrl; pick one approach and align both artifacts—prefer
Option 2 (keep the plan names): update the spec to use Application fields
createdAt (instead of date) and url (instead of pdfUrl), or if you prefer the
spec, update the Prisma Application model to rename createdAt → date and url →
pdfUrl; locate the Application model in the diff and apply the corresponding
renames to ensure both documents reference the same field names
(Application.createdAt ↔ date, Application.url ↔ pdfUrl) and update any
callers/docs that reference those symbols.
- Line 738: The JSX uses an invalid Tailwind class "font-700" in the element
rendering f.title (the div with className "text-sm font-700 font-bold mb-1");
remove the invalid "font-700" token (or replace it with the correct named
weight, e.g. keep "font-bold") so the className becomes valid Tailwind (avoid
duplicate/invalid font-weight utilities).
- Around line 1689-1693: The Anthropic model identifier used in
anthropicClient.messages.stream (the messageStream variable) is deprecated;
update the model string from 'claude-3-5-sonnet-20241022' to the supported
'claude-opus-4-7' and run a quick compatibility check of
anthropicClient.messages.stream (streaming signature/params: model, max_tokens,
messages) against the current Anthropic SDK to ensure no other parameter changes
are required.
- Around line 1717-1718: The current company extraction using companyMatch and
company with regex /(?:at|@|company:?)\s+([A-Z][a-zA-Z\s]+?)(?:\.|,|\s)/i is
brittle; replace it with a more permissive pattern that allows lowercase/Unicode
letters, digits, ampersands/hyphens, and multi-word names (e.g., use
Unicode-aware character classes like \p{L}\p{N} and accept &[\-\s.]) or run a
secondary heuristic: try a broader regex to capture sequences after
"at|@|company:" and then normalize/trim and validate; update the company
assignment logic (companyMatch/company) to prefer the validated capture and fall
back to 'Unknown' only if the cleaned result is empty or obviously not a company
name; alternatively call out to the extractor (Block A) if available for higher
accuracy.
- Line 93: The plan currently installs next-auth@4 but the spec requires
NextAuth.js v5; change the install command to use next-auth@5 and update all
auth code to v5 APIs: replace NextAuthOptions usage and imports that reference
getServerSession and DefaultSession with the v5 equivalents (new import paths
and config shape), update callback/signature changes in your auth handlers
(e.g., session/user callbacks), and adjust any Prisma adapter usage to the v5
adapter API; search for symbols NextAuthOptions, getServerSession,
DefaultSession, and PrismaAdapter to locate and update each occurrence to the
v5-compatible patterns.

In `@docs/superpowers/specs/2026-04-20-career-ops-saas-ui-design.md`:
- Around line 144-155: The spec says the /api/evaluate route should stream via
Vercel AI SDK's streamText but the implementation plan uses
anthropicClient.messages.stream directly; reconcile by switching the
implementation to use Vercel's streamText in the handler for /api/evaluate
(replace usage of anthropicClient.messages.stream with streamText and adapt the
prompt and token handling accordingly), or alternatively update the spec to
explicitly state that anthropicClient.messages.stream will be used; locate
references to streamText and anthropicClient.messages.stream in the evaluate
route implementation and the prompt-building logic (modes/oferta.md) to ensure
streaming, error handling, edge-runtime compatibility, and TypeScript types
align with the chosen approach.
- Around line 1-4: Add metadata fields to the top of the "Career-Ops SaaS Web
App — Design Spec" document: include Author and Contributors (name and
contact/handle), Related PRs/Issues (e.g., PR `#387` or issue links), and a
Version or Revision number with a changelog entry; place these directly beneath
the existing Date and Status lines so consumers can quickly trace authorship and
history and update the Version/Revision on each substantive change.
- Around line 190-197: Add a concrete RLS setup task to the implementation plan:
enable Row Level Security on each multi-tenant table (e.g., Profile,
Application, Report, PipelineItem), and add explicit policies for SELECT,
INSERT, UPDATE, DELETE that enforce auth.uid() = "userId"; include corresponding
migration SQL, a note to mirror column names used by Prisma (userId), and tests
to verify access via NextAuth sessions and bypass attempts (API
routes/middleware). Also add rollout steps to apply RLS after verifying Prisma
filters and update the docs section "10. Auth & Data Isolation" to reference the
new RLS migration and testing steps.
- Line 20: The spec claims "Auth | NextAuth.js v5" but the implementation plan
installs "next-auth@4" and uses v4 patterns; resolve the mismatch by either (A)
updating the spec entry "Auth | NextAuth.js v5" to "NextAuth.js v4" and noting
v4 callback/provider patterns, or (B) updating the implementation plan to
install and use NextAuth v5 (replace "next-auth@4" install, update provider
configuration, jwt/session callback signatures, and middleware snippets to v5
shape) so the plan's code snippets match the spec; search for the exact strings
"NextAuth.js v5" in the spec and "next-auth@4" plus provider/callback examples
in the plan to locate and update all occurrences.
- Around line 70-85: The Application model currently uses date and pdfUrl which
conflict with the implementation plan; update the Application model to remove
the date field, rename pdfUrl to url (String?), and ensure createdAt DateTime
`@default`(now()) and updatedAt DateTime `@updatedAt` exist (no duplicates), then
update any code/schema references to Application.date and Application.pdfUrl to
use Application.createdAt and Application.url respectively so the spec matches
the plan; locate these fields on the Application model to apply the changes.
- Line 163: The spec and implementation disagree about where canonical statuses
come from: either update the spec line to state statuses are hardcoded in the
validation schema, or change the implementation to read the canonical list from
templates/states.yml; to do the latter, modify lib/validations.ts (the status
schema currently declared as status: z.enum([...]) around the existing enum) to
load and parse templates/states.yml at module init and generate the z.enum from
that list (or validate dynamically), ensuring the source of truth is the YAML
file and removing the hardcoded array if the repo contains templates/states.yml.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 99c6d8db-0bb3-4d79-a0aa-54c5b6f88d9e

📥 Commits

Reviewing files that changed from the base of the PR and between 411afb3 and b10a5be.

📒 Files selected for processing (2)
  • docs/superpowers/plans/2026-04-20-career-ops-saas-build.md
  • docs/superpowers/specs/2026-04-20-career-ops-saas-ui-design.md

Comment on lines +17 to +72
```
career-ops-web/
├── app/
│ ├── layout.tsx # Root layout, providers
│ ├── page.tsx # Landing / sign-in
│ ├── globals.css
│ ├── providers.tsx # SessionProvider + QueryClientProvider
│ ├── api/
│ │ ├── auth/[...nextauth]/route.ts # NextAuth handler
│ │ ├── profile/route.ts # POST (create), PUT (update)
│ │ ├── applications/route.ts # GET list, POST create
│ │ ├── applications/[id]/route.ts # PUT update, DELETE
│ │ ├── evaluate/route.ts # POST → Anthropic stream
│ │ ├── pipeline/route.ts # GET, POST, DELETE
│ │ └── reports/[id]/route.ts # GET single report
│ ├── onboarding/page.tsx # 4-step wizard
│ ├── dashboard/page.tsx
│ ├── applications/page.tsx
│ ├── evaluate/page.tsx
│ ├── reports/[id]/page.tsx
│ ├── pipeline/page.tsx
│ └── settings/page.tsx
├── components/
│ ├── layout/
│ │ ├── Sidebar.tsx
│ │ └── AppShell.tsx # Sidebar + main area wrapper
│ ├── onboarding/
│ │ ├── Stepper.tsx # Top progress stepper
│ │ ├── Step1Profile.tsx
│ │ ├── Step2CV.tsx
│ │ ├── Step3Portals.tsx
│ │ └── OnboardingWizard.tsx # Orchestrates steps + state
│ ├── dashboard/
│ │ ├── StatCard.tsx
│ │ └── RecentTable.tsx
│ ├── evaluate/
│ │ ├── EvaluateForm.tsx
│ │ └── StreamingEvaluation.tsx # Block-by-block live render
│ ├── applications/
│ │ ├── ApplicationsTable.tsx
│ │ └── StatusSelect.tsx
│ ├── pipeline/
│ │ └── PipelineInbox.tsx
│ └── settings/
│ └── SettingsTabs.tsx
├── lib/
│ ├── auth.ts # NextAuth options
│ ├── prisma.ts # Prisma singleton
│ ├── anthropic.ts # Anthropic client
│ ├── prompt.ts # Evaluation prompt builder
│ └── validations.ts # Zod schemas
├── middleware.ts # Auth + onboarding guards
├── prisma/schema.prisma
├── .env.example
└── package.json
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider documenting the file map maintenance process.

The file map is a helpful reference, but it can quickly become stale as the project evolves. Consider adding a comment indicating:

  • Whether this map should be kept updated as the project grows
  • Whether it represents the final state or intermediate checkpoints
  • A tool/script to auto-generate it from the actual directory structure

This is especially important for documentation that will be referenced during implementation.

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 17-17: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/plans/2026-04-20-career-ops-saas-build.md` around lines 17 -
72, The file map under the career-ops-web/ heading is helpful but can go stale;
update the docs/superpowers/plans/2026-04-20-career-ops-saas-build.md file to
include a short maintenance note adjacent to the career-ops-web/ file map that
states: whether the map must be kept in sync with the repo, whether it
represents a snapshot or the intended final layout, and the recommended way to
regenerate it (e.g., a named script or command to auto-generate a fresh tree).
Mention the file map title "career-ops-web/" and a suggested script name (e.g.,
generate-file-map) or tooling approach so maintainers know how to refresh the
map.

- [ ] **Step 2: Install all dependencies**

```bash
npm install next-auth@4 @next-auth/prisma-adapter @prisma/client prisma
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check what NextAuth patterns are used in the implementation plan vs v4/v5 compatibility
rg -n "next-auth" --type=md -A 3 -B 1

Repository: santifer/career-ops

Length of output: 12245


🏁 Script executed:

fd -type f -name "*career-ops-saas-ui-design*"

Repository: santifer/career-ops

Length of output: 233


🏁 Script executed:

fd "career-ops-saas-ui-design" --extension md

Repository: santifer/career-ops

Length of output: 125


🏁 Script executed:

sed -n '10,35p' docs/superpowers/specs/2026-04-20-career-ops-saas-ui-design.md

Repository: santifer/career-ops

Length of output: 1213


Update NextAuth version to match design spec.

The build plan installs next-auth@4 (line 93), but the design spec specifies NextAuth.js v5. The code examples throughout the plan use v4 API patterns (NextAuthOptions, getServerSession, DefaultSession). NextAuth v5 introduced breaking changes in configuration, imports, and callback signatures that will require updating all authentication code before this plan can be executed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/plans/2026-04-20-career-ops-saas-build.md` at line 93, The
plan currently installs next-auth@4 but the spec requires NextAuth.js v5; change
the install command to use next-auth@5 and update all auth code to v5 APIs:
replace NextAuthOptions usage and imports that reference getServerSession and
DefaultSession with the v5 equivalents (new import paths and config shape),
update callback/signature changes in your auth handlers (e.g., session/user
callbacks), and adjust any Prisma adapter usage to the v5 adapter API; search
for symbols NextAuthOptions, getServerSession, DefaultSession, and PrismaAdapter
to locate and update each occurrence to the v5-compatible patterns.

Comment on lines +169 to +172
```json
"test": "vitest run",
"test:watch": "vitest"
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Markdown formatting: Add blank line before fenced code block.

The JSON code block starting at line 169 should be preceded by a blank line per markdown best practices (MD031).

Based on static analysis: While this doesn't affect functionality, it improves markdown rendering consistency across different parsers.

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 169-169: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/plans/2026-04-20-career-ops-saas-build.md` around lines 169
- 172, The fenced JSON block containing the "test": "vitest run" and
"test:watch": "vitest" entries is missing a blank line before the opening
```json; insert one blank line immediately before the ```json fence so the
snippet is separated from the preceding paragraph and satisfies MD031 (Markdown
best practices).

Comment on lines +234 to +351
```prisma
generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}

model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
createdAt DateTime @default(now())
accounts Account[]
sessions Session[]
profile Profile?
applications Application[]
reports Report[]
pipelineItems PipelineItem[]
}

model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}

model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}

model Profile {
id String @id @default(cuid())
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
fullName String
location String
targetRoles String
seniority String
salaryMin Int
salaryMax Int
currency String @default("USD")
superpower String @db.Text
cvMarkdown String @db.Text
portalsYaml String @db.Text
includeKw String
excludeKw String @default("")
onboardedAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model Application {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
num Int
company String
role String
score Float?
status String @default("Evaluated")
url String?
notes String?
report Report?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, num])
}

model Report {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
applicationId String? @unique
application Application? @relation(fields: [applicationId], references: [id])
url String?
content String @db.Text
legitimacy String?
createdAt DateTime @default(now())
}

model PipelineItem {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
url String
company String?
role String?
status String @default("pending")
createdAt DateTime @default(now())
}
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Database schema inconsistencies with the design spec.

The Prisma schema here differs from the design spec in docs/superpowers/specs/2026-04-20-career-ops-saas-ui-design.md:

  1. Application model (Line 312-327):

    • Plan has: createdAt DateTime @default(now())
    • Spec has: date DateTime @default(now()) (spec line 75)
  2. Application model URL field (Line 321):

    • Plan has: url String?
    • Spec has: pdfUrl String? (spec line 80)

These inconsistencies mean the implementation won't match the design, causing confusion and potential bugs if developers reference both documents.

Recommended alignment

Either:

  • Option 1: Update the plan to match the spec (change createdAtdate, add pdfUrl field)
  • Option 2: Update the spec to match the plan (change datecreatedAt, change pdfUrlurl)

Recommend Option 2 since the plan's naming is more conventional (createdAt/updatedAt pattern) and url is more generic than pdfUrl (which limits future use cases).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/plans/2026-04-20-career-ops-saas-build.md` around lines 234
- 351, The Application model in the Prisma schema is inconsistent with the UI
spec: the schema defines createdAt and url while the spec expects date and
pdfUrl; pick one approach and align both artifacts—prefer Option 2 (keep the
plan names): update the spec to use Application fields createdAt (instead of
date) and url (instead of pdfUrl), or if you prefer the spec, update the Prisma
Application model to rename createdAt → date and url → pdfUrl; locate the
Application model in the diff and apply the corresponding renames to ensure both
documents reference the same field names (Application.createdAt ↔ date,
Application.url ↔ pdfUrl) and update any callers/docs that reference those
symbols.

].map(f => (
<div key={f.title} className="bg-white rounded-xl border border-gray-200 p-6 shadow-sm">
<div className="text-2xl mb-3">{f.icon}</div>
<div className="text-sm font-700 font-bold mb-1">{f.title}</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix invalid Tailwind CSS class.

font-700 is not a valid Tailwind class. Tailwind uses named font weights, not numeric utilities for font-weight.

🎨 Fix the font-weight class
-            <div className="text-sm font-700 font-bold mb-1">{f.title}</div>
+            <div className="text-sm font-bold mb-1">{f.title}</div>

Note: font-bold is already present, making font-700 redundant anyway. If you need font-weight: 700 specifically, font-bold maps to that value.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="text-sm font-700 font-bold mb-1">{f.title}</div>
<div className="text-sm font-bold mb-1">{f.title}</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/plans/2026-04-20-career-ops-saas-build.md` at line 738, The
JSX uses an invalid Tailwind class "font-700" in the element rendering f.title
(the div with className "text-sm font-700 font-bold mb-1"); remove the invalid
"font-700" token (or replace it with the correct named weight, e.g. keep
"font-bold") so the className becomes valid Tailwind (avoid duplicate/invalid
font-weight utilities).

| Layer | Choice | Reason |
|-------|--------|--------|
| Framework | Next.js 14 (App Router) | Full-stack, server components, API routes |
| Auth | NextAuth.js v5 | Google OAuth, session management |
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

NextAuth version specification conflicts with implementation plan.

This spec designates NextAuth.js v5, but the implementation plan at docs/superpowers/plans/2026-04-20-career-ops-saas-build.md (Line 93) installs next-auth@4. NextAuth v5 has breaking changes:

  • Different provider configuration patterns
  • Updated callback signatures (jwt, session callbacks)
  • New middleware patterns
  • Improved TypeScript types

The plan's code snippets use v4 patterns throughout. Either update the spec to v4 or update the plan's implementation to v5.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/specs/2026-04-20-career-ops-saas-ui-design.md` at line 20,
The spec claims "Auth | NextAuth.js v5" but the implementation plan installs
"next-auth@4" and uses v4 patterns; resolve the mismatch by either (A) updating
the spec entry "Auth | NextAuth.js v5" to "NextAuth.js v4" and noting v4
callback/provider patterns, or (B) updating the implementation plan to install
and use NextAuth v5 (replace "next-auth@4" install, update provider
configuration, jwt/session callback signatures, and middleware snippets to v5
shape) so the plan's code snippets match the spec; search for the exact strings
"NextAuth.js v5" in the spec and "next-auth@4" plus provider/callback examples
in the plan to locate and update all occurrences.

Comment on lines +70 to +85
model Application {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
num Int
date DateTime @default(now())
company String
role String
score Float?
status String @default("Evaluated")
pdfUrl String?
notes String?
report Report?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Database schema conflicts with implementation plan.

The Application model schema here differs from the implementation plan's schema (docs/superpowers/plans/2026-04-20-career-ops-saas-build.md lines 312-327):

In this spec:

  • Line 75: date DateTime @default(now())
  • Line 80: pdfUrl String?

In the plan:

  • Has createdAt DateTime @default(now()) instead of date
  • Has url String? instead of pdfUrl
  • Also has updatedAt DateTime @updatedAt`` (not shown here)

These inconsistencies will cause confusion during implementation. Recommend synchronizing both documents to use the plan's more conventional naming (createdAt/updatedAt pattern, and the generic url field).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/specs/2026-04-20-career-ops-saas-ui-design.md` around lines
70 - 85, The Application model currently uses date and pdfUrl which conflict
with the implementation plan; update the Application model to remove the date
field, rename pdfUrl to url (String?), and ensure createdAt DateTime
`@default`(now()) and updatedAt DateTime `@updatedAt` exist (no duplicates), then
update any code/schema references to Application.date and Application.pdfUrl to
use Application.createdAt and Application.url respectively so the spec matches
the plan; locate these fields on the Application model to apply the changes.

Comment on lines +144 to +155
## 6. Evaluate JD — AI Streaming

1. User pastes URL or JD text into `/evaluate`
2. POST to `/api/evaluate` with `{ url?, jdText, userId }`
3. API route:
- Fetches URL content if provided (WebFetch)
- Loads user's profile + CV from DB
- Builds prompt from `modes/oferta.md` logic (adapted for API)
- Streams response via Vercel AI SDK (`streamText`)
4. Frontend renders each block (A–G) progressively as tokens arrive
5. On stream complete: parse score + legitimacy → save Application + Report to DB → redirect to `/reports/[id]`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Streaming implementation approach differs from plan.

Line 152 states the API will use Vercel AI SDK's streamText, but the implementation plan (lines 1689-1693) uses the raw Anthropic SDK's .messages.stream() method directly:

const messageStream = anthropicClient.messages.stream({
  model: 'claude-3-5-sonnet-20241022',
  max_tokens: 4096,
  messages: [{ role: 'user', content: prompt }],
})

While both approaches work, using Vercel AI SDK's streamText would provide:

  • Framework-agnostic streaming utilities
  • Built-in edge runtime support
  • Simpler error handling
  • Better TypeScript types for streaming responses

The implementation approach should match the spec, or the spec should be updated to reflect using the raw Anthropic SDK.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/specs/2026-04-20-career-ops-saas-ui-design.md` around lines
144 - 155, The spec says the /api/evaluate route should stream via Vercel AI
SDK's streamText but the implementation plan uses
anthropicClient.messages.stream directly; reconcile by switching the
implementation to use Vercel's streamText in the handler for /api/evaluate
(replace usage of anthropicClient.messages.stream with streamText and adapt the
prompt and token handling accordingly), or alternatively update the spec to
explicitly state that anthropicClient.messages.stream will be used; locate
references to streamText and anthropicClient.messages.stream in the evaluate
route implementation and the prompt-building logic (modes/oferta.md) to ensure
streaming, error handling, edge-runtime compatibility, and TypeScript types
align with the chosen approach.

- Full table: #, Date, Company, Role, Score, Status, PDF, Report link, Notes
- Sortable by any column
- Filterable by status (multi-select chips) and score range (slider)
- Inline status edit (click cell → dropdown of canonical statuses from `templates/states.yml`)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clarify canonical statuses source.

This line mentions "canonical statuses from templates/states.yml", but the implementation plan hardcodes statuses in lib/validations.ts (line 388):

status: z.enum(['Evaluated','Applied','Responded','Interview','Offer','Rejected','Discarded','SKIP'])

There's no reference to loading statuses from a YAML file. Either:

  • Update this spec to note statuses are hardcoded in validation schema
  • Update the plan to load statuses from templates/states.yml (if that file exists in the main career-ops repo and should be reused)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/specs/2026-04-20-career-ops-saas-ui-design.md` at line 163,
The spec and implementation disagree about where canonical statuses come from:
either update the spec line to state statuses are hardcoded in the validation
schema, or change the implementation to read the canonical list from
templates/states.yml; to do the latter, modify lib/validations.ts (the status
schema currently declared as status: z.enum([...]) around the existing enum) to
load and parse templates/states.yml at module init and generate the z.enum from
that list (or validate dynamically), ensuring the source of truth is the YAML
file and removing the hardcoded array if the repo contains templates/states.yml.

Comment on lines +190 to +197
## 10. Auth & Data Isolation

- NextAuth v5 with Google provider
- Supabase Row Level Security: all tables scoped to `auth.uid()` = `userId`
- Middleware protects all `/dashboard`, `/applications`, `/pipeline`, `/evaluate`, `/reports`, `/settings` routes
- Unauthenticated → redirect to `/`
- Authenticated but not onboarded → redirect to `/onboarding`
- Authenticated and onboarded → allow through
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Supabase Row Level Security policies are not implemented in the plan.

Line 193 describes multi-tenant data isolation using "Supabase Row Level Security: all tables scoped to auth.uid() = userId", but the implementation plan never sets up RLS policies. The plan only implements application-level authorization via:

  • NextAuth session checks in API routes
  • Middleware redirects based on authentication status
  • Prisma queries filtered by userId: session.user.id

Without database-level RLS policies:

  • Security risk: A compromised API route or developer error could leak data across users
  • Defense in depth: Application-level checks are not sufficient for sensitive multi-tenant data
  • Compliance concerns: Violates principle of least privilege at the database layer
Recommended RLS policies for Supabase

The implementation plan should add a task to create RLS policies in Supabase. Example SQL:

-- Enable RLS on all tables
ALTER TABLE "Profile" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "Application" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "Report" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "PipelineItem" ENABLE ROW LEVEL SECURITY;

-- Policies: users can only access their own data
CREATE POLICY "Users can view own profile" ON "Profile"
  FOR SELECT USING (auth.uid() = "userId");

CREATE POLICY "Users can update own profile" ON "Profile"
  FOR UPDATE USING (auth.uid() = "userId");

-- Repeat for Application, Report, PipelineItem tables...

These policies enforce data isolation at the database level, preventing cross-user data access even if application code has bugs.

Do you want me to generate a complete RLS setup task to add to the implementation plan?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/specs/2026-04-20-career-ops-saas-ui-design.md` around lines
190 - 197, Add a concrete RLS setup task to the implementation plan: enable Row
Level Security on each multi-tenant table (e.g., Profile, Application, Report,
PipelineItem), and add explicit policies for SELECT, INSERT, UPDATE, DELETE that
enforce auth.uid() = "userId"; include corresponding migration SQL, a note to
mirror column names used by Prisma (userId), and tests to verify access via
NextAuth sessions and bypass attempts (API routes/middleware). Also add rollout
steps to apply RLS after verifying Prisma filters and update the docs section
"10. Auth & Data Isolation" to reference the new RLS migration and testing
steps.

clmoon2 pushed a commit to clmoon2/career-ops that referenced this pull request Apr 21, 2026
… scans, 3000+ history entries)

Sources scanned: SimplifyJobs Summer/New-Grad (0d entries), SpeedyApply AI/SWE,
Ashby broad, Greenhouse broad, Lever broad, Greenhouse APIs (Anthropic/Glean/RunPod/Temporal),
SAP iXP, Google, Okta, Roadie, intern-list.com.

New companies found: Mark43 (skipped_title, mobile), Vendelux (skipped_score ~2.8/5,
event analytics), First American (skipped_score ~2.8/5, insurance). All below threshold.

Key confirmations: Cloudflare 7774167 closed; Roadie AI Eng closed; Okta AI SWE 404;
Google SWE intern Sydney AU only; all SAP roles dup/closed from v68-v130.

Pending actions: Anthropic Fellows deadline April 26 (urgent), ByteDance AI Security (santifer#388),
Verkada Security Grad (santifer#389), Rockstar Games Intern (santifer#390), C3 AI Ascend (santifer#380),
Accenture Claude Analyst deadline June 1 (santifer#387).

https://claude.ai/code/session_01AMnyAoREuya3fsFwcbngHN
@santifer
Copy link
Copy Markdown
Owner

@tanmay7264, I appreciate that you read Discussion #156 before writing this — the citation in your body shows intent, and that matters.

I'm closing because the proposal goes in the opposite direction from local-first: career-ops is explicitly not a multi-tenant SaaS, and Discussion #274 explains why. Also, design specs for directions we haven't agreed on shouldn't land in the repo — they belong in a Discussion first.

We have an RFC process exactly for this: Discussion #284. If you want to propose a web interface that respects the persistent-agent model, open an RFC there and we can shape it together.

@santifer santifer closed this Apr 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants