docs: add runbook for renaming an office's subdomain (local-summit -> local-centrestreetsmiles example)

Covers the scoped sudoers grant, updating the three per-office config files, reissuing the LAN cert, deploying nginx, repointing the Cloudflare Tunnel, the manual LAN DNS step, updating Twilio/Telnyx webhooks, and keeping per-office config out of git with skip-worktree.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 23:05:03 -04:00
co-authored by Claude Sonnet 5
parent 6148cfa848
commit 75658aea9c
+102 -14
View File
@@ -666,20 +666,20 @@ In `apps/Backend`, add the subdomain URL to the allowed CORS origins so login an
--- ---
### Example — Adding Summit Dental Care ### Example — Adding Centre Street Smiles
**Office subdomain:** `summitdentalcare.mydentalofficemanagement.com` **Office subdomain:** `centrestreetsmiles.mydentalofficemanagement.com`
**Step 1:** Already done (domain is on Cloudflare). **Step 1:** Already done (domain is on Cloudflare).
**Step 2:** Install `cloudflared` on Summit Dental's PC. **Step 2:** Install `cloudflared` on Centre Street Smiles' PC.
**Step 3:** Run `cloudflared tunnel login` on Summit Dental's PC. **Step 3:** Run `cloudflared tunnel login` on Centre Street Smiles' PC.
**Step 4:** **Step 4:**
```bash ```bash
cloudflared tunnel create summit-dental-app cloudflared tunnel create centrestreetsmiles-app
# Example output: Created tunnel summit-dental-app with id a1b2c3d4-... # Example output: Created tunnel centrestreetsmiles-app with id a1b2c3d4-...
``` ```
**Step 5 — `/etc/cloudflared/config.yml`:** **Step 5 — `/etc/cloudflared/config.yml`:**
@@ -688,14 +688,14 @@ tunnel: a1b2c3d4-xxxx-xxxx-xxxx-xxxxxxxxxxxx
credentials-file: /home/ee/.cloudflared/a1b2c3d4-xxxx-xxxx-xxxx-xxxxxxxxxxxx.json credentials-file: /home/ee/.cloudflared/a1b2c3d4-xxxx-xxxx-xxxx-xxxxxxxxxxxx.json
ingress: ingress:
- hostname: summitdentalcare.mydentalofficemanagement.com - hostname: centrestreetsmiles.mydentalofficemanagement.com
service: http://localhost:3000 service: http://localhost:3000
- service: http_status:404 - service: http_status:404
``` ```
**Step 6:** **Step 6:**
```bash ```bash
cloudflared tunnel route dns summit-dental-app summitdentalcare.mydentalofficemanagement.com cloudflared tunnel route dns centrestreetsmiles-app centrestreetsmiles.mydentalofficemanagement.com
``` ```
**Step 7:** **Step 7:**
@@ -705,9 +705,9 @@ sudo systemctl enable cloudflared
sudo systemctl start cloudflared sudo systemctl start cloudflared
``` ```
**Step 8:** Add `summitdentalcare.mydentalofficemanagement.com` to `allowedHosts` in `vite.config.js`. **Step 8:** Add `centrestreetsmiles.mydentalofficemanagement.com` to `allowedHosts` in `vite.config.js`.
**Step 9:** Add `https://summitdentalcare.mydentalofficemanagement.com` to backend CORS allowed origins. **Step 9:** Add `https://centrestreetsmiles.mydentalofficemanagement.com` to backend CORS allowed origins.
--- ---
@@ -721,7 +721,7 @@ tunnel at nginx's port-80 block instead, which only forwards `/api/twilio/*` and
and returns `403` for everything else (see [nginx.conf](nginx.conf)) — the rest of the app stays and returns `403` for everything else (see [nginx.conf](nginx.conf)) — the rest of the app stays
unreachable from the internet even though the tunnel is live. unreachable from the internet even though the tunnel is live.
Example actually used for Summit Dental Care's Twilio/Telnyx integration — paste this into Example actually used for Centre Street Smiles' Twilio/Telnyx integration — paste this into
`/etc/cloudflared/config.yml` (`sudo nano /etc/cloudflared/config.yml`, this is a YAML file, not `/etc/cloudflared/config.yml` (`sudo nano /etc/cloudflared/config.yml`, this is a YAML file, not
the certbot `.ini` credentials file from the LAN HTTPS section above): the certbot `.ini` credentials file from the LAN HTTPS section above):
@@ -730,7 +730,7 @@ tunnel: fc423bdb-eaae-4af5-bd6d-961a60b1e624
credentials-file: /home/gg/.cloudflared/fc423bdb-eaae-4af5-bd6d-961a60b1e624.json credentials-file: /home/gg/.cloudflared/fc423bdb-eaae-4af5-bd6d-961a60b1e624.json
ingress: ingress:
- hostname: summit.mydentalofficemanagement.com - hostname: centrestreetsmiles.mydentalofficemanagement.com
service: http://localhost:80 service: http://localhost:80
- service: http_status:404 - service: http_status:404
``` ```
@@ -738,7 +738,7 @@ ingress:
Then route DNS and install the service same as Steps 6–7 above: Then route DNS and install the service same as Steps 6–7 above:
```bash ```bash
cloudflared tunnel route dns summit-dental-twilio summit.mydentalofficemanagement.com cloudflared tunnel route dns centrestreetsmiles-twilio centrestreetsmiles.mydentalofficemanagement.com
sudo cloudflared service install sudo cloudflared service install
sudo systemctl enable cloudflared sudo systemctl enable cloudflared
sudo systemctl start cloudflared sudo systemctl start cloudflared
@@ -757,7 +757,7 @@ Each office runs its own `cloudflared` tunnel on its own PC. Ports never conflic
| Office | Local access | Public access | Tunnel name | | Office | Local access | Public access | Tunnel name |
|---|---|---|---| |---|---|---|---|
| Community Dentists of Lowell | `http://192.168.1.236:3000` | `https://communitydentistsoflowell.mydentalofficemanagement.com` | `dental-app` | | Community Dentists of Lowell | `http://192.168.1.236:3000` | `https://communitydentistsoflowell.mydentalofficemanagement.com` | `dental-app` |
| Summit Dental Care | `http://<its-ip>:3000` | `https://summitdentalcare.mydentalofficemanagement.com` | `summit-dental-app` | | Centre Street Smiles | `http://<its-ip>:3000` | `https://centrestreetsmiles.mydentalofficemanagement.com` | `centrestreetsmiles-app` |
| Next office | `http://<its-ip>:3000` | `https://<subdomain>.mydentalofficemanagement.com` | `<office>-app` | | Next office | `http://<its-ip>:3000` | `https://<subdomain>.mydentalofficemanagement.com` | `<office>-app` |
--- ---
@@ -947,6 +947,94 @@ since it fixes every device on the network in one place instead of requiring a h
--- ---
## Renaming an Office's Subdomain (e.g. Moving to a New Office)
When a PC moves to a new office and needs a new subdomain (e.g. `local-summit` →
`local-<newoffice>`), both the LAN HTTPS cert (above) and the Cloudflare Tunnel (above) need to be
re-pointed. This repo's `nginx.conf`, `apps/Backend/.env`, and `apps/Frontend/vite.config.ts` are
**per-office files** — see the note at the end of this section before committing/pushing anything.
### Step 1 — Grant narrow passwordless sudo for the migration commands
Several steps below need `sudo` (certbot, nginx, cloudflared). Since this is normally done
through an assistant/automation that can't type a password interactively, create a scoped
sudoers rule limited to exactly these commands (replace `<youruser>` with your actual username):
```bash
sudo tee /etc/sudoers.d/office-migration <<'RULE'
<youruser> ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx, /usr/bin/systemctl reload nginx, /usr/sbin/nginx -t, /usr/bin/systemctl restart cloudflared, /usr/bin/certbot certonly*, /usr/bin/tee /etc/cloudflared/config.yml, /usr/bin/tee /etc/nginx/sites-available/dental-app, /usr/bin/tee /etc/nginx/nginx.conf
RULE
sudo chmod 440 /etc/sudoers.d/office-migration
sudo visudo -c
```
Remove it when the migration is done: `sudo rm /etc/sudoers.d/office-migration`.
### Step 2 — Update the repo files
In `nginx.conf`, `apps/Backend/.env`, and `apps/Frontend/vite.config.ts`, replace every
`local-<oldoffice>` / `<oldoffice>` occurrence with `local-<newoffice>` / `<newoffice>`. Also
update the hardcoded LAN IP in `vite.config.ts`'s `allowedHosts` if it changed.
### Step 3 — Issue a new cert for the new LAN hostname
```bash
sudo certbot certonly --dns-cloudflare --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
--key-type ecdsa -d local-<newoffice>.mydentalofficemanagement.com --non-interactive --agree-tos
```
> If the new hostname is longer and `nginx -t` later fails with `could not build
> server_names_hash, you should increase server_names_hash_bucket_size: 64`, see the fix in
> [Step 6 above](#step-6--point-nginx-at-the-new-cert).
### Step 4 — Deploy the updated nginx config and reload
```bash
sudo cp nginx.conf /etc/nginx/sites-available/dental-app
sudo nginx -t && sudo systemctl reload nginx
```
### Step 5 — Point the Cloudflare Tunnel at the new public hostname
Edit `/etc/cloudflared/config.yml`'s `hostname:` line to the new public subdomain (keep the same
`tunnel:` ID and `credentials-file:` — it's the same tunnel, just a new hostname), then:
```bash
sudo systemctl restart cloudflared
cloudflared tunnel route dns <tunnel-id-or-name> <newoffice>.mydentalofficemanagement.com
```
### Step 6 — Add the new LAN DNS record
In the Cloudflare dashboard, add the `local-<newoffice>` → this PC's LAN IP record exactly as in
[Step 1 of LAN HTTPS Setup](#step-1--create-the-dns-record-in-cloudflare-not-the-registrar) above.
This has to be done manually in the dashboard — it needs the Cloudflare account login, not just
the scoped DNS-01 API token in `cloudflare.ini`.
### Step 7 — Update third-party webhook URLs
Twilio and Telnyx dashboards both have the old public hostname registered as a webhook URL —
update both to the new `<newoffice>.mydentalofficemanagement.com`, or calls/texts stop arriving.
### Note — don't push the renamed config files to Gitea if this repo is shared
If this Gitea repo is **this office's own dedicated repo** (not pulled by any other office's
PC), committing and pushing the renamed `nginx.conf` / `.env` / `vite.config.ts` is safe — it only
affects this office. But if another PC with a different office's hostname (e.g. `local-broadway`)
ever clones or pulls from this *same* repo, pushing would either create a merge conflict or
silently overwrite that PC's config on its next pull. When in doubt, keep these three files out of
git entirely with:
```bash
git update-index --skip-worktree nginx.conf apps/Backend/.env apps/Frontend/vite.config.ts
```
This makes git treat them as permanently unchanged — the new subdomain keeps working locally, but
the files never show up in `git status`/`diff` and can't be committed or pushed, even by accident
via a future `git add -A`. To undo: `git update-index --no-skip-worktree <file>`.
---
## Payment OCR Service Setup (Google Cloud Vision) ## Payment OCR Service Setup (Google Cloud Vision)
The Payment OCR Service (`apps/PaymentOCRService`, port 5003) uses Google Cloud Vision to read The Payment OCR Service (`apps/PaymentOCRService`, port 5003) uses Google Cloud Vision to read