# DARA Intelligence API - Deployment (Namecheap cPanel + GitHub Actions)

## Architecture

```
Next.js static site (dara-intel-website repo)
https://daraintel.io
        │ HTTPS POST /api/v1/contact
        ▼
FastAPI (this repo), via passenger_wsgi.py under cPanel's Python App (Passenger)
https://api.daraintel.io
        │
        ▼
PostgreSQL, cPanel-managed database, same server
```

FastAPI is ASGI; cPanel's Python App feature hosts a WSGI callable. `passenger_wsgi.py` bridges
the two with `a2wsgi.ASGIMiddleware` - verified locally end-to-end (see commit history), fine
for a plain request/response API with no websockets or server-sent events.

## One-time setup

Do these once, in order - each step depends on the one before it.

### 1. Create the PostgreSQL database

cPanel → **PostgreSQL Databases**:
1. Create a database (cPanel will prefix it with your username, e.g. `cpaneluser_dara`)
2. Create a database user with a strong, generated password
3. Add that user to the database with **ALL PRIVILEGES**

Note the final database name, username and password - you'll need them for `DATABASE_URL`
below. cPanel PostgreSQL is normally reachable at `localhost` from the same account's apps.

### 2. Create the Python App

cPanel → **Setup Python App** → **Create Application**:
- **Python version**: the highest 3.11+ available
- **Application root**: a path *outside* `public_html`, e.g. `dara-api` (becomes
  `/home/<user>/dara-api`)
- **Application URL**: the `api` subdomain, e.g. `api.daraintel.io` (cPanel will offer to create
  the subdomain for you here)
- **Application startup file**: `passenger_wsgi.py`
- **Application Entry point**: `application`

After creation, cPanel shows an **"Enter to virtual environment"** command, e.g.:
```
source /home/<user>/virtualenv/dara-api/3.11/bin/activate && cd /home/<user>/dara-api
```
Copy the `source ...activate` part - that's your `API_VENV_ACTIVATE` secret (step 5).

### 3. Issue an SSL certificate for the API subdomain

cPanel → **SSL/TLS Status** (or **AutoSSL**) → run AutoSSL, make sure `api.daraintel.io` is
covered (it usually picks up new subdomains automatically; if not, add it explicitly).

### 4. Create the production `.env` file on the server

SSH into the server (see "Generating and authorizing a deploy key" below if you haven't already
set this up for the frontend repo - it's the same key, reused here) and create
`/home/<user>/dara-api/.env` **manually** - this file is gitignored and never touched by the
deploy pipeline, so secrets never pass through GitHub:

```bash
ENVIRONMENT=production
FRONTEND_URL=https://daraintel.io
DATABASE_URL=postgresql+asyncpg://<db_user>:<db_password>@localhost/<db_name>
CAPTCHA_SECRET=<your Cloudflare Turnstile secret key>
RATE_LIMIT=3/hour
```

`FRONTEND_URL` must exactly match the frontend's origin - it's used directly as the single
allowed CORS origin in `app/main.py`. Get `CAPTCHA_SECRET` from the Cloudflare dashboard
(Turnstile → your widget → Secret Key) - this is different from the **site key** used on the
frontend, and must never be exposed there.

### 5. Add GitHub Actions secrets

In the `dara-intel-api` repo → **Settings → Secrets and variables → Actions**:

| Secret | Value |
|---|---|
| `SSH_PRIVATE_KEY` | The same deploy key used by `dara-intel-website` (or a separate one - either works, just make sure its public half is authorized in cPanel → SSH Access) |
| `SSH_HOST` | Server hostname, from cPanel → SSH Access |
| `SSH_PORT` | Server SSH port, from cPanel → SSH Access (Namecheap shared hosting commonly uses a non-standard port such as `21098`, not `22`) |
| `SSH_USER` | Your cPanel username |
| `API_REMOTE_PATH` | The application root from step 2, e.g. `/home/<user>/dara-api` |
| `API_VENV_ACTIVATE` | The `source .../activate` command from step 2 |
| `API_HEALTH_URL` | `https://api.daraintel.io/healthz` - used by the pipeline to confirm the deploy actually came up |

### Generating and authorizing a deploy key

If you haven't set this up yet (e.g. you're doing the API before the frontend):

```bash
ssh-keygen -t ed25519 -f ~/.ssh/dara_deploy -C "github-actions-deploy" -N ""
```

cPanel → **SSH Access** → **Manage SSH Keys** → **Import Key** → paste the contents of
`dara_deploy.pub` → then click **Manage** on the imported key → **Authorize**.

Test it before adding the GitHub secret:
```bash
ssh -i ~/.ssh/dara_deploy -p <port> <username>@<host>
```

You should land at a shell prompt. This same key/secret value can be reused as-is for the
`dara-intel-website` repo too - no need to generate a second one.

### 6. Create the database tables

There's no migration tool wired up yet (see "Known gaps" below) - for the first deploy, create
the schema once by hand:

```bash
source /home/<user>/virtualenv/dara-api/3.11/bin/activate
cd /home/<user>/dara-api
python -c "
import asyncio
from sqlalchemy.ext.asyncio import create_async_engine
from app.models import Base
from app.config import get_settings

async def main():
    engine = create_async_engine(get_settings().database_url)
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    await engine.dispose()

asyncio.run(main())
"
```

## How the pipeline works

`.github/workflows/deploy.yml` runs on every push to `main`:
1. `rsync --delete` mirrors the repo into `API_REMOTE_PATH` over SSH (excluding `.env`, venvs,
   caches and `tmp/` so the running app and its config survive the sync)
2. SSHes in, activates the venv, `pip install -r requirements.txt`, and touches
   `tmp/restart.txt` - Passenger's standard "reload the app" signal
3. Curls `API_HEALTH_URL` to confirm the app actually came back up

## Verifying a deploy

```bash
curl -s https://api.daraintel.io/healthz
# → {"status":"ok"}

curl -s -X POST https://api.daraintel.io/api/v1/contact \
  -H "Content-Type: application/json" \
  -d '{"first_name":"Test","last_name":"User","email":"test@example.com","company_name":"Acme","country":"UK","job_title":"Tester","inquiry_type":"General Inquiry","message":"Deployment smoke test message.","privacy_policy_agreement":true,"captcha_token":"invalid"}'
# → 400 "Captcha verification failed." (proves validation + Turnstile call both work end-to-end;
#    a real token from the live contact form will succeed)
```

## Known gaps to close before relying on this in production

- **No migration tool.** Schema changes currently require manually re-running the
  `create_all` snippet above, which won't apply changes to existing tables. Worth adding Alembic
  before the schema needs to change.
- **`test_rate_limit_exceeded` is flaky** in the test suite - slowapi's default handler
  intercepts 429s before the app's own handler runs, and the limiter's in-memory state is
  shared across the whole test session. Doesn't block deployment, but worth fixing.
- **CORS only allows one origin.** If `www.daraintel.io` ever needs to work too,
  `app/main.py`'s `allow_origins=[settings.frontend_url]` needs to become a list.
