Imported from whoistahito/YourJobFinder-Core (
AGENTS.md). Install upstream withnpx skills add whoistahito/YourJobFinder-Core. Copyright stays with the author.
AGENTS.md
Big picture (what runs)
- Two entrypoints:
- API:
app.py(Flask) exposes/usercreate/delete,/confirm/<token>redirect, and/users/<id>/matches. Sends the confirmation email in-request on signup. - Scheduled job:
main.pyis a one-shot — a single scrape+match+email pass over confirmed users, then exits. A platform scheduler (Coolify Scheduled Task →python main.py, daily) drives the cadence; it is no longer a resident loop. The pipeline lives innotifications.py.
- API:
- DB is Flask-SQLAlchemy (
extension.py) with migrations via Flask-Migrate/Alembic (migrations/).
Repo map (start here)
app.py: Flask app + routes; initializesdb+migrate; createsUserManager().main.py: one-shot scheduled-job entrypoint — wrapsnotifications.notify_all_confirmed_users()inapp.app_context()and exits.notifications.py: the notification pipeline (send_welcome_email,notify_user,notify_all_confirmed_users). Noappimport / noapp_context()wrapping — callers supply the context (the Flask request for the signup welcome email;main.pyfor the daily batch).db/models.py:User(+Skill/Experience/Education) andSentEmail(composite PK).db/database_service.py: thin managers (UserManager,UserEmailManager) around SQLAlchemy queries/commits.scrapers/google_scraper_service.py: posts to an external Google scraping API using bearer token.scrapers/google_scraper_models.py: Pydantic modelsGoogleJobPostingandGoogleScrapeResponse.job_matching/job_matching_service.py: calls the external job-matching API; exposesmatch(job_description, user_profile) -> float.job_matching/job_matching_models.py: Pydantic modelsUserProfile,Requirements,SimilarityScore,JobMatchingResponse.email_manager.py:send_email(body, subject, to, is_html=True, sender=None)via Cloudflare Email Service.senderis the from-address (welcome@ for signup, notification@ for job updates); defaults to the welcome sender.html_render.py: HTML-heavy templates (welcome email + daily "job cards").
File notes
main.py / notifications.py
main.pyis the one-shot scheduled-job entrypoint:with app.app_context(): notify_all_confirmed_users(), then exits. Run it on a schedule (Coolify Scheduled Task), not as a resident process.- Confirmation emails are event-driven:
send_welcome_email(user)is called from thePOST /userhandler (app_factory.py) right after the user row commits. No polling, nois_newsweep. JOB_MATCH_THRESHOLD = 0.35(innotifications.py) — jobs scoring below this are silently skipped. Set to0.0to disable filtering.- Helper
_has_profile(user) -> bool: returnsTrueif the user has at least one skill, experience, or education row. - Helper
_build_user_profile(user) -> UserProfile: converts the SQLAlchemyUserrelations into aUserProfilePydantic model (skills →skills, experiences →experiences, educations →qualifications). - Notification pipeline (
notify_user()):- Scrape jobs via
scrape_google(position, location, 10). - Build
UserProfileonce (only if_has_profile(user)is true). - For each job: skip if already sent; if user has a profile and the job has a
description, callmatch()and skip if score < threshold; fail-open on matcher errors. - Render job cards, email, record sent URLs.
- Scrape jobs via
scrapers/google_scraper_service.py
- Thin client for the external Google scraping API.
scrape_google(title, location, limit=10):- Builds query string:
"{title} jobs in {location}". POSTs togoogle_scraper_urlwith JSON{query, limit}andAuthorization: Bearer <google_scraper_token>.- Raises on non-2xx (
response.raise_for_status()) and parses the JSON intoGoogleScrapeResponse.
- Builds query string:
- Expectation: this module does not scrape directly; it delegates to a separate service behind
google_scraper_url.
job_matching/job_matching_service.py
- Thin client for the external job-matching/scoring API.
match(job_description: str, user_profile: UserProfile) -> float:POSTs tojob_matcher_urlwithAuthorization: Bearer <job_matcher_token>.- Payload includes
modelId(extractor model), anextractionPipelinedict (extractor + judge model IDs),inputText(job description), anduserProfile(serialized via.model_dump()). - Parses response into
JobMatchingResponseand returnssimilarityScore.score(0.0–1.0). - Raises on HTTP errors; callers should handle exceptions and fail-open.
Key data flows (follow the call chain)
- Create user:
POST /user→UserManager.add_user(...)(returns the newUser, orNoneif a duplicate) → insertsusersrow withis_confirmed=False,confirmation_token=<uuid>and optional related rows. On success the handler callsnotifications.send_welcome_email(user)in-request (failure is logged, signup still 201). - Confirm user:
GET /confirm/<token>→UserManager.confirm_user(token)setsis_confirmed=Trueand clears token, thenapp.pyredirects tohttps://yourjobfinder.website/.... - Scheduled daily notify:
python main.py→notify_all_confirmed_users()→ for each confirmed user →notify_user(user):scrape_google(position, location, 10)→ list ofGoogleJobPosting.- Build
UserProfilefrom user's skills/experiences/educations (skipped if profile is empty). - Per job: skip if
UserEmailManager.is_sent(...)→ optionally calljob_matching.match(description, profile)→ skip if score <JOB_MATCH_THRESHOLD. create_job_card(job)× N →get_html_template(...)→send_email(...).UserEmailManager.add_sent_email(...)for each sent job.
Configuration / env vars (see credential.py)
- DB:
db_host,db_port,db_name,db_username,db_password(credential.get_db_uri()builds the URI). In proddb_host=ssh-tunnel— the app connects to the autossh sidecar (docker-compose.yml), which forwards to the remote Postgres. The app has no SSH code; the sidecar's tunnel is configured viaSSH_HOST,SSH_USER,SSH_PORT,SSH_DB_HOST,SSH_DB_PORT. The private key is not an env var — it lives on the backend host at/data/jobfinder/id_ed25519(chmod 600) and is mounted read-only into the sidecar as/keys(a directory mount; override the host dir withSSH_KEY_DIR). Env-injected keys were abandoned: Coolify truncates long env values and flattens multi-line ones, and its single-file bind mounts are created as directories — only a directory mount is reliable. The matching public key is in the DB server's rootauthorized_keys. - Email (Cloudflare Email Service):
cloudflare_email_token,cloudflare_account_id,cloudflare_email_welcome_from,cloudflare_email_notification_from. - Scraper API:
google_scraper_url,google_scraper_token. - Job Matcher API:
job_matcher_url,job_matcher_token,extractor_model,judge_model.
Project-specific conventions / gotchas
- The worker does DB work only inside
with app.app_context():blocks (seemain.py). If you add DB access elsewhere in the worker, keep this pattern. - Duplicate prevention is DB-backed (
SentEmailcomposite primary key:email + job_url + position + location). This is whyUserEmailManager.is_sent(...)requires all 4 fields. - Job cards expect a
GoogleJobPostingobject (seehtml_render.create_job_card); fields accessed:title,company,location,date_posted,link. scrapers/andjob_matching/are proper Python packages (each has an__init__.py). Always import them with their full package path (e.g.from scrapers.google_scraper_service import scrape_google).- Job matching is opt-in per user: users without any skills/experiences/educations skip the matcher entirely and receive all scraped jobs. Users with a profile but whose scraped jobs lack a
descriptionfield also skip matching for those jobs. - The matcher is fail-open: if the external matching API errors, the job is included anyway to avoid users missing opportunities. Log the error and move on.
- Deployment URLs are embedded in code/templates (
yourjobfinder.website,api.yourjobfinder.website). If you change domains, search/replace acrossapp.py,main.py,html_render.py.
Developer workflows (verified from repo files)
- Dependencies are defined in
pyproject.toml(requires Python>=3.13) and there is auv.lock→ preferuv. - Common commands:
uv venv && uv sync- Run API locally:
uv run python app.py(Flask dev server on:5000) - Run the daily job once (unbuffered logs):
uv run python -u main.py - Apply migrations:
uv run flask db upgrade
- Deployment: Docker (
Dockerfile+docker-compose.yml) on Coolify. Thewebservice runsgunicorn app:app -c gunicorn_config.py(binds0.0.0.0:8080); amigrateservice applies migrations on each deploy; the dailypython main.pyruns via a Coolify Scheduled Task. See.env.examplefor env vars.