Automated job search and business lead discovery for Austria.
Jobstrian searches Austrian job portals for configured role keywords and Austrian cities, derives a portal city from your validated home address when no explicit cities are configured, finds configured business categories near your home for speculative applications, and rates each job / business using a user-configured OpenAI-compatible LLM.
- Portal scraping via interchangeable adapters:
hokify,willhabenJobs (embedded JSON),karriere.at(HTML), andAMS eJob-Room(browser-driven, because the search API requires an SPA token).- Job portal scraping requires configured role keywords plus either explicit search cities or a validated address with a city / municipality. If no city can be determined, portal scraping is skipped instead of using a default city.
- Nearby businesses: OpenStreetMap (Overpass) finds configured business categories within a radius; missing e-mail addresses are extracted from the respective website by scanning bounded same-origin contact, imprint and recruiting pages. Automatic e-mail candidates are filtered for clearly unusable technical / no-reply addresses and reviewed in batches by the configured LLM before they become eligible for application sending. If Overpass is temporarily overloaded, the update skips this business sync and still completes the job scraping / ranking run. Websites, phone numbers and e-mail addresses can be manually added, edited, or deleted; manual changes remain authoritative during later updates.
- LLM ranking: jobs and businesses are scored through configurable weighted criteria; the LLM scores each criterion from 0 to 5 and the app calculates the final score, verdict and reasoning. Every business also receives a German e-mail draft even when no contact address was found. Unchanged jobs / businesses are skipped using content and ranking-context fingerprints.
- Structured CV profile: maintain skills, employment, education and certifications manually, or upload a PDF and selectively apply an editable AI-extracted preview.
- Automatic application e-mails: after Resend DNS verification in Settings, the Betriebe page can queue one CV-backed application e-mail per eligible business. Sending is paced on weekdays between 09:00 and 18:00 with short randomized gaps and a configurable daily send limit.
- Update button: one run scrapes, deduplicates, closes vanished listings, synchronizes businesses and rates new or changed entries. Structured progress is shown live with per-phase counts, LLM queue metrics and cancellation.
- OIDC-protected (Better Auth), German UI, light / dark mode.
| Area | Tools |
|---|---|
| Framework | SvelteKit, @sveltejs/adapter-node |
| UI | Tailwind CSS, shadcn-svelte, @lucide/svelte, mode-watcher |
| Data & Auth | PostgreSQL, Drizzle ORM, Better Auth |
| Forms | Superforms, Zod |
| Scraping | cheerio (HTML), Playwright (browser adapter), OSM Overpass |
| i18n / Tests | Paraglide, Vitest |
pnpm install
pnpm exec playwright install chromium # for browser-backed adaptersApplication runtime variables (required for local dev and deployment):
DATABASE_URL="postgres://postgres:postgres@localhost:5432/jobstrian"
ORIGIN="http://localhost:5173"
BETTER_AUTH_SECRET="…" # openssl rand -hex 32
OIDC_ISSUER="https://identity.example.com/realms/admins"
OIDC_CLIENT_ID="jobstrian"
OIDC_CLIENT_SECRET="…"
GEOAPIFY_API_KEY="…" # Geoapify Autocomplete APIIf you use the local Podman command in the next step, add PostgreSQL container variables too (used by the postgres image only):
POSTGRES_USER="postgres"
POSTGRES_PASSWORD="postgres"
POSTGRES_DB="jobstrian"Create the key in Geoapify and select Autocomplete API. The server uses it for Austrian address and city suggestions.
BODY_SIZE_LIMIT is no longer used by the app at runtime. CV uploads are currently
hard limited to 10 MB in src/routes/api/cv/+server.ts.
podman run --name postgres-sveltekit --env-file .env \
--publish 5432:5432 \
--volume postgres-sveltekit-data:/var/lib/postgresql/data \
--detach postgres:18-alpine
pnpm db:migratepnpm devOpen http://localhost:5173/login and sign in through the configured OIDC provider.
Provider access policy determines which administrators may use Jobstrian; the app does not maintain
an identity or claim allowlist. After signing in, enter your address, role keywords, business
categories and the OpenAI-compatible LLM (base URL including /v1, model, API key) in Settings.
Then click KI Status prüfen: Jobstrian sends one live request to the endpoint and only
marks the connection aktiviert if it succeeds. AI-powered features (profile import, search
and criteria generation) and the Aktualisieren run on the overview stay disabled until the
check passes, and the status resets whenever the base URL, model or API key changes.
For automatic applications, also configure Resend in Settings:
- In the Resend dashboard, create a full-access API key under API Keys and paste it into Resend API-Key. Jobstrian uses this key to create / read the sending domain and to send application e-mails.
- Enter the sending domain, sender local part, sender name and reply-to address in Settings.
- Click Resend-Status prüfen, add the shown DNS records at your domain provider, verify the domain in the Resend dashboard, then click Resend-Status prüfen again so Jobstrian can sync the verified status.
- In the Resend dashboard, create a webhook endpoint for
https://your-domain.example/api/webhooks/resend, replacing the host with the deployed Jobstrian URL. Enable at leastemail.bouncedandemail.complained. - Copy the webhook signing secret from Resend (usually
whsec_…) into Webhook Secret. Jobstrian uses it to verify that incoming bounce / complaint events really came from Resend. - Click Test-E-Mail senden to send a CV-backed test e-mail to the saved reply-to address before starting automatic applications.
For local development, Resend cannot call localhost directly; expose the app
with a tunnel and use that public tunnel URL as the webhook endpoint.
Upload a PDF CV and trigger the AI profile import manually in Settings. Then click Update on the dashboard.
Jobstrian uses Better Auth with the OIDC authorization code flow plus PKCE (S256), keeps sessions on the server, and clears the local session and provider SSO session on logout.
- Public Client: Off
- Callback URL:
/api/auth/oauth2/callback/oidc - Logout Callback URL:
/login - Required authentication variables:
ORIGIN,BETTER_AUTH_SECRET,OIDC_ISSUER,OIDC_CLIENT_ID,OIDC_CLIENT_SECRET(see Environment Variables → Required).
Production app, issuer, and discovered provider URLs must use HTTPS; plain HTTP is accepted only for loopback development. The database migration removes legacy credential identities and their auth sessions while preserving Jobstrian settings, CVs, listings, leads, and other app-owned data.
pnpm dev # development
pnpm check # type check (svelte-check)
pnpm build # production build
pnpm lint # Prettier + ESLintGenerate migration files with pnpm drizzle-kit generate after schema changes.
Pending migrations are applied automatically when the server starts (both pnpm dev
and production); pnpm drizzle-kit migrate remains available to apply them manually.
| Setting | Value |
|---|---|
| Build Pack | Nixpacks |
| Base Directory | / |
nixpacks.toml installs Chromium during the build with
pnpm exec playwright install --with-deps chromium so browser-backed portal
adapters work in production.
Pending migrations are applied automatically at server startup, so no post-deployment migration command is needed.
| Variable | Purpose |
|---|---|
DATABASE_URL |
PostgreSQL connection string. |
BETTER_AUTH_SECRET |
Auth secret — generate with openssl rand -hex 32. |
ORIGIN |
Public URL of the deployed app (e.g. https://jobstrian.example.com). |
OIDC_ISSUER |
Exact OIDC issuer URL from the provider discovery document. |
OIDC_CLIENT_ID |
Confidential OIDC client identifier registered for Jobstrian. |
OIDC_CLIENT_SECRET |
Confidential OIDC client secret. |
GEOAPIFY_API_KEY |
Server-side Geoapify Autocomplete API key for Austrian suggestions. |
CV uploads are hard capped to 10 MB by backend validation.
- Portal scrapers are inherently fragile: adapters prefer internal JSON endpoints, are isolated from each other (a broken adapter does not stop the run) and occasionally need maintenance when markup changes.
- LLM, Resend and profile configuration live in the database (
settingstable) and are maintained inside the app – not in.env.