Skip to content

Latest commit

 

History

History
380 lines (311 loc) · 27.2 KB

File metadata and controls

380 lines (311 loc) · 27.2 KB

OpenJobRadar — Product Roadmap

Path selected: B — Multi-Tenant SaaS · Drafted 2026-08-22 · Status: pre-implementation planning

Turn JobRadar — a personally-tuned, single-user AWS pipeline for job discovery, scoring, and alerting (source repo: jobradar) — into OpenJobRadar: a hosted product where any job seeker can customize every dimension of discovery, filtering, scoring, values screening, notification, and cost through friendly surfaces.

Companion audit: every personalization point cited below (file:line references) was verified against the source repo on 2026-08-22. Appendix A is the purge checklist; Appendix C maps which source assets carry forward and how.


0. Product thesis & non-goals

Thesis. The hard part of a job search isn't finding postings — it's trusting a filter that knows you. JobRadar's engine (50 ATS adapters + aggregators, hard filters, two-pass Bedrock scoring, explainable verdicts, outcome-driven source control) already works and is measured. Productizing means: (1) zero person-specific facts in code, (2) strict per-user isolation, (3) configuration so approachable that non-engineers succeed, and (4) unit economics where a paid plan comfortably covers a user's Bedrock spend.

Audience breadth (2026-08-22 revision). The prototype served one engineering-leadership search; the product serves knowledge workers across role families: software engineering (IC and leadership), product management, design, research (UX and applied), data/analytics/ML, program management, marketing, operations, and custom families users define. Role family is a first-class configuration dimension — it selects filter vocabularies, rubric archetypes, and onboarding question flows — and every shipped default set (presets, examples, golden sets) must cover at least engineering, product, design, and research before GA.

Non-goals (v1).

  • Not a LinkedIn/social network; not an applicant-tracking system for employers.
  • Not resume-blasting automation — human-in-the-loop everywhere alerts are acted on.
  • No scraping that violates robots.txt / ToS; Gmail ingestion stays consent-first, minimal-scope.
  • No enterprise SSO/RBAC/SOC 2 in v1 (tracked post-GA).

1. Decision record: why Path B, and what it commits us to

Dimension Path A (self-host OSS) Path B (multi-tenant SaaS) — chosen
User effort Clone + own AWS account + CDK deploy Sign up, configure in browser
Audience reached Engineers only Anyone job-hunting who fits the product
Isolation burden Per-deployment (natural) Code-level tenancy (the core engineering risk)
Cost model Users pay their own AWS We pay AWS; billing must cover Bedrock per user
Ops burden Ours per our stack Ours for everyone, 24/7
Deliverability Each deployer verifies own domain Shared sending reputation to engineer carefully

Path B subsumes most of A's technical work anyway: de-personalized config (M1), parameterized infrastructure, and DRY_RUN-first safety are identical. The CDK app stays parameterized so a future "Enterprise dedicated stack" tier can reuse Path-A packaging without rework.


2. Target architecture

                        ┌────────────────────── CONTROL PLANE ─────────────────────┐
 Browser ── CloudFront ─▶ Astro web app                                            │
                        │   ├─ Onboarding wizard        ├─ Watchlist manager         │
                        │   └─ Settings hub             └─ Matches/Pipeline/Trends   │
 API Gateway ───────────▶ web_api (split)                                          │
                        │   ├─ authn: Cognito Hosted UI + PKCE (exists) → userId=sub│
                        │   ├─ settings service   (per-user config docs, validated) │
                        │   ├─ credentials vault  (KMS envelope-encrypted BYO keys) │
                        │   ├─ entitlements+metering (Stripe-backed plan gates)     │
                        │   └─ admin/break-glass  (audit-logged, support role)      │
                        └───────────────────────────────────────────────────────────┘
                        ┌────────────────────── DATA PLANE ────────────────────────┐
 EventBridge dispatcher │ poll → hard filters → Bedrock score → notify              │
   (due-work fanout)    │   every Lambda receives userId context                    │
                        │   fairness lanes (free/plus/pro SQS) + per-user budgets   │
 SES (product domain →  │ digest/alert/brief renderers, per-user quiet hours        │
   custom-domain wizard)│                                                           │
 DynamoDB (all rows     │ Seen·Postings·Orgs·Subscriptions·Sends·Applications·      │
   keyed by userId)     │ Settings·Credentials·Entitlements·BudgetLedger·DueWork    │
                        └───────────────────────────────────────────────────────────┘

Key architecture decisions (each becomes an ADR)

# Decision Choice Rationale
AD1 Tenancy model Pooled (one stack, row-level ownership), not stack-per-tenant Seconds-not-minutes provisioning; no idle floor cost per free user; single deploy surface. Escape hatch: keep CDK parameterized for a future dedicated-stack enterprise tier
AD2 Identity Cognito sub = canonical userId, threaded through every table key, Lambda event, and log line Auth already exists in the source repo (web/src/lib/auth.ts, Hosted UI + PKCE); the gap is authorization scoping, not authentication
AD3 Storage Keep multi-table design; every entity gains a userId partition; repository layer enforces tenant predicate deny-by-default; operator GSIs unreachable from app roles Smallest diff from the source repo's table-per-concern layout
AD4 Scheduling Replace global EventBridge cadences with a due-work index (DueWork{nextDueAt,userId}) scanned by a dispatcher fanning bounded batches into lane queues Per-user cadence replaces global tier intervals; fair-share becomes enforceable
AD5 Settings Per-user config documents in DynamoDB (JSON-Schema-validated, versioned); layering platform defaults < plan defaults < user config < saved-filter override. SSM /jobradar/* becomes platform-only Kills the source repo's single global namespace mutated by web_api/settings.py
AD6 Secrets Credentials table w/ KMS CMK + per-user data-key envelope encryption; server-side OAuth refresh; masked reads; never logged Replaces single-slot SSM entries (Adzuna/Theirstack/PDL/Hunter/SAM/GitHub PAT, one Gmail refresh token)
AD7 Email identity Three stages: (1) send from product domain to user's inbox, (2) custom-FROM wizard using SES identities verified in our account, (3) BYO SES/SMTP creds for Pro Sandbox→production path explicit; shared reputation protected by suppression lists + bounce/complaint handling from day 1
AD8 LLM safety User-supplied rubric/profile text = untrusted input: delimited, output-validated, shadow-mode first, golden-set replay gate before activation; extend prompt-injection corpus Source repo already has a 15-test injection suite and shadow-rubric machinery to build on
AD9 Environments Three AWS accounts (dev/stage/prod); IaC promotion; ephemeral control-plane stacks per PR Mirrors the source repo's CI discipline

3. Milestone plan

Critical path: M0 → M1 → M2 → M4 → beta(M7/M8). M3/M5/M6 overlap M2–M4 where staffing allows. Sizes: S ≤ 1 wk · M ≈ 1–2 wk · L ≈ 2–4 wk · XL ≈ 4+ wk. Weeks are calendar estimates for one senior engineer plus part-time review, matching how the source repo is run.

M0 — Genesis & guardrails (wk 1–2, M)

  • Name/legal: confirm "OpenJobRadar" (trademark search); pick license — AGPL-3.0 recommended if any engine code stays public alongside the SaaS; proprietary if not. Record it as this repo's first ADR either way.
  • New repo, clean history: deliberately copy engine code from jobradar; never copy .gitleaksignore, data/profile.yaml, registry.reviewed.csv, account IDs, or personal domains. The source repo keeps running untouched throughout.
  • Parity bridge: golden-set + recorded-fixture suites run in both repos until cutover; port the coverage ratchet, lint/type gates, axe + Lighthouse gates, and the prompt-injection corpus.
  • CI skeleton: ruff/mypy/pytest/cdk synth on PRs; branch protection on main.
  • Exit: green CI · zero owner-identifying bytes in history · parity suite wired.

M1 — De-personalization: zero personal facts in code (wk 2–7, L)

The heart of the conversion. Everything below exists today hardcoded or owner-shaped:

  • Config schema v2 + validation layer (M1.1). JSON Schema for profile/strategy/policy docs; validate on load and on write; schema versioning + migrations. Layered resolution extending the source-tracking pattern from src/common/config.py to per-user documents.
  • Declarative location policy (M1.2). Replace the hardcoded metro allowlist, home-city regexes, employer-based location exceptions, and per-city onsite-cadence carve-outs in score/filters.py with config-driven rules: metro list, radius, max-onsite-days, remote-only, timezone. Property-based tests (Hypothesis is already a dep).
  • Filter policy DSL (M1.3). Clearance stance, seniority bands, comp floor, excluded archetypes/orgs, current-employer suppression → declarative config. Delete the hardcoded current-employer default (common/config.py:79).
  • Rubric-as-config (M1.4). Parameterize scorer prompts by profile facets; no person-specific prose in source. Reuse RUBRIC_VERSION, shadow-rubric mode, and the replay harness. Ship archetypes: IC-SWE, eng-leadership, PM, data, design, custom.
  • Values/exclusion presets (M1.5). Generalize discover/exclusions.py + values_policy.yaml into opt-in preset packs ("no defense/surveillance/incarceration", "climate only", "none") plus a user-extensible denylist. Defaults politically neutral; the packs carry the stances.
  • Purge sweep (M1.6). applications/profile.py _PREFERENCES and its employer-specific confidentiality mirror in enrich/handler.py; scorer pronouns/background prose; infra constants (RECIPIENT_EMAIL, SENDER_DOMAIN at infra/lib/jobradar-stack.ts:116-122); README/docs voice.
  • Exit: grep -ri for owner name / employers / metros / personal domains across src/ returns nothing · pipeline green on ≥3 synthetic stranger profiles under the parity harness.

M2 — Tenancy foundations (wk 5–11, XL)

  • Schema migration (M2.1). Add userId to every entity (Appendix B sketch); dual-write + backfill tooling; read-shims during transition.
  • Tenant-scoped repository (M2.2). Deny-by-default data access: no query without a tenant predicate; explicit context objects rather than ambient globals where feasible.
  • AuthZ middleware + adversarial suite (M2.3). Every API route attempted cross-tenant must fail; fuzz the route×resource matrix; merge-gated like the coverage ratchet.
  • Settings service (M2.4). Implements AD5; migrates everything web_api/settings.py writes to SSM today into per-user documents.
  • Credentials vault (M2.5). Implements AD6; rotation reminders; per-key health-check probes.
  • Per-user budget ledger (M2.6). Port common/budget.py's $12/day governor to per-user ledgers enforced before Bedrock invocation, under a platform-wide ceiling.
  • Threat model + DPIA refresh (M2.7). Extend the docs/RESPONSIBLE-TECH-AUDITS.md skeletons to consumer-product scope.
  • Exit: adversarial suite is red-on-mutation in CI · all reads/writes tenant-scoped · budget denial observable per user.

M3 — Control plane: signup, plans, entitlements (wk 8–14, L)

  • Signup/signin/email-verify/password-reset/MFA-optional on Cognito pools; workspace = user (single-player v1; shared workspaces post-GA).
  • Entitlements service + metering events (scores consumed, orgs watched, emails sent).
  • Stripe integration: checkout, customer portal, webhooks → entitlement updates, dunning; downgrade lands in dry-run mode — never silent email death.
  • Plan tiers (hypotheses; priced in §7): Free (dry-run only, 25 orgs, weekly digest) / Plus $15mo (live alerts, 300 orgs, extra channels) / Pro $30mo (BYO keys, custom rubric, custom sender domain, priority lane).
  • Exit: plan changes flip entitlements within 60s · Free tier physically cannot incur more than ~$0.50/user/mo (cap enforced pre-invocation).

M4 — Tenant-aware pipeline & fairness (wk 10–16, XL)

  • Due-work scheduler per AD4 replacing global tier intervals; poll_tiering.yaml semantics move per-org and per-user.
  • Fair-share lanes: Free/Plus/Pro SQS lanes with per-user concurrency caps; noisy-neighbor load-shedding tests; scarcity-aware market scan runs per-user strategy.
  • Tenant-aware notify: per-user thresholds (ALERT_THRESHOLD / DIGEST_THRESHOLD semantics), quiet hours, timezone, channel routing (common/channels.py generalized).
  • Outcome tracking (source_yield.py provenance across ≥72 outcomes) keyed by user — the yield-adaptive polling loop must keep working when every org belongs to someone.
  • Exit: 200 synthetic tenants × heterogeneous configs soak-tested without cross-talk · p95 poll latency within 2× of the single-user baseline at beta scale.

M5 — Customization UX: onboarding wizard + settings hub (wk 12–18, L)

Progressive disclosure: presets → forms → expert YAML escape hatch.

  • Onboarding wizard (M5.1). Paste résumé or upload a LinkedIn export → LLM drafts profile + search strategy → confirm/edit → immediately see sample scored matches. Target: first dry-run digest preview ≤15 min from signup. Trust moment: everything DRY_RUN until explicitly armed.
  • Settings hub (M5.2). The ~14 scattered YAMLs become grouped, validated sections: Alerts & thresholds · Location & remote · Seniority & comp · Values & exclusions · Channels · Models & budget. Every control states what it affects.
  • Watchlist manager (M5.3). Add-org-by-URL via an interactive discover→ATS-detect funnel; tier toggles; bulk CSV import; dead-board healing surfaced with a re-resolve button.
  • Preview & simulate (M5.4). Threshold what-if against stored history ("this would have been 4 alerts instead of 11 last week"); dry-run digest rendering; config-diff impact statements.
  • Plain-language editing (M5.5, stretch). NL request → proposed validated config diff → review → apply. Human-in-the-loop; reuses Bedrock.
  • Explainability everywhere (M5.6). Surface filter-drop reasons per posting ("hidden: location rule"); extends the why-this-scored work.
  • Exit: five non-engineers complete onboarding unaided in usability tests · every setting has validation + impact text · no raw YAML required for full functionality.

M6 — Billing GA-readiness (wk 14–19, M, overlaps M5)

Metering accuracy audits (reconcile metered events vs actual Bedrock usage), invoice emails, Stripe Tax, fraud controls (card-testing defenses, disposable-email gating on Free), refund/cancel runbook, revenue dashboard. Exit: month-close ledger reconciles within 1% of AWS bill attribution.

M7 — Deliverability & channels (wk 15–20, L)

Implements AD7 stages 1–2.

  • Product-domain sending with per-class configuration sets (alerts/digest/brief subdomains); bounce + complaint SNS handling → per-user suppression lists; SES production-access filing; DKIM/SPF/DMARC automation; staged warmup plan.
  • Channel expansion beyond email: Slack/Discord/Telegram/webhooks, each with a test-send button.
  • Digest schedule + quiet-hours UI.
  • Exit: ≥98% inbox placement in seed tests across Gmail/Outlook/iCloud · complaint rate never above 0.1%.

M8 — Closed beta & hardening (wk 18–24, L)

  • 20–40 friendly users running real searches; activation instrumentation end-to-end: signup → onboarding complete → first match → first alert → first application logged.
  • Weekly friction triage; fix-or-kill the top issue each week.
  • Pen test before widening; secret-rotation drill; backup/restore drill; incident runbook + status page.
  • Load: 500 simulated users. Chaos: adapter malformed-response drills (port the fixture-refresh discipline so board-format drift fails CI, not silently returns zero).
  • Exit: activation funnel ≥60% to first alert · zero open P1 security findings · support load ≤ 2 h/wk (sustainable solo).

M9 — Public launch (wk 24+, M then ongoing)

Legal set live: ToS, privacy policy, subprocessor list (AWS/Stripe/Google), GDPR export/delete, EU AI Act transparency notes for consumer-facing AI scoring. Pricing page; Product Hunt/HN launch plan; community channels (GitHub Discussions for roadmap feedback even if the engine stays closed); post-GA backlog grooming (§5). Exit: GA definition-of-done checklist (§8) fully green.


4. Cross-cutting workstreams (continuous from M2 onward)

Workstream Commitments
Security Threat-model-as-merge-gate; least-privilege IAM per function audited quarterly; KMS key hygiene; no secrets in logs (enforced by test); pen test before beta widening and annually
Privacy PII minimization (JD-archive bucket policy revisited for multi-tenant); per-user retention defaults; GDPR delete = crypto-erase of the user data key + row purge; DSR runbook
Cost safety DRY_RUN default for every new user; caps enforced pre-invocation; per-user spend visible in-app; platform alarm on aggregate drift
Quality Coverage ratchet carried over; adversarial tenant suite; property-based config fuzzing (malformed YAML fails closed with helpful errors, never half-applies); contract fixtures refreshed monthly
Observability Tenant dimension on every EMF metric; per-user health view (port jobradar health checks: discovery / scoring / verdict-reachability / delivery / registry freshness); anomaly detection on per-user spend
Accessibility Hold WCAG 2.2 AA + Lighthouse 100 on every new surface (the source repo's bar)
Ethics Preset packs reviewed quarterly; screened-out-org audit log inspectable by the user who owns the screen

5. Post-GA catalog (parking lot)

Shared/team workspaces (recruiter/coach mode) · browser extension · VC-board connectors · webhook eventing · embeddings pre-filter rollout · company-enrichment cards · Enterprise tier (dedicated stack — Path-A packaging finally reused) · SOC 2 · mobile push · i18n (currently N/A by ADR in the source repo — revisit at public launch).

6. Risks & mitigations

Risk Mitigation
Tenant-isolation bug leaks one user's data to another Adversarial suite as a CI gate; deny-by-default repositories; threat model updated per feature; pen test pre-launch
One noisy/rogue user torches Bedrock spend Pre-invocation per-user caps; platform ceiling; anomaly alarms; Free tier structurally capped
Prompt-injection via user-supplied rubric/profile AD8: delimiting, output validation, shadow mode, injection-corpus extension, golden-set replay gate
SES reputation damage hurts every user Suppression lists day 1; complaint-rate alarm at 0.1%; staged warmup; per-class config sets; BYO-SES escape hatch for high-volume Pro
"Customize everything" becomes unusable Progressive disclosure (presets / forms / expert YAML); usability tests gate the M5 exit
Solo-operator on-call burnout Incident runbook + status page before beta; sustainable-support exit criterion in M8; feature-freeze windows
Unit economics don't close §7 hypotheses validated weekly in beta; pricing/entitlement knobs designed to flip without deploy
Two-repo transition drift Parity bridge until cutover; source repo feature-frozen through M4

7. Cost model (hypotheses to validate in beta)

Per-user marginal cost, extrapolated from the source repo's measured single-user run (~$1–5/mo infra + Bedrock-dominated variable cost):

Component Free (capped) Plus Pro
Polling + storage <$0.10 <$0.25 <$0.50
Bedrock scoring (dominant driver: watchlist size × rescore policy) ≤$0.50 (25 orgs, weekly digest only) ~$1–3/mo (300 orgs, live scoring) ~$3–10/mo (large watchlists, BYO keys option shifts cost off us)
Email + misc <$0.05 <$0.10 <$0.20
Total marginal ≤$0.65 ~$1.30–3.35 ~$3.70–10.70

Gross-margin targets: Plus ≥ 75% · Pro ≥ 60%. Platform fixed costs (control plane, three AWS accounts, monitoring): target ≤ $150/mo under 100 users. Every number here is a hypothesis with a named owner metric in M8's instrumentation; pricing and entitlement knobs must be flippable without deploy.

8. GA definition of done

  • Zero personal facts in src/ (Appendix A checklist green)
  • Adversarial cross-tenant suite merge-gated and passing
  • Per-user budgets enforced pre-invocation; platform ceiling alarmed
  • Onboarding: ≥60% of beta users reach first alert unaided, ≤15 min to dry-run digest
  • Settings hub covers all former YAML knobs with validation + impact text
  • Billing reconciles within 1%; dunning → dry-run downgrade verified
  • Deliverability: production SES access, suppression lists, ≥98% seed placement
  • GDPR export/delete exercised end-to-end; DSR runbook tested
  • Incident runbook + status page live; on-call rotation (solo-viable) defined
  • Coverage ratchet, a11y, Lighthouse gates carried over and passing
  • Legal set published (ToS, privacy, subprocessors, EU AI Act notes)
  • Source repo either archived or clearly marked as the personal deployment

Appendix A — Personalization purge checklist

Source-repo references verified 2026-08-22.

# Item Location in source repo Done when
A1 Candidate identity/profile data/profile.yaml (+ profile.example.yaml) Schema v2 + validation; example profiles for tests only
A2 Search strategy, values, model, tiering, rescore policies data/*.yaml (14 files) All owner-shaped defaults neutralized; presets carry stances
A3 Geography logic (metros, onsite cadence) src/score/filters.py hardcoded metro/city regexes and employer location exceptions Config-driven rules; property-tested
A4 Rubric prompt prose (pronouns, degrees, CA roots) src/score/scorer.py Facet-parameterized templates
A5 Targeting preferences + employer confidentiality rules src/applications/profile.py _PREFERENCES; mirrored in src/enrich/handler.py Deleted; user-config equivalent shipped
A6 Hardcoded current-employer default src/common/config.py:79 Default empty; validated config field
A7 Ethical exclusion defaults src/discover/exclusions.py + data/values_policy.yaml Opt-in preset packs; neutral default
A8 Recipient/sender identity + account ID infra/lib/jobradar-stack.ts:116-122; README deploy section Fully parameterized CDK app
A9 Global /jobradar/* settings namespace src/common/config.py, src/web_api/settings.py Platform-only SSM; per-user settings docs (AD5)
A10 Single-slot integration credentials SSM entries: Gmail OAuth token, Adzuna/Theirstack/PDL/Hunter/SAM/GitHub keys Vault per AD6
A11 Owner-less tables Seen, Postings, Subscriptions, Sends, Applications, Orgs userId partitions everywhere (M2.1)
A12 One-wallet cost controls ($12/day, weekend shares) src/common/budget.py, data/rescore_policy.yaml Per-user ledgers + lanes (M2.6/M4)
A13 Ops tooling bound to one stack (~130 scripts, cli/) repo-wide Operator tooling behind admin auth; tenant-aware where user-facing
A14 Shared registry artifacts registry.reviewed.csv, known_careers.yaml, manual_boards.csv, Company_Follows.csv Rows keyed by user; files become import formats
A15 Docs voice (first person, personal domain) README.md and most of docs/ Rewritten handbook vs operator guide

Appendix B — Storage sketch (per-table ownership)

All tables gain userId = Cognito sub as partition key (or partition prefix). Sketch:

Users          PK userId                       profile ref, plan, created, state
Settings       PK userId        SK configKey   versioned JSON-schema'd documents
Orgs           PK userId        SK orgSlug     watchlist/tier/ATS resolution per user
Seen           PK userId        SK stableId    dedupe ledger
Postings       PK userId        SK stableId    scores, verdicts, provenance
Subscriptions  PK userId        SK filterId    saved filters / near-miss watches
Sends          PK userId        SK sendId      send-audit (idempotency preserved)
Applications   PK userId        SK appId       pipeline tracker
BudgetLedger   PK userId        SK date        per-day spend caps + actuals
Credentials    PK userId        SK credId      KMS envelope-encrypted BYO keys/OAuth tokens
Entitlements   PK userId                       plan-derived limits, Stripe refs
DueWork        GSI nextDueAt → userId, kind    dispatcher scan index (AD4)

Operator/analytics GSIs are separate keyspaces reachable only from admin roles.

Appendix C — Source-asset map (what carries forward from jobradar)

Source asset Disposition
src/common/ (config, storage, http, budget, health) Adapt: config → layered per-user service; budget → per-user ledger; health checks ported to per-user view
src/poll/ (50 ATS adapters + aggregators) Carry forward nearly verbatim; tenant context threaded through; adapters stay shared/global (board knowledge is universal)
src/discover/ (resolve funnel, ATS detect) Carry forward; becomes interactive backend for the watchlist manager
src/score/ (filters, scorer, jd archive) Heaviest rework: filters → DSL (M1.3), scorer prompts → facet-parameterized (M1.4); JD archive gets per-user retention policy
src/notify/ (renderers, SES) Extend: channels beyond email, per-user quiet hours, suppression lists
src/marketscan/, src/inbox/, src/outreach/, src/enrich/, src/applications/, src/trends/ Tenantize mechanically after M2; purge sweep items A5
web/ (Astro UI) Foundation for control-plane UI; settings hub + wizard added alongside existing matches/pipeline/trends pages
infra/ (CDK TypeScript stack) Split into ControlPlaneStack + DataPlaneStack; fully parameterized (A8); dev/stage/prod promotion (AD9)
cli/, scripts/ (~40 operator tools), bin/jobradar Stay operator-only; re-pointed at stage/prod accounts via admin role; never exposed to users
tests/ (2,800+ tests, fixtures, injection corpus) Port harnesses first (parity bridge); extend with tenancy suites
docs/ standards culture (ADRs, runbooks, audits) Adopt wholesale; first ADRs = AD1–AD9 above

This roadmap is the working plan for this repository. Update it as milestones land; keep ADRs authoritative for decisions.