# speech.neurapy.ai — deployment and database reference

How the Speech app reaches production and where its data lives. No secrets here;
the real values are only in the server `.env`.

## Server

| Item | Value |
|---|---|
| Host | cPanel account `neurapyc` on `66.116.196.174` (`66-116-196-174.webhostbox.net`) |
| Runtime | PHP 8.2 (CLI and vhost), Composer at `/usr/local/bin/composer`, Node 22 at `/opt/cpanel/ea-nodejs22/bin` (the system `node` is 16 and must not be used for Vite) |
| Repo checkout | `~/repositories/vp_speech` — cPanel Git Version Control clone of `github.com/gmadan78/vp_speech`, branch `main` |
| Deployed app | `~/public_html/speech.neurapy.com` (rsync target, **not** a git checkout) |
| Document root | `~/public_html/speech.neurapy.com/public` |
| Deploy log | `~/deploy-speech.log` |
| WebGL uploads | `~/webgl/speech/Build/<version>/` and `~/webgl/speech/ServerData/<version>/ServerData/WebGL` (see `docs/webgl-integration.md`) |
| SSH alias (dev machine) | `ssh neurapy-host` |

Sibling Laravel apps on the same host follow the identical layout:
`admin`, `auth` (the IdP), `books`, `cvi`, `mcq`, `neuracrm`, `ot`, `perception`, `per`, `visuoprime`, `vp`, `vpo`.

## How a deploy happens

1. Push to `main` on GitHub.
2. Pull into the server clone. Two ways:
   - **Manual (current practice):** `ssh neurapy-host 'bash ~/repositories/deploymulti.sh speech'`, which runs
     [`scripts/deploy.sh`](../scripts/deploy.sh): `git pull origin main`, then `cpanel-deploy.sh`, then `webgl-deploy.sh`.
   - **cPanel UI:** Git Version Control → Pull or Deploy → "Update from Remote", then "Deploy HEAD Commit".
     `.cpanel.yml` runs the same `cpanel-deploy.sh`. The clone's `post-receive` hook queues a deployment
     if you push straight to the server clone (`receive.denyCurrentBranch=updateInstead`).
3. [`scripts/cpanel-deploy.sh`](../scripts/cpanel-deploy.sh) does, in order:
   - create `storage/*` and `bootstrap/cache` dirs
   - rsync repo → deploy path, excluding `.env`, `vendor/`, `storage/`, `bootstrap/cache/`, `node_modules/`, `.git/` (`--delete-delay`)
   - `composer install --no-dev --optimize-autoloader`
   - `npm ci && npm run build` with Node 22; falls back to a committed `public/build` if the server build fails
   - `php artisan migrate --force`
   - permissions (`775` on storage and bootstrap/cache)
   - `storage:link`, `config:cache`, `route:cache`, `view:cache`
   - `php artisan neurapy:publish-capabilities` (best effort; pushes the role→capability catalogue to the IdP)
4. `scripts/webgl-deploy.sh` re-points `public/<version>` and `public/ServerData/WebGL` symlinks at the version in `scripts/latestwebglversion.txt`.

Because the deploy path is an rsync copy, **never edit files in `public_html/speech.neurapy.com` by hand**; the next deploy overwrites them. `.env` is the one file that lives only there.

## Environment file

`.env.example` in the repo is the template. Production sets, beyond the Laravel defaults:

| Group | Keys | Production value or note |
|---|---|---|
| App | `APP_ENV`, `APP_DEBUG`, `APP_URL` | `production`, `false`, `https://speech.neurapy.ai` |
| Core DB | `DB_CONNECTION`, `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD` | `mysql`, `127.0.0.1`, `3306`, `neurapyc_speech_core`, `neurapyc_web` |
| Shared DB | `DB_CONNECTION_SECOND`, `DB_HOST_SECOND`, `DB_PORT_SECOND`, `DB_DATABASE_SECOND`, `DB_USERNAME_SECOND`, `DB_PASSWORD_SECOND`, `DB_SECOND_PREFIX` | `mysql2`, same host, `neurapyc_speech_shared01`, same user, empty prefix. **Required**; without them the tenant connection falls back to root and fails. |
| Drivers | `SESSION_DRIVER`, `CACHE_STORE`, `QUEUE_CONNECTION`, `FILESYSTEM_DISK` | all `database` except filesystem `local` |
| Session | `SESSION_COOKIE`, `SESSION_SAME_SITE`, `SESSION_SECURE_COOKIE` | `vpspeech_session`, `lax`, `true` (unique cookie name per app on the shared domain) |
| SSO | `NEURAPY_BASE_URL`, `NEURAPY_ISSUER`, `NEURAPY_APP_KEY`, `NEURAPY_HMAC_SECRET`, `NEURAPY_REDIRECT_URI` | `https://auth.neurapy.com`, app key registered on the IdP, callback `https://speech.neurapy.ai/auth/neurapy/callback` |
| Licensing | `NEURAPY_LICENSE_ENFORCE`, `NEURAPY_LICENSE_APP_KEY`, `NEURAPY_LICENSE_TTL` | `true`, product key on the IdP, `300` |
| IdP TTLs | `IDP_ISSUER`, `IDP_ACCESS_TTL`, `IDP_REFRESH_TTL`, `IDP_INVITE_TTL`, `IDP_OTP_TTL`, `IDP_MESSENGER`, `IDP_MAIL_FROM*` | copied from the auth app |
| Mail | `MAIL_*` | SMTP (currently a Mailtrap sandbox) |
| AI | `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `OPENAI_MODEL`, `OPENAI_MCQ_MODEL`, `OPENAI_RAW_TEXT_MODEL` | `gpt-4o` for the narrative report |
| Redis | `REDIS_*` | present but unused (drivers are `database`) |

After changing `.env` on the server run `php artisan config:cache` in the deploy path, or the cached config keeps the old values.

## Database connections

Defined in [`config/database.php`](../config/database.php):

| Connection | Reads from | Used for |
|---|---|---|
| `mysql` (default) | `DB_*` | core: users, organisations, ACL, org_settings, credits, games, sessions, cache, jobs |
| `mysql2` / `mysql_second` | `DB_*_SECOND` | base config for the shared clinical DB |
| `tenant` | `DB_*_SECOND` at boot, then rewritten per request | patient, assessments, notes, therapy plans, daily logs |
| `core` | set at runtime only | core DB assigned centrally by the IdP, when present |

Migrations targeting the clinical DB declare `protected $connection = 'mysql2'` (or `tenant`); everything else lands in core.

### How the tenant connection is chosen per request

Global web middleware runs `OrgScope` then `ResolveTenantDatabase`:

1. `OrgScope` works out the active organisation from `acl_org_user` (staff) or `sph_user_patient_org_map` (patients) and sets `active_org` on the request.
2. `ResolveTenantDatabase` looks for `session('neurapy.tenant.<orgId>')`. The SSO callback stores it at login from the IdP userinfo `organizations[].databases` payload (`{core: {...}, shared: {...}}`, full host/db/user/password). If present, `tenant` and `core` are re-pointed at those databases for this request.
3. Otherwise it consults `corp_db_mapper` → `corp_databases` for a per-org database name and clones the base connection with that name.
4. If neither exists (production today: both tables are empty) the default `tenant` connection, i.e. `neurapyc_speech_shared01`, is used for every organisation.

So Speech currently runs one core DB and one shared clinical DB for all organisations. Per-org databases can be introduced later without code changes, either from the IdP or via `corp_databases`.

### Current production state (2026-09-05)

- Core `neurapyc_speech_core`: 39 tables, 74 migrations applied. 3 organisations, 4 users.
- Shared `neurapyc_speech_shared01`: 11 tables. 1 patient, 1 speech assessment.
- Scheduler: `sph:slot-capacity-alerts` daily at 08:00, but **no crontab entry runs `schedule:run` for this app** and no queue worker runs. Add when queued work is introduced:

```
* * * * * cd /home/neurapyc/public_html/speech.neurapy.com && php artisan schedule:run >> /dev/null 2>&1
```

Use `php artisan db:show --counts` and `php artisan db:show --database=mysql2 --counts` on the server to inspect; the `mysql` CLI needs a password the shell user does not have.

## Replicating this for a new app or environment

1. Create the cPanel subdomain with document root `public_html/<host>/public`.
2. cPanel → Git Version Control → clone the GitHub repo to `~/repositories/<name>`; make sure `.cpanel.yml` points at the app's own `scripts/cpanel-deploy.sh` and that script's `REPOPATH`/`DEPLOYPATH`/`DEPLOY_LOG` are updated.
3. Create two MySQL databases (`<prefix>_<app>_core`, `<prefix>_<app>_shared01`) and grant the `neurapyc_web` user on both.
4. Copy `.env.example` to `public_html/<host>/.env`, fill the two DB groups, `APP_KEY` (`php artisan key:generate`), a unique `SESSION_COOKIE`, and the SSO keys from the IdP app registration (`idp:app-register` on auth.neurapy.com).
5. Register the callback URL on the IdP and assign roles with the app's prefix (`sph_` for Speech).
6. Run the deploy once from the shell (`bash scripts/deploy.sh`) and watch the log.
7. Add the app to `~/repositories/deploymulti.sh` if it should join the multi-app deploy.
