# Deploying to cPanel (site administrator guide) This guide assumes the standard cPanel layout `/home/USER/`. Replace `USER` with the account's username throughout. Nothing here changes DNS or publishes anything until step 8. > **Current state (2026-09-22):** `visit.waterschurch.org` already resolves to the same server as > waterschurch.org (108.160.152.27) and currently 301-redirects to a Church Center form > (`waterschurch.churchcenter.com/people/forms/21186`). That redirect lives either in the main > site's `.htaccess` or in a cPanel **Redirects** entry and must be removed when this site goes live. ## Fastest path: `deploy/deploy.sh` (SSH) If you have SSH access (alias `waterschurch` in `~/.ssh/config`), one command does steps 2, 5, 6 and 7 below: ```bash deploy/deploy.sh --keep-redirect # deploy, but keep visitors on the Church Center form (pre-launch) deploy/deploy.sh # build, deploy and go live (removes the redirect) deploy/deploy.sh --no-build # re-upload the existing ./out ``` It preserves cPanel's own `.htaccess` blocks and `.user.ini`, never overwrites an existing `~/visit-private/.env`, and prints the health check when done. The manual steps below describe what it does, for File Manager users or troubleshooting. **Status 2026-09-22 (evening): LIVE.** Deployed with `deploy/deploy.sh`; the Church Center redirect is gone. `INTEGRATION_MODE=live` with Mailchimp (audience "Waters Church", datacenter `us2`) and Sinch (all five campus sub-accounts verified). The 12 merge fields exist in the audience. A real email-only test lead succeeded end to end; the SMS path is verified against Sinch auth but a live text has not yet been sent. Journey "New Visit" (id 5842) is still a draft and triggers only on `Visit:Apollo Beach`. ## 0. What you will need * cPanel login for the waterschurch.org account * The build package: run `npm run build && npm run package` → `dist/visit-waterschurch--.zip` * Filled-in `.env` values (Mailchimp key, Sinch key/secret, etc.) — see `.env.example` ## 1. Check the account's PHP cPanel → **Software → MultiPHP Manager** (or *Select PHP Version*). * Set the domain (once created) to **PHP 8.1, 8.2 or 8.3**. * **MultiPHP INI Editor / Select PHP Version → Extensions**: ensure `curl`, `pdo_sqlite`, `sqlite3`, `mbstring`, `json`, `openssl` are enabled. (`pdo_mysql` only if you choose MySQL storage.) Node.js is **not** required. If the account has "Setup Node.js App", ignore it. ## 2. Create the private configuration folder (outside the web root) cPanel → **Files → File Manager** → navigate to `/home/USER/` (the home directory, *not* `public_html`). 1. Create folder **`visit-private`**. 2. Inside it create folders **`data`** and **`logs`**. 3. Upload `.env.example` from the package (`dist/env.example.txt`), rename it to **`.env`**, and edit it in File Manager: fill in real values, set `INTEGRATION_MODE=live` only when ready to go live, and set a long random `HASH_SALT`. 4. Permissions (right-click → *Change Permissions*): folders `0750`, the `.env` file `0640`. PHP runs as your cPanel user under suPHP/LSAPI, so it can read and write these while the web server cannot serve them. If your host runs PHP as a different user (rare), use `0755`/`0644` and rely on the folder being outside the document root. ## 3. Create the subdomain cPanel → **Domains → Domains** (or *Subdomains* on older versions) → **Create a New Domain**: * Domain: `visit.waterschurch.org` * Untick "Share document root"; set **Document Root** to `/home/USER/visit.waterschurch.org` * Submit. cPanel creates the folder and the DNS `A` record automatically because the zone is hosted here (`ns1/ns2.waterschurch.org`). The record already exists and points at this server, so nothing external changes. Then remove the old redirect: * cPanel → **Domains → Redirects**: delete any entry for `visit.waterschurch.org`, **or** * File Manager → `public_html/.htaccess`: remove the `RewriteCond %{HTTP_HOST} ^visit\.` block that redirects to churchcenter.com. Do this at the moment you are ready to launch (step 8); until then the redirect keeps working. ## 4. Enable SSL cPanel → **Security → SSL/TLS Status** → tick `visit.waterschurch.org` → **Run AutoSSL**. Wait for the green padlock (usually under 5 minutes). If AutoSSL fails with a DCV error, make sure the document root exists and nothing blocks `/.well-known/` (our `.htaccess` explicitly allows it). Verify: `https://visit.waterschurch.org/BUILD.txt` shows the build id once files are uploaded (step 5). Force-HTTPS is handled by the site's own `.htaccess`; you do **not** need cPanel's "Force HTTPS Redirect" toggle, but enabling it is harmless. ## 5. Upload the build 1. File Manager → open `/home/USER/visit.waterschurch.org/`. Delete the placeholder `cgi-bin` folder if present. 2. **Upload** the zip from `dist/`, then right-click → **Extract** into this folder. Delete the zip. 3. Enable **Settings → Show Hidden Files (dotfiles)** and confirm `.htaccess` is present at the root and inside `api/lib`, `api/cron`, `api/config`. 4. Tell the API where the private folder is. Either: * leave the default (`/home/USER/visit-private` — the folder *next to* the document root), **or** * edit `/home/USER/visit.waterschurch.org/.htaccess` and uncomment `SetEnv VISIT_PRIVATE_DIR /home/USER/visit-private` with the real path. 5. Permissions: folders `0755`, files `0644` (File Manager's defaults after extraction are fine). The upload never requires editing generated files. To update the site later, rebuild locally and repeat steps 5.2–5.3 (or use *File Manager → Upload* with "Overwrite existing files"). An alternative for frequent updates is cPanel's **Git Version Control** pointed at a repo whose `out/` is committed, or SFTP with `rsync --delete out/ USER@host:visit.waterschurch.org/`. ## 6. Verify the API Open `https://visit.waterschurch.org/api/health.php`. You should see `"ok": true` and: ``` "private_dir_found": true, "env_file_found": true, "data_dir_writable": true, "database": "ok (sqlite)", "campuses_loaded": 6, "https": true ``` `integration_mode` shows `mock` until you change it in `.env`. Fix anything `false` before continuing (most often: wrong `VISIT_PRIVATE_DIR` path or `data` folder not writable). Confirm private paths are blocked (each must return **403**): * `https://visit.waterschurch.org/api/lib/Config.php` * `https://visit.waterschurch.org/api/config/campuses.json` * `https://visit.waterschurch.org/api/cron/retry-integrations.php` ## 7. Cron job for retries cPanel → **Advanced → Cron Jobs** → add, every 5 minutes (`*/5 * * * *`): ``` VISIT_PRIVATE_DIR=/home/USER/visit-private /usr/local/bin/php /home/USER/visit.waterschurch.org/api/cron/retry-integrations.php >> /home/USER/visit-private/logs/cron.log 2>&1 ``` Check the PHP binary path with **MultiPHP Manager** (often `/usr/local/bin/php`, `/opt/cpanel/ea-php82/root/usr/bin/php`, or `/usr/local/bin/ea-php82`). The job re-sends any Mailchimp/Sinch delivery that failed transiently (up to 5 attempts with backoff) and, if `LEAD_RETENTION_DAYS` is set, deletes fully-processed leads older than that. Optional weekly export for staff (contains personal data — download and delete promptly): ``` VISIT_PRIVATE_DIR=/home/USER/visit-private /usr/local/bin/php /home/USER/visit.waterschurch.org/api/cron/export-leads.php --since=$(date -d '7 days ago' +%F) > /home/USER/visit-private/data/leads-week.csv ``` ## 8. Go live 1. Set `INTEGRATION_MODE=live` in `/home/USER/visit-private/.env`. 2. Remove the Church Center redirect (step 3). 3. Submit one real test lead with your own email and mobile: confirm the Mailchimp contact + tags and the confirmation text arrive. Reply STOP to test opt-out routing, then re-subscribe. 4. Run through [LAUNCH-CHECKLIST.md](LAUNCH-CHECKLIST.md). ## 9. Updating content later | Change | Where | Then | | --- | --- | --- | | Service times, address, phone, SMS number, campus status | `config/campuses.json` | `npm run build` → re-upload | | Logo | `assets/source/logos/` (same file names) | `npm run build` → re-upload | | Photos | `assets/source/photos/` (same file names) | `npm run build` → re-upload | | FAQs, kids info, headlines, consent text | `src/content/*.ts` (bump `consent.version` if consent text changes) | `npm run build` → re-upload | | Central contact number, privacy URL | `.env` (`NEXT_PUBLIC_*`) | `npm run build` → re-upload | | API keys, mode, providers | `/home/USER/visit-private/.env` | takes effect immediately, no rebuild | | SMS template | `SINCH_CONFIRMATION_TEMPLATE` in server `.env` | immediate | ## 10. Files, permissions and logs ``` /home/USER/ ├── visit.waterschurch.org/ document root (0755) — everything in out/ │ ├── .htaccess HTTPS + canonical redirects, caching, security headers, 404 │ ├── index.html, 404.html, _next/, images/ │ └── api/ PHP endpoints (public) + lib/ cron/ config/ (403) └── visit-private/ NOT web accessible (0750) ├── .env secrets (0640) ├── data/leads.sqlite lead store (created automatically) ├── data/retry.lock └── logs/app.log, cron.log JSON lines; personal data and secrets are redacted ``` Rotate `app.log` occasionally (cPanel cron: `find /home/USER/visit-private/logs -name '*.log' -size +20M -delete`) or simply download and clear it. ## 11. Optional: MySQL instead of SQLite cPanel → **Databases → MySQL Database Wizard**: create `USER_visit` + a user with ALL PRIVILEGES. In `.env`: `DATABASE_DSN=mysql:host=localhost;dbname=USER_visit;charset=utf8mb4`, `DATABASE_USER`, `DATABASE_PASSWORD`. Tables are created automatically on first request. ## 12. Content Security Policy `.htaccess` ships a **report-only** CSP covering GTM, GA4, Meta, TikTok and Turnstile. After confirming the tags you actually use, change `Content-Security-Policy-Report-Only` to `Content-Security-Policy` and remove hosts you do not need.