# Deployment — Said Yaqub Sadat Personal Portfolio

## 1. Requirements

| Component | Version used | Notes |
|---|---|---|
| PHP | 8.3 | with `pdo_mysql`, `mbstring`, `openssl`, `tokenizer`, `xml`, `ctype`, `json`, `fileinfo` |
| Composer | 2.x | |
| Node.js | 20 LTS or newer | for the Vite build (Vite 8 requires Node 20.19+/22.12+) |
| MySQL | 8.0 | `utf8mb4` |
| Web server | Nginx or Apache | document root must point at `backend/public` |

## 2. Backend

```bash
cd backend
composer install --no-dev --optimize-autoloader
cp .env.example .env          # then edit it, see below
php artisan key:generate
```

### Environment variables

| Variable | Required | Description |
|---|---|---|
| `APP_NAME` | yes | Application name |
| `APP_ENV` | yes | `production` on the live server |
| `APP_KEY` | yes | `php artisan key:generate` |
| `APP_DEBUG` | yes | **must be `false` in production** |
| `APP_URL` | yes | Public API origin, e.g. `https://api.example.com` |
| `FRONTEND_URL` | yes | Origin of the SPA; also the root redirect target and the CORS allow-list (comma separated) |
| `DB_CONNECTION` | yes | `mysql` |
| `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD` | yes | Database credentials — use a dedicated user, not `root` |
| `ADMIN_EMAIL`, `ADMIN_PASSWORD` | for seeding only | Credentials for the administrator account; leave `ADMIN_PASSWORD` empty to skip account creation |
| `MAIL_*` | optional | SMTP settings for contact notifications. With `MAIL_MAILER=log` the message is stored and no email is sent |
| `SESSION_DRIVER`, `CACHE_STORE`, `QUEUE_CONNECTION` | recommended | `database` or `redis` |
| `FILESYSTEM_DISK` | optional | `public` for media served through `/storage` |

Never copy `.env` from development to production, and never commit `.env`
(it is already listed in `backend/.gitignore`).

### Database and content

```bash
php artisan migrate --force
php artisan db:seed --force      # requires ADMIN_EMAIL and ADMIN_PASSWORD
php artisan storage:link          # publishes storage/app/public as public/storage
php artisan optimize              # config, route and view caches
```

`storage:link` may report "link already exists" on Windows — that is harmless when the
junction is already in place.

### Permissions

```bash
chown -R www-data:www-data backend/storage backend/bootstrap/cache
chmod -R 775 backend/storage backend/bootstrap/cache
```

### Web server

Nginx:

```nginx
server {
    listen 443 ssl http2;
    server_name api.example.com;
    root /var/www/portfolio/backend/public;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
        include fastcgi_params;
    }

    client_max_body_size 12M;   # must allow the CV upload (10 MB)
}
```

Apache: point the `DocumentRoot` at `backend/public` and allow `mod_rewrite` with the
standard Laravel `RewriteRule` block.

`GET /` redirects to `FRONTEND_URL`, so the API host can serve as a fallback entry
point for the whole site.

## 3. Frontend

```bash
cd frontend
npm ci
npm run build          # type-checks with tsc -b and writes dist/
```

`dist/` contains the static files. Host them on any static host (GitHub Pages,
Netlify, S3 + CloudFront, or the same web server under a separate vhost).

```bash
# Vite environment
VITE_API_URL=https://api.example.com/api
VITE_SITE_URL=https://example.com
```

* `VITE_API_URL` must be the absolute API base including `/api`. Everything that is
  not set falls back to `/api`, which only works behind a same-origin proxy.
* `VITE_SITE_URL` is used for canonical URLs and Open Graph metadata.
* These variables are **public** — they are baked into the JavaScript bundle. No
  secret ever belongs in a `VITE_*` variable.

If the SPA and the API share one origin, configure the web server so that `/api`
and `/storage` are proxied to Laravel and everything else is served from
`dist/`. The CV file itself is downloaded through `/api/cv`, while `/cv` stays with
the SPA page.

## 4. TLS and security headers

* Terminate TLS at the web server or a reverse proxy and redirect HTTP to HTTPS.
* `APP_DEBUG=false` — stack traces must never reach a visitor.
* Send `Strict-Transport-Security`, `X-Content-Type-Options: nosniff` and
  `Referrer-Policy: strict-origin-when-cross-origin`.
* Keep `storage/logs` outside the document root (it already is) and rotate logs.

## 5. Scheduled tasks

Add to the crontab of the web user:

```cron
* * * * * cd /var/www/portfolio/backend && php artisan schedule:run >> /dev/null 2>&1
```

The default Laravel schedule purges the cache and the queue. Backups should be
scheduled separately (see below).

## 6. Deployment checklist

1. `composer install --no-dev --optimize-autoloader`
2. `.env` configured with production values, `APP_DEBUG=false`
3. `php artisan key:generate`
4. `php artisan migrate --force`
5. `php artisan db:seed --force` (only on a fresh database, with strong credentials)
6. `php artisan storage:link`
7. `npm ci && npm run build` and upload `frontend/dist`
8. `php artisan optimize`
9. Copy the CV into the media folder — `cp Cv.pdf backend/storage/app/public/cv/Said_Yaqub_Sadat_CV.pdf`
   (the folder is git-ignored, so the file is not part of the repository) — or upload
   it from **Settings → Current CV (PDF)** after the first sign-in.
10. Upload the profile photo in **Settings → Profile photo** (optional; until then the
    branded placeholder from `frontend/public/profile-placeholder.png` is shown).
11. Verify: public pages, project detail, contact submission, image upload, CV
    download, admin sign-in and one content edit.
12. Confirm the sitemap is reachable at `/sitemap.xml` and that `robots.txt` allows
    indexing.

## 7. Backup and restore

### What to back up

| Asset | Location | Why |
|---|---|---|
| Database | MySQL `portfolio_db` | all content |
| Media | `backend/storage/app/public` | images, certificate files, CV |
| Environment | `backend/.env` (stored in a password manager, not in git) | configuration |

### Schedule

```bash
# Daily database dump, keep 14 days
0 2 * * * mysqldump -u portfolio -p'***' --single-transaction --routines portfolio_db | gzip > /var/backups/portfolio_$(date +\%F).sql.gz
find /var/backups -name 'portfolio_*.sql.gz' -mtime +14 -delete

# Weekly media archive
0 3 * * 0 tar -czf /var/backups/media_$(date +\%F).tar.gz -C /var/www/portfolio/backend/storage/app/public .
```

Store the copies off the server and encrypt them.

### Restore drill (run at least once per quarter)

```bash
systemctl stop php8.3-fpm
mysql -u root -p -e "DROP DATABASE IF EXISTS portfolio_restore; CREATE DATABASE portfolio_restore CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
gunzip -c /var/backups/portfolio_2026-01-01.sql.gz | mysql -u root -p portfolio_restore
tar -xzf /var/backups/media_2026-01-01.tar.gz -C /var/www/portfolio/backend/storage/app/public
# point DB_DATABASE at portfolio_restore, start the app and verify the pages
systemctl start php8.3-fpm
```

### Rollback

1. Put the previous release back in place (keep the previous `composer.lock`,
   `package-lock.json` and `dist/`).
2. `php artisan migrate:rollback --step=<n>` if the release added migrations, or
   restore the database dump.
3. `php artisan optimize:clear` and restart PHP-FPM.
4. Verify the public pages and the admin dashboard.

## 8. Troubleshooting

| Symptom | Cause / fix |
|---|---|
| `500` on every API call | `APP_KEY` missing or `.env` not readable — run `php artisan key:generate` |
| `419` or CORS errors in the browser | `FRONTEND_URL` does not match the real SPA origin; clear the config cache (`php artisan config:clear`) |
| Images return 404 | `php artisan storage:link` was not run, or `public/storage` is missing |
| Contact form stores the message but no email arrives | mail transport is `log` or SMTP is misconfigured; check `MAIL_*` and `storage/logs/laravel.log` |
| CV download returns 404 | no CV file is stored — upload one in **Settings → CV**, or copy `Cv.pdf` to `backend/storage/app/public/cv/` and set `cv_path` |
| Uploads fail with 413 | raise `client_max_body_size` in the web server configuration |
| Changes to settings do not appear | the settings cache is stale — run `php artisan cache:clear` |
