# Production deployment plan

This guide covers server requirements, deploy steps, file permissions, cron, and post-deploy checks for **KiyoHR / HRMS SaaS** (Laravel 12).

---

## 1. Server requirements

| Component | Minimum |
|-----------|---------|
| PHP | **8.2+** (extensions: `bcmath`, `ctype`, `curl`, `dom`, `fileinfo`, `json`, `mbstring`, `openssl`, `pdo`, `pdo_mysql`, `tokenizer`, `xml`, `zip`, `gd` or `imagick` if generating images/PDFs) |
| Composer | **2.8+** |
| Database | **MySQL 8.0+** or MariaDB 10.6+ |
| Web server | **Nginx** or **Apache** (document root = `public/`) |
| OS | Linux (Ubuntu 22.04/24.04 recommended) |

Optional for scale:

- **Redis** — cache (`CACHE_STORE=redis`)
- **SSL** — Let's Encrypt / Cloudflare (HTTPS required for production)

---

## 2. Directory layout (Linux)

Typical path:

```text
/var/www/hrms-saas-laravel/     ← application root (NOT web root)
/var/www/hrms-saas-laravel/public/   ← only this folder is exposed to the web
```

**Never** point the web server document root to the project root. Only `public/` must be reachable.

---

## 3. Pre-deploy checklist

- [ ] Code merged and tested on staging
- [ ] `composer.lock` committed (Laravel **12.61.1+**)
- [ ] `.env` prepared on server (never commit `.env`)
- [ ] Database backup taken
- [ ] DNS / SSL configured for production domain
- [ ] Mail (SMTP), Razorpay, SMS/WhatsApp keys ready if used

---

## 4. First-time production setup

### 4.1 Clone and install

```bash
cd /var/www
git clone <your-repo-url> hrms-saas-laravel
cd hrms-saas-laravel

composer install --no-dev --optimize-autoloader --no-interaction
```

> Use **`composer install`**, not `composer update`, on production.

### 4.2 Environment

```bash
cp .env.example .env   # or copy from secure vault
php artisan key:generate
```

Essential `.env` values:

```env
APP_NAME="HRMS SaaS"
APP_ENV=production
APP_DEBUG=false
APP_URL=https://your-domain.com
APP_VERSION=1.0.0

LOG_CHANNEL=daily
LOG_LEVEL=warning
LOG_DAILY_DAYS=14

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=kiyohr_live_sass
DB_USERNAME=...
DB_PASSWORD=...

SESSION_DRIVER=file
SESSION_ENCRYPT=true
SESSION_SECURE_COOKIE=true
SESSION_SAME_SITE=lax

# Comma-separated browser origins for Sanctum/SPA (never use *)
CORS_ALLOWED_ORIGINS=https://your-domain.com

QUEUE_CONNECTION=sync
CACHE_STORE=file

MAIL_MAILER=smtp
# ... mail settings
```

See also `docs/security/` for the full production security pack.

### 4.3 Database

```bash
php artisan migrate --force
# Optional one-time seed (roles/plans only — not demo users on live):
# php artisan db:seed --class=RolePermissionSeeder
```

### 4.4 Storage link

```bash
php artisan storage:link
```

Uploads (resumes, documents, logos) use `storage/app/public` → `public/storage`.

### 4.5 Optimize

```bash
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache   # if using events
```

### 4.6 File permissions (see section 6)

Apply ownership and chmod **before** going live.

---

## 5. Release / update deploy

Run on each release after `git pull`:

```bash
cd /var/www/hrms-saas-laravel

git pull origin main

composer install --no-dev --optimize-autoloader --no-interaction

php artisan migrate --force

php artisan config:cache
php artisan route:cache
php artisan view:cache
```

Put the app in maintenance mode for large migrations:

```bash
php artisan down --retry=60 --secret="your-secret-token"
# deploy steps...
php artisan up
# or visit https://your-domain.com/your-secret-token during maintenance
```

---

## 6. File permissions

### 6.1 Linux — recommended (Nginx/Apache + PHP-FPM)

Assume:

- Web server user: **`www-data`**
- Deploy user: **`deploy`** (your SSH user)

**Ownership** — app owned by deploy, web group can write storage/cache:

```bash
cd /var/www/hrms-saas-laravel

sudo chown -R deploy:www-data .
sudo find . -type f -exec chmod 644 {} \;
sudo find . -type d -exec chmod 755 {} \;
```

**Writable directories** (Laravel must write here):

```bash
sudo chown -R deploy:www-data storage bootstrap/cache
sudo chmod -R ug+rwx storage bootstrap/cache
```

**Protect secrets** — `.env` readable only by owner:

```bash
chmod 600 .env
chown deploy:deploy .env
```

**Must NOT be world-writable:**

```bash
# Never do this on production:
# chmod -R 777 storage
```

### 6.2 Permission reference table

| Path | Owner | Mode | Notes |
|------|-------|------|-------|
| Project root | `deploy:www-data` | dirs `755`, files `644` | Code read-only for web user |
| `public/` | `deploy:www-data` | `755` | Document root |
| `storage/` | `deploy:www-data` | `775` dirs, `664` files | Logs, cache, sessions, uploads |
| `bootstrap/cache/` | `deploy:www-data` | `775` | Compiled config/routes/views |
| `.env` | `deploy:deploy` | `600` | Secrets |
| `vendor/` | `deploy:www-data` | `755`/`644` | From `composer install` |

### 6.3 SELinux (RHEL/CentOS)

If enabled:

```bash
sudo chcon -R -t httpd_sys_rw_content_t storage bootstrap/cache
sudo setsebool -P httpd_can_network_connect 1   # if app calls external APIs
```

### 6.4 Windows / WAMP (dev or small hosting)

| Path | Permission |
|------|------------|
| `storage/` | IIS_IUSRS or `IUSR` — **Modify** |
| `bootstrap/cache/` | Same — **Modify** |
| `.env` | Administrators + app pool identity — **Read** only |
| `public/` | Web root in IIS site binding |

Apache on Windows: grant `Modify` on `storage` and `bootstrap/cache` to the Apache service account.

---

## 7. Web server

### Nginx (snippet)

```nginx
server {
    listen 443 ssl http2;
    server_name your-domain.com;
    root /var/www/hrms-saas-laravel/public;

    index index.php;
    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_hide_header X-Powered-By;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}
```

### Apache

Enable `mod_rewrite`. DocumentRoot = `public/`. Ensure `.htaccess` in `public/` is allowed (`AllowOverride All`).

---

## 8. Scheduler (cron)

Laravel scheduler must run every minute:

```cron
* * * * * cd /var/www/hrms-saas-laravel && php artisan schedule:run >> /dev/null 2>&1
```

Scheduled jobs in this app include:

| Job | Schedule |
|-----|----------|
| `leaves:accrue-monthly` | 1st of month, 01:00 |
| `leaves:carry-forward` | 1 Jan, 02:00 |
| `employees:deactivate-past-lwd` | Daily 00:30 |
| `leads:send-due-followup-reminders` | Every 5 minutes |

Use crontab for user `deploy` or `www-data` (consistent with file ownership).

---

## 9. Background work (no queue workers)

This application **does not use Laravel jobs or queue workers**. Notifications and post-request side effects run in the same PHP process after the HTTP response (`defer()` / `App\Support\AfterResponse`).

Set:

```env
QUEUE_CONNECTION=sync
```

Use the scheduler (`php artisan schedule:run` via cron) for recurring Artisan commands such as `leads:send-due-followup-reminders`. Do **not** run `php artisan queue:work` for HRMS notifications or imports.

---

## 10. Logging

- App log: `storage/logs/laravel-YYYY-MM-DD.log`
- HRMS channel: `storage/logs/hrms-YYYY-MM-DD.log`
- Ensure `storage/logs` is writable and **not** under web root
- Rotate or ship logs (logrotate, CloudWatch, etc.)

---

## 11. Branding & public assets

- **Favicon:** `public/favicon.ico` or `public/kiyohr.png`
- **Logo:** `public/logo.png`
- Tenant uploads: `storage/app/public/` (linked to `public/storage`)

---

## 12. Security hardening

- [ ] `APP_DEBUG=false`, `APP_ENV=production`
- [ ] HTTPS only; HSTS at load balancer
- [ ] `.env` mode `600`, outside git
- [ ] Block web access to `/storage`, `/vendor`, `/.env`, `/.git`
- [ ] Strong DB passwords; DB user with least privilege
- [ ] Change default seeded passwords (`docs/LOGIN.md`)
- [ ] `composer audit` — no advisories (Laravel 12.61.1+)
- [ ] Rate-limit API if exposed publicly
- [ ] Backups: daily DB + weekly `storage/app` snapshot

---

## 13. Post-deploy verification

```bash
php artisan about                    # Laravel 12.x, production env
composer audit                       # no vulnerabilities
php artisan route:list | head        # routes cached OK
curl -I https://your-domain.com/up    # health check (200)
```

Manual smoke tests:

1. Login — tenant admin + employee + superadmin
2. Dashboard, employees, attendance, leave, payroll
3. File upload (document / resume)
4. API login (`POST /api/auth/login`) if mobile app is used — if 2FA enabled, verify with `POST /api/auth/two-factor/verify`
5. Notifications: trigger a leave/expense action and confirm in-app notification appears (no queue worker)
6. Scheduler: `php artisan schedule:list`

**Auth environment (production):**

| Variable | Default | Purpose |
|----------|---------|---------|
| `FACE_LOGIN_ENABLED` | `false` | Keep off unless face login is explicitly approved |
| `TWO_FACTOR_MAX_ATTEMPTS` | `5` | Lockout after failed OTP attempts |
| `TWO_FACTOR_LOCKOUT_MINUTES` | `15` | OTP lockout duration |
| `SANCTUM_TOKEN_EXPIRATION_MINUTES` | `10080` (7 days) | API token lifetime; `never` or `0` = no expiry |
| `API_REVOKE_TOKENS_ON_LOGIN` | `true` | Revoke other API tokens when user logs in again |

Login, 2FA verify, and face login endpoints are rate-limited per IP (and per email for login).

**Sessions (production):** set `SESSION_DRIVER=database` and run migrations so revoked sessions are invalidated server-side. The `sessions` table migration is included in this project.

**Payroll smoke test (admin with `payrolls_view`):**

1. Open `/payroll?tab=run` — month grid, employee table, no broken links
2. **Generate / Re-generate** for current month — rows appear with gross/net
3. **Process All** — cycle status becomes completed; payslip links work
4. **Export** CSV and **Bank Export** — files download
5. Open a payslip → **Download PDF**
6. **Lock Cycle** — generate/process buttons hidden; unlock if needed
7. Employee login → **Payroll → My Pay → Payslips** — view own payslip only

**Payroll smoke test (employee):**

1. Sidebar **Payroll** opens Summary / My Pay (not Run tab)
2. Direct `/payroll?tab=run` redirects to Summary

---

## 14. Payroll go-live checklist

Payroll uses **PayrollCycle + EmployeePayroll**. The admin run screen is **`/payroll?tab=run`**. Cycle actions use **`/payroll/cycles/{id}/…`**.

### Before first live run

| Step | Action |
|------|--------|
| 1 | **Salary structure** — salary groups assigned to employees; components (Basic, HRA, PF, etc.) configured |
| 2 | **Statutory** — PF/ESI/PT toggles and slabs under Payroll → Setup / Statutory |
| 3 | **Bank details** — employee account number + IFSC for bank file export |
| 4 | **Attendance & leave** — month attendance finalized (LOP affects net pay) |
| 5 | **Permissions** — after finance role split run `php artisan hrms:finance-migrate-permissions`; otherwise `hrms:reset-permissions` |
| 6 | **Legacy data** — if migrating from old `payrolls` table, run once: `php artisan payroll:backfill-v2` |

### Monthly run workflow

1. **Run Payroll** tab → select month → **Generate / Re-generate**
2. Review employee rows; open payslips for spot checks
3. **Process All** when totals are correct (marks cycle completed)
4. **Export** summary CSV; **Bank Export** for NEFT file
5. **Lock Cycle** after payout — prevents accidental re-runs

### Canonical URLs

| Purpose | URL |
|---------|-----|
| Run payroll (admin) | `/payroll?tab=run&year=YYYY&month=M` |
| Generate / complete / lock | `POST /payroll/cycles/{id}/run` (etc.) |
| Admin payslip | `/payroll/cycles/{id}/payslips/{employeePayroll}` |
| Settings | `/payroll?tab=setup` or `tab=statutory` |
| Audit log | `/payroll?tab=audit` |
| Employee self-service | `/payroll?tab=my` |
| Old `/payroll/v2/*` bookmarks | Redirect to the URLs above |

### Production commands

```bash
php artisan hrms:reset-permissions          # after permission config changes
php artisan payroll:backfill-v2             # one-time migration from old payrolls table (if needed)
php artisan route:list --name=payroll       # verify routes after deploy
```

### Do not

- Run payroll for **future months** (UI blocks generate; use current or past only)
- Unlock a **locked** cycle unless correcting a verified error
- Commit `.env` or expose bank export files publicly

---

## 15. Useful artisan commands (production)

```bash
php artisan hrms:reset-permissions          # sync tenant roles after permission changes
php artisan hrms:finance-migrate-permissions  # finance role split on existing tenants
php artisan hrms:finance-migrate-permissions  # after finance role split deploy (alias: hrms:reset-tenant-permissions)
php artisan employees:deactivate-past-lwd --dry-run
php artisan leaves:sync-balances
php artisan config:clear && php artisan cache:clear   # troubleshooting only
```

---

## 16. Rollback

```bash
git checkout <previous-tag-or-commit>
composer install --no-dev --optimize-autoloader --no-interaction
php artisan migrate --force    # only if down migrations exist; else restore DB backup
php artisan config:cache
php artisan route:cache
php artisan view:cache
```

If migration cannot be reversed, **restore database from backup** taken before deploy.

---

## Quick reference — one-page deploy

```bash
cd /var/www/hrms-saas-laravel
git pull
composer install --no-dev --optimize-autoloader --no-interaction
php artisan migrate --force
php artisan config:cache && php artisan route:cache && php artisan view:cache
sudo chown -R deploy:www-data storage bootstrap/cache
sudo chmod -R ug+rwx storage bootstrap/cache
```

---

## 17. Troubleshooting

### `light-mode.css` / exception renderer dist missing (HTTP 500 after login)

**What you see:** `file_get_contents(.../exceptions/renderer/dist/light-mode.css): Failed to open stream`

**What it means:** Login often **succeeds**, but the **next page throws an error**. With `APP_DEBUG=true`, Laravel tries to show the pretty error page and **that page also fails** because `vendor/laravel/framework` is incomplete or mixed (new `Renderer.php` but old/missing `dist/` CSS files).

**Fix on server:**

```bash
cd /var/www/html/kiyohr/v2_version

# 1. Production must NOT use debug mode
# In .env:
#   APP_DEBUG=false
#   APP_ENV=production

# 2. Clean reinstall vendor (do not copy vendor from local)
rm -rf vendor
rm -f bootstrap/cache/packages.php bootstrap/cache/services.php bootstrap/cache/config.php

composer install --no-dev --optimize-autoloader --no-interaction

# 3. Verify exception renderer assets exist
ls -la vendor/laravel/framework/src/Illuminate/Foundation/resources/exceptions/renderer/dist/
# Expect: styles.css and scripts.js (and on some versions light-mode.css, dark-mode.css)

# 4. Clear all caches
php artisan optimize:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
```

**Find the real error** (the one that happens *before* the CSS error):

```bash
tail -100 storage/logs/laravel-$(date +%Y-%m-%d).log
```

Common post-login causes: missing DB column (run `php artisan migrate --force`), bad permissions on `storage/`, empty `company_id`, stale view cache.

### `Class "NunoMaduro\Collision\Adapters\Laravel\CollisionServiceProvider" not found`

**Cause:** `nunomaduro/collision` is a **dev** dependency (`require-dev`). Production uses `composer install --no-dev`, so Collision is not installed — but a stale `bootstrap/cache/packages.php` (built on a dev machine or with dev packages) still references it.

**Fix on server:**

```bash
cd /var/www/html/kiyohr/v2_version

rm -f bootstrap/cache/packages.php bootstrap/cache/services.php
composer install --no-dev --optimize-autoloader --no-interaction
php artisan package:discover --ansi
php artisan config:clear
php artisan config:cache
```

Never copy `bootstrap/cache/*.php` from local/dev to production.

### `Log [] is not defined`

**Cause:** `LOG_CHANNEL` is empty in `.env`, or a stale config cache was built with an empty value.

**Fix on server:**

```bash
# In .env — must not be blank:
LOG_CHANNEL=daily
LOG_LEVEL=warning

php artisan config:clear
php artisan config:cache
```

### `Class "Pdo\Mysql" not found`

**Cause:** Laravel 12 loads framework database config that references `Pdo\Mysql` (PHP 8.4+). On PHP 8.2/8.3 this relies on `symfony/polyfill-php84`. Missing vendor packages or incomplete `composer install` causes the error.

**Fix on server:**

```bash
composer install --no-dev --optimize-autoloader --no-interaction
php artisan config:clear
```

Ensure PHP extensions:

```bash
php -m | grep -E 'pdo|mysql'
# Required: pdo, pdo_mysql
```

This repo loads `bootstrap/polyfills.php` from `public/index.php` and `artisan` as a safety net. Deploy the latest code if the file is missing on production.

**Optional:** upgrade server PHP to **8.4+** for native `Pdo\Mysql` support.

### `Unable to create configured logger` (emergency logger)

Usually the same as empty `LOG_CHANNEL`. Fix logging env vars and clear config cache first; the underlying exception (often database/PDO) appears in the next log line.

### After any `.env` change

```bash
php artisan config:clear
php artisan config:cache
php artisan cache:clear
```

### Verify PHP and extensions

```bash
php -v
php -m
php artisan about
composer audit
```
