Multi-provider job board aggregator. Follow companies, get notified of new positions daily, and manage your application pipeline.
ekswai lets you follow companies across job boards and track their job postings. You add companies by their provider slug, the system validates them against the provider and syncs new positions every day. You get email notifications for companies you care about, and you manage your entire application pipeline from one dashboard.
For users:
- Register and go to My Companies
- Add a company slug (e.g.
laravelfrom apply.workable.com/laravel for Workable) - The system validates and syncs job postings automatically
- Browse your dashboard: bookmark, mark as submitted, track interviews, dismiss
For admins: Filament 4 panel to manage companies, job postings, users, and scraper configurations.
| Provider | Integration | Example URL |
|---|---|---|
| Workable | REST API | apply.workable.com/laravel |
| Lever | REST API | jobs.lever.co/scaleway |
| Ashby | REST API | jobs.ashbyhq.com/ramp |
| Greenhouse | REST API | boards.greenhouse.io/discord |
| Teamtailor | HTML Scraper | weroad.teamtailor.com/jobs |
| Factorial | HTML Scraper | shippypro.factorialhr.com |
API providers use JSON endpoints directly. HTML scraper providers use configurable CSS selectors with built in health checks and retry logic (managed via the admin panel).
| Component | Technology |
|---|---|
| Framework | Laravel 12 (PHP 8.4) |
| Frontend | React 19, Inertia.js, TypeScript, Tailwind CSS 4 |
| Database | PostgreSQL (SQLite for tests) |
| Admin | Filament 4 |
| Country Data | laravel-istat-foreign-countries |
| Testing | Pest |
| Static Analysis | PHPStan (Larastan) level 6 |
| Code Style | Laravel Pint |
| Refactoring | Rector (PHP 8.4, code quality, dead code, early return, type declarations) |
| CI/CD | GitHub Actions |
| Local Dev | Laravel Sail (Docker) |
DDD with three layers:
app/
├── Domain/
│ ├── Company/ # Company entity, JobBoardProvider enum
│ ├── JobPosting/ # Job posting with per-user status tracking
│ ├── ScraperConfig/ # CSS selectors and health check config for HTML scrapers
│ ├── Shared/ # BaseModel (UUIDs, $guarded = [])
│ ├── JobFilter/ # Per-user job posting filters (global + per-company overrides)
│ └── User/ # User with company subscriptions and job statuses
├── Application/
│ ├── Actions/
│ │ ├── Company/ # FollowCompany, UnfollowCompany
│ │ ├── JobPosting/ # SyncCompanyJobPostings, UpdateJobPostingStatus
│ │ ├── Notification/# NotifyUserOfNewJobs
│ │ └── Sync/ # RunDailySync (orchestrator)
│ ├── DTOs/ # JobPostingDTO
│ └── Services/ # JobFilterService (filter resolution and application)
└── Infrastructure/
├── Admin/Filament/ # Admin panel (Companies, Jobs, Users, ScraperConfigs)
├── Console/ # Artisan commands (jobs:sync-daily)
├── Mail/ # NewJobsFoundMail, ScraperHealthAlertMail
└── Services/
├── Contracts/ # JobBoardClient interface
├── Workable/ # Workable API client
├── Lever/ # Lever API client
├── Ashby/ # Ashby API client
├── Greenhouse/ # Greenhouse API client
├── Teamtailor/ # Teamtailor HTML scraper
├── Factorial/ # Factorial HTML scraper
├── Scraping/ # BaseHtmlScraper, ScraperHealthChecker, exceptions
└── JobBoardClientFactory.php
Provider pattern: each job board integration implements the JobBoardClient interface (fetchJobsForCompany and validateSlug). The JobBoardProvider enum lists available providers, and JobBoardClientFactory resolves the correct client. API providers (Workable, Lever, Ashby, Greenhouse) call JSON endpoints directly. HTML scraper providers (Teamtailor, Factorial) extend BaseHtmlScraper which handles retry logic, DOM parsing, and validation using CSS selectors from ScraperConfig.
Job Filters:
Users can define filters to control which job postings appear in their dashboard and email notifications. Filters support title keywords (include/exclude), country (powered by laravel-istat-foreign-countries for ISO country matching), remote only, and department. Filters are global by default, with optional per-company overrides. All criteria combine with AND logic, values within each criterion combine with OR. Filter data is stored in the job_filters table and managed through the Filters page (global) or the company filter dialog (per-company overrides).
Key relationships:
Users subscribe to companies via company_user pivot (with email notification toggle). Each user has a per-job status via job_posting_user pivot (new, bookmarked, submitted, interview, dismissed). Users can have one global job filter and optional per-company overrides via the job_filters table. The sync is shared: one API/scrape call per company regardless of subscriber count.
- Add a new case to
JobBoardProviderenum (app/Domain/Company/JobBoardProvider.php) - Create a class implementing
JobBoardClient(app/Infrastructure/Services/Contracts/JobBoardClient.php) - Register it in
JobBoardClientFactory::make()(app/Infrastructure/Services/JobBoardClientFactory.php) - For HTML scraper providers: add a
ScraperConfigentry with CSS selectors inScraperConfigSeeder
git clone https://github.com/danielebarbaro/ekswai-jobs-scraper.git
cd ekswai-jobs-scraper
composer install
cp .env.example .env
php artisan key:generate
./vendor/bin/sail up -d
./vendor/bin/sail artisan migrate --seed
./vendor/bin/sail artisan foreign-countries:import
npm install && npm run buildOpen http://localhost and register. Then go to /companies and add a company by selecting a provider and entering its slug.
The seeder creates an admin user (me@plincode.tech / password) with demo companies across all 6 providers, already synced with real job postings.
# Sync job postings from all active companies
./vendor/bin/sail artisan jobs:sync-daily
# Add a company from CLI
./vendor/bin/sail artisan companies:add ashby jimdo.com
./vendor/bin/sail artisan companies:add greenhouse carta
# Import ISTAT country data (required for country filters)
./vendor/bin/sail artisan foreign-countries:import
# Run tests
composer test
# Code style (fix)
composer lint
# Static analysis
composer analyse
# Rector refactoring
composer rector # Apply changes
composer rector-dry # Preview changes
# Run analyse + test together
composer checkThe sync runs automatically every day at 9:00 AM UTC via Laravel scheduler.
MIT
