To build a Laravel job board with AI, describe the domain in plain English (employers, job listings, applications, and candidates), let the AI generate the Eloquent models, migrations, Policies, FormRequests, and admin screens, then review that output like any pull request and deploy it. A working first version takes days instead of months.

Most job board tutorials stop at a jobs table and a Blade index page. Real job portals in Laravel break somewhere else. The expired listing that still ranks in Google. The employer who can edit a rival's post. The filter query that crawls once the table passes 40,000 rows.

You already know the domain is not hard, it is just broad. That breadth is exactly what AI generation is good at, and exactly where an AI that knows Laravel conventions beats one that writes generic PHP. This guide walks through the data model that survives scale, the prompts that produce reviewable output, search and job alerts, employer authorization, billing, and the launch details that decide whether candidates ever find your listings.

Key Takeaways

What a Laravel job board actually has to do

You can build a job board in Laravel over a weekend. Making it survive 40,000 listings and 200 employers is the actual project. A Laravel job board looks like a CRUD app and behaves like a marketplace, because four groups touch the same data and each wants something different from it.

Surface

Who uses it

What Laravel handles

Public job search

Candidates

Eloquent scopes, full text search, cached filter counts

Application flow

Candidates

Validated resume uploads, queued mail, notifications

Employer dashboard

Hiring teams

Policies, FormRequests, company scoped queries

Moderation

You

Filament resources, soft deletes, audit trail

Billing

Employers

Subscription plans, Stripe webhooks, queue workers

Count the rows that have nothing to do with a jobs table. Authorization, uploads, queues, webhooks, and admin screens are where the weeks disappear. The listing CRUD itself is an afternoon.

Scope version one hard. One employer role, one candidate role, listings with an expiry date, applications with a resume upload, and an admin who can unpublish anything. Saved searches, company profile pages, and salary benchmarking can wait until a paying employer asks for them.

Hand writing that scaffolding is the part nobody enjoys twice. Describe it once instead and Try StackMivo Now to generate the models, migrations, and admin screens as standard Laravel you own.

Build your Laravel app with AI

Skip the boilerplate. StackMivo turns a plain-English prompt into production-ready Laravel apps, with models, migrations, controllers, CRUD, auth, and tests.

Get started free

The data model behind a Laravel job board

Six tables carry the product. Get their columns right and the rest of the build stops fighting you. Start with the listing, because every other decision hangs off it.

Schema::create('job_listings', function (Blueprint $table) {
    $table->id();
    $table->foreignId('company_id')->constrained()->cascadeOnDelete();
    $table->string('title');
    $table->string('slug')->unique();
    $table->text('description');
    $table->string('location')->nullable();
    $table->boolean('is_remote')->default(false);
    $table->unsignedInteger('salary_min')->nullable();
    $table->unsignedInteger('salary_max')->nullable();
    $table->string('employment_type')->default('full_time');
    $table->string('status')->default('draft');
    $table->timestamp('published_at')->nullable();
    $table->timestamp('expires_at')->nullable();
    $table->timestamps();

    $table->index(['status', 'published_at']);
});

Two columns do most of the work here. status keeps drafts out of public queries, and expires_at turns expiry into a data fact rather than a side effect of a scheduled command. The composite index on status and published_at is what keeps the search page responsive past 50,000 rows.

Relationships and the one scope you will reuse everywhere

The model stays thin. Relationships, casts, and a single query scope cover almost every read path in the app.

class JobListing extends Model
{
    protected $casts = [
        'is_remote' => 'boolean',
        'published_at' => 'datetime',
        'expires_at' => 'datetime',
    ];

    public function company(): BelongsTo
    {
        return $this->belongsTo(Company::class);
    }

    public function applications(): HasMany
    {
        return $this->hasMany(Application::class);
    }

    public function skills(): BelongsToMany
    {
        return $this->belongsToMany(Skill::class);
    }

    public function scopeLive(Builder $query): void
    {
        $query->where('status', 'published')
            ->whereNotNull('published_at')
            ->where(fn ($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()));
    }
}

That live() scope is the most valuable thing in the file. Controllers, RSS feeds, the sitemap, and every alert email call it, so an expired role cannot reach a candidate. Laravel's Eloquent relationships guide covers the pivot and polymorphic patterns you will want when listings need multiple locations or attachment types.

Scaffolding it with Artisan

php artisan make:model JobListing --all
php artisan make:model Application --all
php artisan make:model Company --all

The --all flag returns the migration, factory, seeder, resource controller, policy, and both FormRequests in one pass. For the full flag list, the php artisan make:model cheatsheet breaks down every option and when it earns its keep.

How to build a Laravel job board with AI step by step

AI generation works when you treat it as a senior pair who needs the domain explained once, precisely. Five steps, in this order.

  1. Describe the domain before the code. Roles, entities, and the rules that connect them. An employer belongs to one company. A candidate applies once per listing. A listing expires 30 days after publishing.

  2. Name the Laravel artifacts you expect back. Ask for the migration, the Eloquent relationships, the Policy, the FormRequest, and the Pest test. Vague prompts return vague PHP.

  3. Generate one vertical slice at a time. Listing publishing, then application submission, then employer billing. A slice you can test beats a codebase you have to audit.

  4. Review it like a pull request. Check eager loading, mass assignment, registered Policies, indexes on filtered columns, and anything that sends mail synchronously.

  5. Ask for the tests in the same conversation. The model that wrote the feature knows the edge cases it skipped, so make it write the failing paths too.

What a useful prompt looks like

Add job listing publishing to this Laravel app.

An employer can publish a JobListing their company owns. Publishing sets
status to published, stamps published_at, and sets expires_at 30 days out.
Enforce ownership with a JobListingPolicy, validate with a
PublishJobListingRequest, fire a JobPublished event, and queue a digest
for candidates whose saved search matches the listing.

Include Pest feature tests for the happy path, an employer publishing a
listing they do not own, and an employer already at their monthly limit.

That prompt is specific about artifacts, ownership, and failure cases, which is why the output is reviewable instead of decorative. Assistants trained mostly on JavaScript return PHP that runs and Laravel a reviewer rejects. Laravel aware AI code generation returns the FormRequest, the Policy, and the Filament resource in the shapes your team already reviews, across Laravel 9 through 12.

A small team building a job portal for a staffing client generated the employer dashboard in an afternoon and pushed it to staging the same day. Two days later a tester signed in as one employer and edited another company's listing. The controller was fine. Nobody had registered the Policy on the route, and the update request accepted a company_id straight from the payload. The fix took ten minutes. The trust took a fortnight. Generated code earns the same review as code you typed yourself.

Ready to point this at your own schema? You can Get started free and generate the first vertical slice against your existing repository.

Search, filters, and job alerts that keep candidates coming back

Candidates rarely browse. They filter, leave, and come back for the alert email. Both paths have to be fast and correct.

JobListing::query()
    ->live()
    ->with('company:id,name,logo_path')
    ->when($request->input('q'), fn ($query, $term) => $query->whereFullText(['title', 'description'], $term))
    ->when($request->boolean('remote'), fn ($query) => $query->where('is_remote', true))
    ->when($request->integer('min_salary'), fn ($query, $min) => $query->where('salary_max', '>=', $min))
    ->latest('published_at')
    ->paginate(20);

The with() call prevents the N+1 query that every job board ships at least once. whereFullText needs a fulltext index on those two columns, so add it in the migration rather than discovering it in production. Once you need facet counts, typo tolerance, or relevance tuning, move search to Laravel Scout and keep the structured filters in Eloquent.

Job alerts belong on the queue

A saved search that emails matching roles is the feature that brings candidates back, and it is also the feature that will time out your web requests. Push it onto queued jobs, batch recipients with chunkById, and rate limit the mailer. One queued digest per saved search, dispatched from a scheduled command, keeps the whole thing observable as volume grows.

Index maintenance follows the same rule. Publishing a listing should dispatch the indexing job, not wait for it, so the employer's save button stays instant.

Employer dashboards, authorization, and moderation

This is where frontend first AI builders stall. Two employers on the same Laravel job board must never see each other's applicants, and no amount of UI polish substitutes for a Policy.

class JobListingPolicy
{
    public function update(User $user, JobListing $listing): bool
    {
        return $user->role === UserRole::Employer
            && $user->company_id === $listing->company_id;
    }

    public function viewApplications(User $user, JobListing $listing): bool
    {
        return $this->update($user, $listing);
    }
}

Three habits keep the dashboard honest. Authorize in the Policy, validate in the FormRequest, and scope in the query so a stray route parameter cannot cross a company boundary. Then add the admin layer. A Filament resource for listings, companies, and applications gives you moderation, bulk unpublish, and somewhere to answer support questions from day one.

Multi vendor is the honest description of a job board, one platform, many companies, isolated data. The same architecture powers the marketplace use case, so the authorization and tenancy patterns transfer directly.

Soft deletes matter more than they look. When an employer removes a listing, applications, interview notes, and billing records still reference it. Soft delete the listing, keep the audit trail, and let admins restore whatever a support ticket asks about.

Payments, structured data, and getting the Laravel job board live

A Laravel job board makes money three ways, paid single listings, featured placement, and employer subscriptions. All three come down to the same pieces, a plan or credit balance on the company, a Stripe checkout, and a webhook that flips the listing to published once payment clears. Handle that webhook in a queued listener so a slow response never blocks a publish.

Then comes the part most tutorials skip. Google for Jobs reads JobPosting structured data, and it expects title, description, datePosted, validThrough, hiringOrganization, and jobLocation. Render it from the same model attributes your live() scope already trusts, and expiry stays consistent across the page, the feed, and the schema.

A solo developer launched a regional job board for warehouse roles and got indexed within a month. Traffic climbed, then sagged. Expired listings were still returning 200 with a dead apply button, so Google kept them in the index and candidates kept bouncing. The repair was small. Return 410 for expired listings, set validThrough honestly, and drop the URL from the sitemap the moment it expires. Rankings recovered over the following weeks.

Before launch, work through the short list that actually breaks job boards.

  • Queue worker running under a process supervisor, with a failed jobs table somebody watches.

  • Resume uploads validated by MIME type and size, stored off the public disk.

  • Rate limiting on the application endpoint so one bot cannot flood an employer's inbox.

  • A scheduled command that expires listings and refreshes the sitemap daily.

  • Sitemap and feed built from the live() scope, never from a raw query.

Deployment is the last mile, and it is worth automating on day one. A one-click deployment to Laravel Cloud, Forge, Ploi, or your own SSH server runs migrations, clears caches, and restarts queue workers, with a rollback ready if a release misbehaves.

Start with the smallest Laravel job board that works

Ship the version an employer can pay for and a candidate can apply through, then grow it. That means six tables, a live() scope guarding every public read, a Policy on every employer action, queued mail, valid JobPosting markup, and an automated deploy. Everything after that is iteration.

AI changes the cost of the boring 70% of the work. Migrations, resource controllers, FormRequests, Filament screens, and the first round of Pest tests stop being a fortnight of typing, which leaves your attention for the decisions that need judgment, pricing, moderation policy, and how listings rank. The same pattern applies to any multi vendor build, as the walkthrough on how to build a marketplace app with AI shows.

Pick your first vertical slice this week. Listing publishing with an expiry date and a Policy is a good one, because it forces the data model, the authorization layer, and the public query into shape at the same time. Get that slice reviewed and deployed, and the rest of the job portal is repetition you already know how to handle.

Build your Laravel app with AI

Skip the boilerplate. StackMivo turns a plain-English prompt into production-ready Laravel apps, with models, migrations, controllers, CRUD, auth, and tests.

Get started free