TreeMap
무료 오픈소스 디스크 사용량 시각화 도구로, 사각형 트리맵·중복 파일 탐색·디스크 가득 예측·정리 시 파일을 휴지통으로만 이동한다
이력서를 한 번만 올리면 Careerely가 지속적으로 관련 직무를 찾아주고, 이력서를 맞춤화하며, 자기소개서를 작성합니다
Upload your resume once. Careerely continuously finds relevant jobs, tailors your resume, and writes your cover letter.
생산성TypeScript · HTMLGitHub @lisafiskum-png가동 상태 재확인 필요운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Careerely is an AI career agent. The user uploads a resume; Careerely finds,
evaluates and prioritises jobs, prepares a tailored resume and cover letter for
the best ones, and the user reviews and applies.
The Careerely Master Brief is the source of truth for product, pricing, copy
and design.
• Next.js 16 (App Router) on Vercel (Hobby); engine ticks from Supabase Cron
• Supabase: Postgres, Auth, Storage
• Stripe subscriptions: Basic $29 / Pro $49 / Max $79 per month
• Claude API (claude-sonnet-4-6)
Apply the database schema before first use; see supabase/README.md.README 전문 보기 ↗Careerely is an AI career agent. The user uploads a resume; Careerely finds, evaluates and prioritises jobs, prepares a tailored resume and cover letter for the best ones, and the user reviews and applies.
The Careerely Master Brief is the source of truth for product, pricing, copy and design.
claude-sonnet-4-6)npm install
cp .env.example .env.local # fill in the values
npm run dev
Apply the database schema before first use; see supabase/README.md.
| Command | What it does |
|---|---|
npm run dev | Development server |
npm run build | Production build |
npm test | Unit tests plus the database access-rule tests (in-memory Postgres) |
npm run test:e2e | Full onboarding journey in a browser (see below) |
npm run lint | ESLint |
npm run typecheck | TypeScript |
npm run remediate:reevaluate -- <opportunity-id>... [--apply] | Re-evaluates prepared opportunities with the current evidence rules and regenerates their packages in place (dry run without --apply) |
app/: pages and API routesproxy.ts: refreshes the Supabase session and keeps signed-out users out of /dashboardlib/auth.ts: requireUser() for API routes (user id always comes from the session)lib/supabase/: browser, server (user session) and admin (service role) clientslib/plans.ts: plans, limits and access rules (mirrored in the database)lib/billing.ts, lib/stripe.ts: Stripe subscription synclib/engine/: Opportunity Engine (see below)app/(app)/: signed-in app: top-bar shell and Dashboard (app.css ports
design/dashboard-final.html); lib/dashboard.ts and
lib/opportunity-detail.ts read the stored data it showssupabase/migrations/: database schema and row level securitytests/: Vitest suitesnpm run test:e2e runs signup → email confirmation → resume upload and review
→ preferences → checkout → first search → dashboard, and password reset, in
Chromium against the real app. It needs:
npx supabase start # local Supabase + mail inbox
docker run -d -p 12111:12111 stripe/stripe-mock # Stripe API stand-in
The Claude API is replaced by e2e/mock-anthropic.mjs, and the Stripe-hosted
checkout page is simulated by the test (payment succeeds, the webhook's
subscription row is written, Stripe redirects back).
/dashboard follows design/dashboard-final.html (locked). Every number and
claim comes from stored data: counts from the user's live opportunities and
applications, "reviewed in latest scan" and "last scan" from the latest
completed search_runs row, evidence and reasoning from the engine's records.
The opportunity panel (/api/opportunities/[id]) shows Summary (Why I picked
this · What Careerely changed · Things I considered), Resume, Cover letter and
The role. Actions: "Not for me" with an optional reason (Shortlisted only,
enforced in the database), "Continue to application" then "Did you apply?",
and PDF downloads of the prepared documents.
/opportunities (Phase D3) lists every live opportunity in stored rank order:
My Pick featured, then "Also shortlisted", each with its match % and two
primary evidence points, using the same panel. Dashboard and Opportunities
leave out dismissed and applied opportunities and postings no longer listed
on their board (nothing is changed; a relisted posting reappears).
/applications (Phase D4) shows "Ready to apply" (stored rank order) and
"Your applications" (Offer, Interview, Applied, then closed; most recent update first), with the
summary "N prepared · N applied · N interviews" from stored rows. Status is
tracked manually: Applied / Interview / Offer, closed as Declined /
Withdrawn (choosing a stage reopens). Only "Yes, I applied" moves an
application out of Ready to apply; it never goes back (enforced in the
database). Each change is an application_status_changed activity event,
which is the panel's timeline and appears in Recent activity.
/searches (Phase D5) shows each search with its parameters and, from its
latest completed scan, "Reviewed in latest scan" and "Shortlisted in latest
scan" (never summed or live counts), plus the plan's active-search usage
(limit from lib/plans.ts, enforced in the database). Users create, edit,
pause and resume searches; there is no delete in V1. At the limit a new
search can only be saved as paused, and Resume is refused until another
search is paused. A new active search is scanned straight away; a resumed one
too, at most once per search per day (shared with the nightly dedupe key);
an edit applies from the next nightly scan. Immediate scans from these Search
actions also have a per-user safety cap per UTC day (Basic 1, Pro 5, Max 10;
IMMEDIATE_SCANS_PER_DAY, enforced by public.claim_immediate_scan()), not
shown as a plan entitlement. Over the cap the search is still saved or
resumed and active, nothing is queued, and the page says its scan "will run
at the next nightly run" (a never-scanned card shows "First scan at next
nightly run"; "First scan queued" only when a scan task really exists).
Onboarding's first scan and nightly scans don't use the cap. A search's optional minimum
compensation is annual (whole number, up to 10,000,000) and has its own ISO 4217
currency (searches.compensation_currency), compared only with salaries in the
same currency (no conversion);
when blank, the engine uses the Career Profile's minimum only if it has both
an amount and a currency, otherwise there is no minimum. The nav's
"Scanning the market" only counts scans of searches that are active now.
npm run test:e2e includes e2e/dashboard.spec.ts, e2e/opportunities.spec.ts, e2e/applications.spec.ts and e2e/searches.spec.ts, which seed a local
account; set SCREENSHOT_DIR to save desktop and mobile screenshots.
/signup (Step 1, optional ?plan=basic|pro|max), /login,
/forgot-password, /reset-password, /auth/confirm (email links)/onboarding sends the user to the step to continue from/onboarding/2: upload → parsing (/api/resume/parse) → review
(/api/resume/confirm)/onboarding/3: preferences; "Find my matches" calls
/api/onboarding/complete, which asks for checkout when there is no active
subscription and creates the first search once there isImplements OPPORTUNITY_ENGINE_SCHEMA.ts. Code in lib/engine/:
| Stage | Where | What |
|---|---|---|
| Sources | companies.ts, sources.ts, ingest.ts | Greenhouse, Lever and Ashby company boards only; postings no longer listed become inactive |
| 1 Hard filter | filter.ts | Deterministic: expired, duplicate, location, compensation floor, role category. Unknown never rejects |
| 2 Relevance | evaluate.ts, verify.ts | Claude evaluates requirements; only verbatim-quoted evidence is kept |
| 3 Scoring | scoring.ts | Weighted dimensions; dimensions without evidence are left out; shortlist at ≥ 60, max 10 per search per night |
| 4 Ranking | scoring.ts, scan.ts | Goal-aligned (matches a Step 3 target role) always above non-aligned; then industry, score, recency; rank 1 = My Pick |
| 5 Evidence | scan.ts, text.ts | Evidence records stored; every claim points to them. A quote must match whole words within one line, bullet or resume field. Audit stored evidence with supabase/audit/evidence_traceability.sql |
| 6 Preparation | prepare.ts | Top 2 per nightly run, within the plan's monthly allowance (reserved atomically in the database) |
| 7 Package | prepare.ts | Tailored resume changes + segmented cover letter, fact-checked, regenerated once if a check fails |
Every evaluated posting ends in one of four outcomes. Shortlisted becomes
an opportunity. Rejected gets a rejections row: failed Stage 1, or scored
below 60. Two outcomes are not rejections and are kept on
candidate_evaluations with a reason:
not_selected: scored ≥ 60 but fell outside the run's top 10. It competes
again in the next scan.unevaluable: too little traceable evidence to score. Its fit is unknown,
not low. It is evaluated again once the resume, the search or the posting
changes.Scheduling: the engine is built around repeated ticks of
GET /api/engine/tick (with Authorization: Bearer <CRON_SECRET>), every 5
minutes in production. Each tick creates one idempotent market cycle and works
through engine_tasks for up to 40 seconds. User scans and package work are
claimed before source refreshes. Direct ATS boards are spread evenly across a
six-hour window instead of being queued in one burst. Production stays on Vercel
Hobby, so the ticks come from Supabase Cron, not Vercel Cron (there is no
vercel.json cron). User actions (onboarding, new or resumed searches) also
start the worker straight away. Expired leases and ownerless search runs are
recovered in the same database transaction that claims new work.
Boards are configured in lib/engine/companies.ts. Each failed sync records
last_error as <provider>/<slug>: HTTP <status> (<class>), never response
bodies:
not_found (404 / 410): the board doesn't exist under that provider and
slug. One attempt, then public.source_health records it, and nightly runs
skip it until a weekly recheck (one attempt) or until the source is
corrected in companies.ts (a new provider/slug is synced immediately). A
successful sync removes the record. The migration backfills it from
boards whose latest finished sync already failed with HTTP 404/410
(production 2026-10-02: all 56 repeatedly failing boards were 404s).rejected (other 4xx, e.g. 403) and malformed (not the provider's JSON
shape): one attempt, then a 24-hour pause. A malformed response never counts
as "no jobs", so it can't expire a board's postings.rate_limited (429), provider_error (408, 5xx), timeout, network:
retried with bounded backoff; after the final failed attempt the board is
paused for 24 hours. Successful syncs clear the pause. Job rows are written
in small batches so large descriptions do not exhaust the database statement
timeout.npm run sources:check (optionally -- greenhouse|lever|ashby) probes every
configured board with the engine's exact request and prints each outcome. It
needs no database or secrets; run it from a machine that can reach
boards-api.greenhouse.io, api.lever.co and api.ashbyhq.com.
The secret lives in Supabase Vault, never in this repository, in SQL you save, or in the cron job's command text.
Choose the secret. Generate a long random value (for example
openssl rand -hex 32). In Vercel → Project → Settings → Environment
Variables, set CRON_SECRET to it for Production, then redeploy so the
tick endpoint uses it.
Enable the extensions. Supabase Dashboard → Integrations → Cron:
enable it (installs pg_cron). Database → Extensions: enable pg_net.
Vault (supabase_vault) is enabled on every Supabase project.
Store the secret in Vault. Dashboard → Integrations → Vault → Add
new secret: name careerely_cron_secret, value = the same value as
CRON_SECRET. (Using the Vault screen keeps the value out of SQL editor
history.)
Schedule the job. In the SQL editor, run:
select cron.schedule(
'careerely-engine-tick',
'*/5 * * * *',
$$
select net.http_get(
url := 'https://www.careerely.ai/api/engine/tick',
headers := jsonb_build_object(
'Authorization',
'Bearer ' || (select decrypted_secret from vault.decrypted_secrets where name = 'careerely_cron_secret')
),
timeout_milliseconds := 300000
);
$$
);
The command only references the Vault secret by name; the value is read at
run time. Use the exact production host (https://www.careerely.ai) so no
redirect drops the header. The 300 s network timeout leaves headroom for the
endpoint's maxDuration; the worker itself stops after 40 seconds.
Verify (a few minutes later):
select jobid, jobname, schedule, active from cron.job;
select status, return_message, start_time from cron.job_run_details order by start_time desc limit 5;
select status_code, error_msg, created from net._http_response order by created desc limit 5;
Expect succeeded runs and HTTP 200 responses (a JSON body like
{"processed":…,"failed":…}). 401 means the Vault secret and Vercel's
CRON_SECRET differ. Vercel → Logs shows the matching /api/engine/tick
requests every 5 minutes.
Rotate the secret: set the new value in Vercel and redeploy, then
update it in Vault (Integrations → Vault → edit careerely_cron_secret).
Ticks in between return 401 and are simply retried 5 minutes later.
Pause or remove: select cron.unschedule('careerely-engine-tick');
On Vercel Hobby the tick's maxDuration = 300 needs Fluid compute (on by
default for new projects; Vercel → Project → Settings → Functions). Finishing onboarding queues the user's first scan
immediately. npm test includes an end-to-end engine run against the local
Supabase stack when it's running.
Point a Stripe webhook at /api/stripe-webhook with these events:
checkout.session.completed, customer.subscription.created,
customer.subscription.updated, customer.subscription.deleted,
customer.subscription.paused, customer.subscription.resumed.
/settings shows the current plan with its entitlements from lib/plans.ts
(no price: amounts, currency, tax and proration live in Stripe), the renewal or
end date stored from Stripe, the account email and Sign out. "Manage billing"
(POST /api/billing/portal) opens Stripe's Customer Portal for the signed-in
user's stored Stripe customer (never taken from the request) and returns to
/settings. It is shown whenever a Stripe customer exists, so former
subscribers can still see invoices. Accounts without access keep the existing
read-only rules and can subscribe again through Checkout.
Downgrades: the stored plan comes from the subscription's current item, so a
downgrade scheduled for the period end changes nothing until it takes effect.
After every sync, public.apply_plan_search_limit() pauses only the active
searches above the new limit (the search created from preferences is kept
first, then the oldest by created_at, then id), sets
searches.paused_by_plan_change_at, and queues no scans. It is idempotent, and
an upgrade never resumes anything. The marker is server-controlled provenance
(users can't set or clear it); only a search becoming active again (the user
resuming it) clears it. Searches names the paused searches and Settings says
searches were paused; "Dismiss" calls public.dismiss_plan_change_notice(),
which only sets profiles.plan_change_notice_dismissed_at (notices show
searches paused by a plan change after that time) and changes no search.
Settings → Billing → Customer portal, default configuration:
return_url = <NEXT_PUBLIC_APP_URL>/settings on each session.STRIPE_PRICE_BASIC,
STRIPE_PRICE_PRO and STRIPE_PRICE_MAX (one monthly price each). Quantity
changes off.customer.subscription.updated
and customer.subscription.deleted, which carry portal changes.Every signed-in action is an API route under app/api/ that takes the user
from the session, never from the request. Ownership is enforced by row level
security: another user's opportunity, application or search is "not found"
(404) and unchanged. Writes by read-only accounts all return
403 { error: 'Your account is read-only.', code: 'read_only' }
(lib/write-access.ts, the canonical access rule; RLS remains the backstop).
Reading, PDF downloads and Manage billing stay available to read-only
accounts. Signed-out requests get 401. After each action the page refreshes
its server data, so nav badges, Dashboard, Opportunities, Applications and
Searches agree. e2e/access.spec.ts covers these rules across all actions.
Application status changes are atomic with their history: the activity rows
are both Recent activity and the panel's Timeline, so
public.mark_application_applied() ("Yes, I applied": Applied plus exactly
one application_applied event) and public.set_application_status() (a
stage or closed outcome plus exactly one application_status_changed event)
write the change and its event in one transaction, for the signed-in user's
own application and only with an active plan. Signed-in users can't update
the status columns directly.
Hardening: security headers on every response (next.config.ts), a
not-found page and error boundaries (app/not-found.tsx,
app/(app)/error.tsx, app/global-error.tsx), an error message when the
opportunity panel can't load, maxDuration = 300 on the Searches routes
that start a scan in after(), and only http(s) posting links are stored or
opened. e2e/hardening.spec.ts and e2e/webhook.spec.ts cover these and
webhook signature checks and idempotency.
Can't be validated locally (stand-ins are used); check once in production:
after() work; engine_tasks rows done), and the
ATS boards are reachable from Vercel./api/engine/tick every 5 minutes with the Vault
secret (cron.job_run_details succeeded, net._http_response 200, Vercel
logs), continuous market cycles complete without claimable tasks, expired
leases or ownerless search_runs accumulating.NEXT_PUBLIC_APP_URL is the production URL (checkout and portal return
links, email links).