
August 16, 2026
9 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
Building a Nepal Job Portal from Scratch is fundamentally an exercise in managing high-volume, time-sensitive content rather than just coding a CRUD application. The primary challenge isn't displaying a list of vacancies; it is architecting a system that handles thousands of expiring listings, integrates local payment gateways like eSewa or Khalti for employer credits, and maintains indexability for Google without collapsing under duplicate content penalties. If you are planning this build in 2026, you need a stack that balances rapid feature iteration with strict performance budgets, typically Laravel 12 on PHP 8.4 backed by MySQL 8.4 and Redis.
How do you architect the database schema for a Nepal-specific job portal?
The most common mistake when building database-driven websites in Nepal is treating a job portal like a standard blog. Jobs have lifecycles: they are drafted, published, featured, expired, archived, and deleted. Your schema must reflect this state machine explicitly to support both user experience and search engine crawling.
In my experience shipping directory and listing platforms, a normalized schema prevents data integrity issues as volume grows. You need separate tables for employers, jobs, applications, and transactions. Crucially, for the Nepal market, you must account for variable salary formats (monthly vs. annual, NPR vs. USD) and location hierarchies that go beyond simple city names.
For salary storage, never use a single string field. Store salary_min and salary_max as unsigned integers representing NPR. This allows filtering ("Jobs above Rs 50,000") and aggregation. Display formatting happens at the view layer. For locations, create a districts table referencing Nepal’s 77 districts rather than free-text input. This standardization powers faceted search and prevents "Ktm", "Kathmandu", and "KTM" from fragmenting your results.
// Migration excerpt for jobs table (Laravel 12 / PHP 8.4)
Schema::create('jobs', function (Blueprint $table) {
$table->id();
$table->foreignId('employer_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->string('slug')->unique();
$table->text('description');
$table->unsignedInteger('salary_min')->nullable();
$table->unsignedInteger('salary_max')->nullable();
$table->enum('salary_period', ['monthly', 'annual', 'fixed'])->default('monthly');
$table->foreignId('district_id')->constrained();
$table->timestamp('expires_at')->index();
$table->enum('status', ['draft', 'published', 'expired', 'filled'])->default('draft');
$table->timestamps();
// Composite index for common public queries
$table->index(['status', 'expires_at', 'district_id']);
}); How do you integrate eSewa and Khalti payments for employer packages?
Monetizing a job portal in Nepal almost always involves selling posting credits or subscription packages to employers. International gateways like Stripe are rarely useful here because most Nepali HR departments and SMEs operate via local banking channels. You must integrate eSewa, Khalti, or ConnectIPS. I have implemented these integrations on multiple projects, and the pattern is consistent: treat every payment as an asynchronous webhook event, never as a synchronous redirect confirmation.
A common failure mode is trusting the success URL redirect. Users close tabs, networks timeout, and browsers block redirects. Your system must only grant credits when the gateway confirms the transaction via server-to-server verification. For Laravel payment integrations, create a dedicated PaymentVerificationService that handles idempotency. If eSewa sends the same webhook twice (which happens), your system should recognize the transaction_id and skip re-crediting.
- Package Model: Create a
packagestable defining credit amounts, validity periods, and prices in NPR. - Transaction Logging: Log every API request and response to the gateway. When a client claims "I paid but credits aren't showing," you need the raw JSON response to diagnose whether it was a gateway delay or a user error.
- Credit Ledger: Don't just update a
balancecolumn. Maintain acredit_transactionsledger (debit/credit entries) so you can audit exactly when and why credits were added or consumed. - Invoice Generation: Nepali businesses require VAT/PAN-compliant invoices for tax deductions. Generate PDF invoices automatically upon successful verification using a package like
barryvdh/laravel-dompdf.
What technical SEO strategy prevents index bloat on job boards?
Job portals are SEO minefields. Every job listing is a unique page today but dead content tomorrow. If Google indexes 10,000 expired jobs, your crawl budget evaporates and your domain authority tanks. When conducting technical SEO audits for listing sites, I frequently see this exact problem: massive indexation of low-value, expired content.
The solution is a proactive expiration strategy combined with structured data. Never return a 404 for an expired job that received traffic. Instead, implement a soft-expiry workflow:
- Active Phase: Job is live, indexable, canonical points to self. Schema markup includes
validThrough. - Grace Period (7 days post-expiry): Page remains accessible with a "This position has been filled" banner. Add
noindexmeta tag. Keep the content visible for users who bookmarked it, but tell Google to drop it. - Archive Phase: Redirect (301) the expired job URL to the most relevant category page (e.g., "Marketing Jobs in Kathmandu"). This preserves link equity from any external backlinks the specific job acquired.
- Sitemap Hygiene: Your XML sitemap must ONLY contain active jobs. Run a scheduled command hourly to regenerate the sitemap and remove expired entries. Submit sitemaps split by category to aid debugging in Search Console.
Additionally, implement JobPosting schema.org markup rigorously. Google for Jobs relies entirely on this structured data. Validate every listing against Google’s Rich Results Test before deployment. Missing datePosted or employmentType will exclude you from the job carousel, which drives the majority of organic traffic in this niche.
How do you handle CV uploads and file security in Laravel?
Resumes contain PII (Personally Identifiable Information). Storing them in the public web root is a critical security vulnerability. On legal-tech portals I’ve built, document security is non-negotiable, and the same standards apply to job applications. Never store CVs in public/uploads/. Use Laravel’s private filesystem driver and serve files through signed URLs or a controller that checks authorization.
For a job portal, employers should only access CVs for jobs they own. Implement policy-based authorization:
// In ApplicationController.php
public function downloadCv(Application $application)
{
// Authorize: only the employer who owns the job can download
if ($application->job->employer_id !== auth()->id()) {
abort(403);
}
// Return file from private disk with secure headers
return Storage::disk('private')->download(
$application->cv_path,
'candidate-cv-' . $application->id . '.pdf',
['Content-Type' => 'application/pdf']
);
} Validate file types strictly on the server side. MIME type checking alone is insufficient; attackers can spoof Content-Type headers. Use the mimes validation rule combined with file extension whitelisting (pdf,doc,docx). Scan uploads with ClamAV if possible, especially if you allow executable formats (which you shouldn’t). For performance, offload virus scanning to a queued job so the upload response remains instant.
What infrastructure supports high-frequency job searches and alerts?
Search is the core product. Default MySQL LIKE queries won’t scale beyond a few thousand listings. For a production Nepal job portal, you have two viable paths depending on budget and complexity tolerance.
| Feature | MySQL Full-Text (InnoDB) | Meilisearch / Typesense |
|---|---|---|
| Setup Complexity | Low (built-in) | Medium (separate service) |
| Relevance Ranking | Basic TF-IDF | Typo-tolerant, customizable |
| Nepali Language Support | Poor (needs custom tokenizer) | Good (Unicode-native) |
| Faceted Filtering | Slow on large datasets | Instant, native support |
| Infrastructure Cost | Zero extra (uses DB server) | ~Rs 2,000–4,000/mo VPS |
| Best For | <5,000 active jobs | >5,000 jobs + typo tolerance |
If you choose Meilisearch (my recommendation for serious portals), use Laravel Scout with Meilisearch for seamless Eloquent integration. Index only active jobs. Configure synonyms for Nepali/English variations: "ktm" → "kathmandu", "dev" → "developer". This dramatically improves zero-result rates.
Email alerts are equally critical. Job seekers expect daily or weekly digests matching their criteria. Do not send emails synchronously during registration. Use Laravel Queues with Redis as the driver. Batch alert emails to respect SMTP rate limits. Personalize subject lines with actual job titles, not generic "New Jobs Available." Track open rates and unsubscribe links meticulously; spam complaints will destroy your domain reputation faster than any technical debt.
Start Building Your Nepal Job Portal with the Right Foundation
Building a Nepal Job Portal from Scratch is a significant engineering undertaking that demands attention to local payment realities, SEO hygiene, and scalable search architecture. Start with a solid Laravel 12 foundation, model your database around job lifecycles rather than static content, and integrate eSewa/Khalti with proper webhook verification from day one. Prioritize technical SEO early to avoid costly rewrites later. If you need guidance on architecture decisions, payment integration, or performance optimization for your job portal project, reach out to discuss your requirements. I help founders and agencies build production-grade platforms tailored to the Nepali market.

