A modern, white-label job board for staffing and recruiting firms running Bullhorn ATS. Forked from bullhorn/career-portal and rebuilt from the ground up — no proprietary UI dependencies, no legacy framework debt, and first-class support for Boostie apply flows.
The original Bullhorn career portal is built on Angular 10 and a proprietary component library (novo-elements) that is tightly coupled to Bullhorn's internal design system. This makes it difficult to customize, hard to maintain, and slow to update.
This fork replaces the entire UI layer with Tailwind CSS utility classes and standard Angular patterns, making the portal:
- Easy to white-label — change logo, colors, and copy through config and CSS variables, no component library to fight
- Easy to deploy — configure entirely through environment variables, no code edits required per client
- Boostie-ready — when a Boostie client ID is present, the portal automatically injects the Boostie script and wires up the apply experience end-to-end
| Original | This fork | |
|---|---|---|
| Framework | Angular 10 | Angular 18 (standalone components) |
| UI library | novo-elements (proprietary) |
Tailwind CSS |
| Icons | bullhorn-icons / bhi-* |
lucide-angular |
| Forms | novo-form |
Angular Reactive Forms |
| SSR | @nguniversal |
@angular/ssr (built-in) |
| Node | 16 | 20 LTS |
| Linting | tslint | ESLint + angular-eslint |
| Config | Edit JSON files | Environment variables |
- Node.js 20+
- npm 9+
- A Bullhorn ATS account with a public
corpTokenandswimlane - (Optional) A Boostie account with a
clientId
git clone https://github.com/boostie-talent/career-portal.git
cd career-portal
npm installCopy the example env file and fill in your values:
cp .env.example .envOpen .env and set at minimum:
COMPANY_NAME=Your Company Name
COMPANY_WEBSITE=https://yourcompany.com
COMPANY_LOGO_URL=./assets/logo.png
BULLHORN_SWIMLANE=91
BULLHORN_CORP_TOKEN=your_corp_token
BOOSTIE_CLIENT_ID=your-boostie-client-id # omit or leave blank to disableFinding your Bullhorn credentials:
corpTokenandswimlaneare visible in your Bullhorn ATS admin panel under API settings. ThecorpTokenis the short alphanumeric code used in public REST API URLs.
npm run configure # writes .env values into the build config
npm run serve # dev server at http://localhost:4200npm run configure
npm run build:static
# deploy dist/career-portal/browser/npm run build
# set env vars in your hosting environment, then:
npm startFor SSR deployments, env vars are read at server startup — no npm run configure step needed. Set them directly in your hosting dashboard or container environment.
| Variable | Required | Description |
|---|---|---|
COMPANY_NAME |
Yes | Displayed in the nav bar |
COMPANY_WEBSITE |
Yes | Company URL — shown as nav link |
COMPANY_LOGO_URL |
Yes | Path or URL to logo (any image format) |
BULLHORN_SWIMLANE |
Yes | Bullhorn swimlane number |
BULLHORN_CORP_TOKEN |
Yes | Bullhorn public corp token |
HOSTED_ENDPOINT |
No | Public URL of this portal (used in sitemaps/RSS) |
BOOSTIE_CLIENT_ID |
No | Boostie client ID — enables Boostie apply flow |
GOOGLE_ANALYTICS_TRACKING_ID |
No | GA tracking ID |
GOOGLE_VERIFICATION_CODE |
No | Google Search Console verification code |
PORT |
No | Server port for SSR (default 4000) |
ALLOWED_HOST |
No | Additional allowed hostname for SSRF mitigation |
When BOOSTIE_CLIENT_ID is set, the portal:
- Injects the Boostie script into
<head>at app boot (browser-only, SSR-safe) - Renders a
<button id="job-apply">on each job detail page for Boostie to wire up - Renders a hidden
<div id="jobId">so Boostie's script can identify the Bullhorn job - Suppresses the native Bullhorn apply modal entirely — Boostie owns the apply experience
To disable Boostie and use the native Bullhorn apply flow, omit BOOSTIE_CLIENT_ID or set it to empty.
Drop your logo file into src/assets/ and point COMPANY_LOGO_URL at it:
# any format works
COMPANY_LOGO_URL=./assets/logo.png
COMPANY_LOGO_URL=./assets/logo.svg
COMPANY_LOGO_URL=./assets/logo.webpThe nav constrains the image to 28px tall (h-7) with w-auto to preserve aspect ratio — any size source file renders correctly.
| Mode | Command | Use case |
|---|---|---|
static |
npm run build:static |
Browser-only bundle for CDN/static hosting |
dynamic |
npm run build |
Express + SSR bundle |
qa |
npm run build:qa |
QA environment |
src/
├── app/
│ ├── app.component.* # Root shell — nav + footer
│ ├── app.routes.ts # Routes (lazy-loaded)
│ ├── app.config.ts # App providers, i18n, settings init
│ ├── footer/ # Footer — customize here
│ ├── job-list/ # Job card grid
│ ├── job-details/ # Job detail + apply CTA
│ ├── apply-modal/ # Native Bullhorn apply form (non-Boostie)
│ ├── sidebar/ # Filter sidebar
│ │ └── sidebar-filter/ # Individual filter group (category/state/city)
│ ├── main-page/ # Two-column layout + search bar
│ ├── privacy-policy/ # Static privacy page
│ ├── services/
│ │ ├── settings/ # Loads app.json config at boot
│ │ ├── search/ # Bullhorn search API
│ │ └── apply/ # Bullhorn apply API (native flow only)
│ └── typings/settings.d.ts # ISettings interface
├── assets/ # Static assets (logo, etc.)
├── configuration/
│ ├── static/app.json # Config for static builds ← edit this for dev
│ ├── dynamic/app.json # Config for SSR builds
│ └── qa/app.json # Config for QA builds
└── static/i18n/ # Translation files (en, fr, es, de, ...)
scripts/
└── generate-config.js # Writes env vars into config JSON
This portal sends a single anonymous ping to Boostie once per browser session when it is loaded in production. The ping contains:
| Field | Example | Purpose |
|---|---|---|
domain |
careers.acmestaffing.com |
Identify the deployment |
version |
3.7.0 |
Track which portal version is in use |
timestamp |
2026-04-26T14:32:00Z |
First-seen / last-seen |
No personal data, no job seeker information, and no Bullhorn credentials are ever transmitted. The ping fires once per session (deduplicated via sessionStorage) and uses navigator.sendBeacon so it never blocks page load.
To opt out, set TELEMETRY_DISABLED=true in your environment:
# .env
TELEMETRY_DISABLED=trueThen run npm run configure (static builds) or set the env var in your hosting dashboard (SSR builds).
By default, static builds use Angular's hash-based routing (/#/jobs/...). This works on any static host without configuration, but isn't ideal for sharing or SEO.
To get clean URLs (/jobs/...), you need one redirect rule that tells your host to serve index.html for all paths. The Angular change is a one-liner — remove withHashLocation() from the router config in src/app/app.config.ts.
Add public/_redirects:
/* /index.html 200
Add vercel.json:
{
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}In your CloudFront distribution, add a custom error response:
- HTTP error code:
403and404 - Response page path:
/index.html - HTTP response code:
200
No changes needed — the Express server already handles all routes and returns index.html. Hash routing is only used in the static build.
Original open-source portal by Bullhorn.
Rebuilt and maintained by Boostie.