# Deploying the LeadPilot API to cPanel / XMart (Passenger)

Target: `https://leadpilot-api.easylife.com.pk`

| cPanel field | Value |
| --- | --- |
| Python version | 3.13.15 |
| Application root | `leadpilot-api.easylife.com.pk` |
| Application URL | `leadpilot-api.easylife.com.pk` |
| Application startup file | `passenger_wsgi.py` |
| Application entry point | `application` |

Nothing about the application's architecture, routing or business logic changed for this
deployment. Passenger speaks WSGI and FastAPI speaks ASGI, so `passenger_wsgi.py` wraps the existing
ASGI app with `a2wsgi.ASGIMiddleware` and exposes it as `application`.

---

## 1. Where `passenger_wsgi.py` must live

**`passenger_wsgi.py` sits in the application root — the same directory as `app/` and
`requirements.txt`, one level *above* `app/main.py`.**

```
~/leadpilot-api.easylife.com.pk/     <- cPanel "Application root"
├── passenger_wsgi.py                <- HERE (startup file, exposes `application`)
├── requirements.txt
├── requirements-prod.txt
├── app/
│   ├── main.py                      <- passenger_wsgi.py imports app.main:app
│   ├── config.py
│   ├── database.py
│   ├── models/
│   ├── schemas/
│   ├── routers/
│   ├── rules/
│   ├── services/
│   └── utils/
├── tests/                           (optional on the server)
├── seed.py                          (optional)
├── .env                             (create on the server)
├── leadpilot.db                     (created automatically on first request)
└── tmp/
    └── restart.txt                  (touch this to restart the app)
```

Relative paths, stated exactly:

- `passenger_wsgi.py` → `app/main.py` is **`./app/main.py`** (one directory down).
- `passenger_wsgi.py` → `requirements.txt` is **`./requirements.txt`** (same directory).
- From `app/main.py`, the startup file is **`../passenger_wsgi.py`**.

> Upload the **contents** of the local `backend/` folder into the application root — not the
> `backend` folder itself. There must be no `backend/` level on the server.

---

## 2. What `passenger_wsgi.py` handles

Running under Passenger differs from `uvicorn` locally in three ways, each handled explicitly:

1. **Import path** — the application root is added to `sys.path`, so `import app.main` resolves no
   matter how Passenger invokes the file.
2. **Working directory** — Passenger does not guarantee the process CWD, and both `.env` loading and
   the default relative SQLite path resolve against it. The CWD is pinned to the application root,
   and `LEADPILOT_DATABASE_URL` defaults to an absolute path inside it.
3. **Startup hook** — **WSGI has no lifespan protocol.** FastAPI's `lifespan` (which calls
   `init_db()`) never fires under Passenger, so the schema would never be created. `passenger_wsgi.py`
   calls `init_db()` itself. It is idempotent, so the local uvicorn path is unaffected.

---

## 3. Deployment commands

### 3.1 Create the application in cPanel

cPanel → **Setup Python App** → *Create Application*, using the table at the top of this file. Save.
cPanel creates the virtualenv and writes an `.htaccess` in the application root — **do not edit or
delete that `.htaccess`**.

### 3.2 Upload the code

From your machine (adjust `USER` and the host):

```bash
cd "d:/Easylife Linkedin app/leadpilot/backend"

# Upload the CONTENTS of backend/ into the application root.
rsync -av --delete \
  --exclude '.venv' --exclude '__pycache__' --exclude '.pytest_cache' \
  --exclude '*.db' --exclude '.env' \
  ./ USER@easylife.com.pk:~/leadpilot-api.easylife.com.pk/
```

No rsync? Zip it and use cPanel File Manager:

```bash
cd "d:/Easylife Linkedin app/leadpilot/backend"
zip -r leadpilot-api.zip . \
  -x ".venv/*" "__pycache__/*" "*/__pycache__/*" ".pytest_cache/*" "*.db" ".env"
```

Upload `leadpilot-api.zip` to `~/leadpilot-api.easylife.com.pk/` and **Extract** it there. Confirm
`passenger_wsgi.py` is directly inside the application root, not inside a nested `backend/`.

### 3.3 Install dependencies (SSH)

cPanel shows the exact "Enter to the virtual environment" command on the app's page. It looks like:

```bash
source /home/USER/virtualenv/leadpilot-api.easylife.com.pk/3.13/bin/activate \
  && cd /home/USER/leadpilot-api.easylife.com.pk
```

Then:

```bash
pip install --upgrade pip
pip install -r requirements-prod.txt
```

Use `requirements-prod.txt` on the server. It is `requirements.txt` minus `uvicorn[standard]`,
`pytest` and `httpx`: Passenger never uses uvicorn, and its `[standard]` extras (uvloop, httptools,
watchfiles) need a compiler that shared hosting often lacks. If your host does have build tools,
`pip install -r requirements.txt` also works.

No SSH? Put `requirements-prod.txt` in the cPanel app's **Configuration files** field and press
**Run Pip Install**.

### 3.4 Configure the environment

Create `~/leadpilot-api.easylife.com.pk/.env`:

```ini
LEADPILOT_APP_NAME=LeadPilot
LEADPILOT_DATABASE_URL=sqlite:////home/USER/leadpilot-api.easylife.com.pk/leadpilot.db
LEADPILOT_CORS_ORIGINS=https://leadpilot.easylife.com.pk
LEADPILOT_FOLLOWUP_DAYS=3,7,14
```

Note the **four** slashes in the SQLite URL — `sqlite:///` plus an absolute path beginning with `/`.
If you omit `LEADPILOT_DATABASE_URL` entirely, `passenger_wsgi.py` derives the same absolute path
automatically.

`LEADPILOT_CORS_ORIGINS` must list the exact origin(s) serving the dashboard, comma separated.
Chrome-extension origins are already allowed by a regex in `app/config.py`.

Variables can also be set in cPanel's **Environment variables** panel instead of `.env`.

### 3.5 Seed and start

Optional demo data:

```bash
python seed.py
```

Restart the app after any code or dependency change:

```bash
mkdir -p tmp && touch tmp/restart.txt
```

(or press **Restart** on the cPanel application page).

### 3.6 Verify

```bash
curl -s https://leadpilot-api.easylife.com.pk/api/health
# {"status":"ok","app":"LeadPilot","version":"1.0.0","usesAi":false,...}

curl -s https://leadpilot-api.easylife.com.pk/api/campaigns
curl -s https://leadpilot-api.easylife.com.pk/api/rules/metadata
```

Interactive docs: <https://leadpilot-api.easylife.com.pk/docs>

---

## 4. Point the clients at the deployed API

**Dashboard** — `dashboard/.env`, then rebuild:

```ini
VITE_API_BASE_URL=https://leadpilot-api.easylife.com.pk
```

**Chrome extension** — the popup's Settings page has a *Backend URL* field; set it to
`https://leadpilot-api.easylife.com.pk`. Also add the host to
`extension/public/manifest.json` → `host_permissions` and rebuild, otherwise Chrome blocks the
request:

```json
"host_permissions": [
  "http://127.0.0.1:8000/*",
  "http://localhost:8000/*",
  "https://leadpilot-api.easylife.com.pk/*"
]
```

---

## 5. Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| 500, log says `ModuleNotFoundError: app` | `passenger_wsgi.py` is not in the application root, or the code is nested in `backend/` | Move files so `passenger_wsgi.py` and `app/` are siblings in the app root |
| 500, `ImportError: a2wsgi` | Dependencies installed into the wrong interpreter | Activate the cPanel virtualenv first, then `pip install -r requirements-prod.txt` |
| `no such table: campaigns` | Startup never ran | `passenger_wsgi.py` calls `init_db()`; confirm you did not replace it with a bare `from app.main import app` |
| `unable to open database file` / read-only | SQLite path points outside the app root, or bad permissions | Set the absolute `LEADPILOT_DATABASE_URL`; `chmod 755` the app root and `644` the `.db` |
| Dashboard requests blocked by CORS | Origin not allowed | Add it to `LEADPILOT_CORS_ORIGINS`, then restart |
| Code changes have no effect | Passenger caches the loaded app | `touch tmp/restart.txt` |
| `uvloop`/`httptools` build failure during pip install | `uvicorn[standard]` needs a compiler | Install `requirements-prod.txt` instead |

Passenger logs errors to `~/leadpilot-api.easylife.com.pk/stderr.log` and to cPanel → *Errors*.

---

## 6. Notes

- SQLite is fine for a single Passenger app, but Passenger may run several worker processes against
  one file. If you see `database is locked` under load, move to PostgreSQL/MySQL by changing
  `LEADPILOT_DATABASE_URL` only — no code changes.
- There is no authentication in front of this API. Anyone who can reach the URL can read and write
  the prospect database. Put it behind HTTP auth, an IP allowlist, or add authentication before
  storing real data.
- Nothing in this deployment adds LinkedIn automation, scraping, CAPTCHA handling, stealth
  behaviour, cookie/token access, or any AI/LLM dependency.
