# Deploying QoraTec to cPanel

Three Node applications on three subdomains:

| Archive | Subdomain | What it is |
|---|---|---|
| `qoratec-api-*.zip` | `api.qoratec.com` | The API. **The only process with a database connection.** Also serves uploaded media from `/uploads`. |
| `qoratec-web-*.zip` | `qoratec.com` | The public website. Server-rendered — it is not static HTML. |
| `qoratec-admin-*.zip` | `admin.qoratec.com` | The admin portal. |

**Deploy in that order.** The website reads its content from the API at startup, so
an API that is not yet running gives you a website with nothing on it.

The public URLs are compiled into the two website bundles. These archives were built
for `qoratec.com` and its `api.` / `admin.` subdomains. Moving to a different domain
means a rebuild, not an environment change — see [Rebuilding](#rebuilding-for-a-different-domain).

---

## 0. Before you upload

In cPanel:

1. **Domains → Create A Domain** — add `api.qoratec.com` and `admin.qoratec.com`.
   Untick "Share document root" so each gets its own directory.
2. **MySQL® Databases** — create a database and a user, and add the user to the
   database with **All Privileges**. Note all three values; cPanel prefixes them with
   your account name, so the real names look like `qoratec_platform` and
   `qoratec_apiuser`.
3. **SSL/TLS Status** — run AutoSSL over all three hostnames. The refresh cookie is
   issued `Secure`, so **the admin portal cannot log in over plain HTTP.**
4. Confirm **Setup Node.js App** exists in the Software section and offers Node 20
   or newer.

---

## 1. The API — `api.qoratec.com`

### Create the application

**Setup Node.js App → Create Application**

| Field | Value |
|---|---|
| Node.js version | 20.x or newer |
| Application mode | Production |
| Application root | `qoratec-api` |
| Application URL | `api.qoratec.com` |
| Application startup file | `app.js` |

Click **Create**, then **Stop** it — it has nothing to run yet.

### Upload

**File Manager** → open `/home/<you>/qoratec-api` → **Upload** `qoratec-api-*.zip` →
right-click it → **Extract** → delete the zip.

`app.js` must sit at the top level of that folder, not inside a nested one.

### Configure

Copy `.env.example` to `.env` and edit it. The API validates every key at startup and
refuses to boot with a readable list of what is wrong, so a typo here is a clear error
rather than a mystery later.

The values that must change:

```ini
NODE_ENV=production
DATABASE_URL="mysql://qoratec_apiuser:PASSWORD@localhost:3306/qoratec_platform"

# 48 random bytes each, all four different. Generate with:
#   node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))"
JWT_ACCESS_SECRET=
JWT_REFRESH_SECRET=
JWT_CUSTOMER_ACCESS_SECRET=
JWT_CUSTOMER_REFRESH_SECRET=

REVALIDATE_SECRET=      # generate one; the website needs the SAME value
BUILD_TOKEN=            # generate one; the website needs the SAME value

SEED_SUPERADMIN_EMAIL=you@qoratec.com
SEED_SUPERADMIN_PASSWORD=   # you will change this at first login
```

> **Percent-encode reserved characters in the database password.**
> `@` → `%40`, `#` → `%23`, `/` → `%2F`, `:` → `%3A`. A raw `@` splits the
> connection string in the wrong place and the error points at the host, not the
> password.

### Install and initialise

**Setup Node.js App → your app → Run NPM Install.** Then open **Terminal** and run the
activation command shown at the top of the application's page — it looks like:

```bash
source /home/<you>/nodevenv/qoratec-api/20/bin/activate && cd /home/<you>/qoratec-api
```

Then, once:

```bash
npx prisma generate     # build the client for this server's platform
npm run db:deploy       # create the tables
npm run db:seed         # brand, palette, content, permissions, super admin
```

The seed is idempotent and safe to re-run. It also imports the logo and image files
into `uploads/`, which is why it runs on the server rather than being packaged.

### Start

**Restart** the application, then check:

```
https://api.qoratec.com/api/v1/health
```

You should get `{"success":true,...}`. The API reference is at `/api/v1/docs`.

---

## 2. The website — `qoratec.com`

**Create Application** with root `qoratec-web`, URL `qoratec.com`, startup file
`app.js`.

Upload and extract `qoratec-web-*.zip` the same way.

**Do not run NPM Install.** This build ships its own `node_modules` — only the
packages the app actually reached. Running an install would try to resolve a
monorepo that is not there.

Set these in **Setup Node.js App → Environment variables** (more reliable than a
`.env` file for a Next.js standalone server):

| Name | Value |
|---|---|
| `NODE_ENV` | `production` |
| `NEXT_PUBLIC_SITE_URL` | `https://qoratec.com` |
| `NEXT_PUBLIC_API_URL` | `https://api.qoratec.com` |
| `API_INTERNAL_URL` | `https://api.qoratec.com` |
| `REVALIDATE_SECRET` | **the same value as in the API `.env`** |
| `BUILD_TOKEN` | **the same value as in the API `.env`** |

Restart, then open `https://qoratec.com`. You should see the QoraTec logo, the
violet→cyan palette and the seeded content.

If the site loads but has no styling, `.next/static` did not extract — re-extract
the archive.

---

## 3. The admin portal — `admin.qoratec.com`

**Create Application** with root `qoratec-admin`, URL `admin.qoratec.com`, startup
file `app.js`. Upload, extract, **no NPM Install**.

Environment variables:

| Name | Value |
|---|---|
| `NODE_ENV` | `production` |
| `NEXT_PUBLIC_API_URL` | `https://api.qoratec.com` |
| `NEXT_PUBLIC_SITE_URL` | `https://qoratec.com` |

Restart and sign in at `https://admin.qoratec.com` with the seeded super admin.

**Change that password immediately** — Settings → My account.

---

## 4. After the first successful login

| # | Do this | Why |
|---|---|---|
| 1 | Change the super admin password, then clear `SEED_SUPERADMIN_PASSWORD` from the API `.env` | It is a known value sitting in a file. |
| 2 | `npm run db:purge-demo` | Six clients, eight testimonials, six articles, four job posts and a worked lead pipeline are **invented**. They exist so a fresh install does not look broken. Publishing them as real is a false claim. |
| 3 | Review the five products | A product page is a commercial claim and needs a person to own it. |
| 4 | Settings → System → turn search indexing **on** | It ships off. A half-configured site being indexed is far harder to undo than one that never was. |
| 5 | Set the SMTP details in the API `.env` | Until then, notification and acknowledgement emails are written to the log instead of sent, and nobody is told a lead arrived. |
| 6 | Check `Settings → Company` | Emails, phone numbers and addresses still hold placeholder values. |

---

## Backups

Two things carry state. Everything else is in the archives.

```bash
# The database
mysqldump -u qoratec_apiuser -p qoratec_platform | gzip > backup-$(date +%F).sql.gz

# Uploaded media
tar czf uploads-$(date +%F).tar.gz -C /home/<you>/qoratec-api uploads
```

cPanel's own scheduled backups cover both if the account backup is enabled.

---

## Updating later

`uploads/` and `.env` are **not** in the archives, so extracting a new build over the
application root leaves both intact. The safe sequence:

1. Back up the database and `uploads/`.
2. **Stop** the application.
3. Extract the new archive over the application root.
4. API only: `npm install --omit=dev` if dependencies changed, then `npm run db:deploy`.
5. **Start** the application.

Update the API before the website, for the same reason as the first deploy.

---

## Rebuilding for a different domain

`NEXT_PUBLIC_*` values are compiled into the JavaScript bundles at build time, and
the website's image host allow-list is derived from the API origin. Changing them on
the server does nothing to what the browser already downloaded — the site renders,
and every request goes to the old host.

So a domain change is a rebuild:

```bash
node scripts/release.mjs --domain newdomain.com
```

Or set each URL explicitly:

```bash
node scripts/release.mjs \
  --site-url https://newdomain.com \
  --api-url https://api.newdomain.com \
  --admin-url https://admin.newdomain.com
```

`--only api,web` limits which archives are rebuilt.

---

## When something is wrong

**The website shows "Application Error" or a 503.** Read
`/home/<you>/qoratec-web/stderr.log`. Nearly always a missing environment variable or
a Node version below 20.

**The admin signs in and immediately signs out.** The refresh cookie is not coming
back. Check that `admin.qoratec.com` is on HTTPS, that `COOKIE_SECURE=true` and
`COOKIE_DOMAIN=api.qoratec.com` in the API `.env`, and that `CORS_ORIGINS` contains
`https://admin.qoratec.com` exactly — scheme included, no trailing slash.

**Images do not load.** They are served by the API from `/uploads`. Confirm
`https://api.qoratec.com/uploads/` responds and that the seed ran on the server —
the image files are imported by the seed, not shipped in the archive.

**Publishing in the admin does not change the live site.** `REVALIDATE_SECRET` does
not match between the API `.env` and the website's environment. Pages still refresh
on their own within five minutes.

**`P1001: Can't reach database server`.** On cPanel the host is `localhost`, not
`127.0.0.1`, and the database and user names carry your account prefix.
