# Book AI — Security Protocol (every tenant)

Security-first is the platform default. Every new HQ/tenant gets the same walls, kill switch, backups, and launch gate. No tenant shares a Twilio number pool or another tenant’s data.

## Enforced by the platform (automatic)

| Control | How |
|---|---|
| Per-tenant data walls | Every D1 query is parameterized and filtered by `tenant_id`. Isolation tests prove tenant A cannot read/write B. |
| Per-tenant Twilio subaccount | `tenant_settings.twilio_subaccount_sid` + per-tenant auth material in `tenant_secrets` (or env `TWILIO_SUB_*`). Never assign the same number to two tenants. |
| Per-tenant kill switch | `tenant_settings.sending_enabled` defaults to **0 (OFF)**. Engine checks it before every outbound send. HQ Settings shows a big ON/OFF control that takes effect instantly. |
| Per-tenant failed-send alerts | Outbound `failed` / `uncertain` creates a `tenant_alerts` row and a visible HQ banner. Never auto-retry an uncertain send. |
| Per-tenant nightly backups | Cron exports each non-demo tenant’s state to R2 when available, otherwise `backup_snapshots` in D1. Settings → “Back up now” is manual. |
| Demo isolation | Demo shops stay fictional (555 numbers) and cannot steal a live tenant’s number. |

## Onboarding checklist (repeat for every artist)

1. Create tenant (trialing). Confirm `sending_enabled = 0`.
2. Create a **dedicated Twilio subaccount** for that artist. Store SID on the tenant. Put auth token / API key via `wrangler secret put` or encrypted `tenant_secrets` — never commit plaintext.
3. Buy/assign **one** number to that subaccount only. PUT `/api/tenants/:id/number` (platform rejects collisions).
4. Point Messaging webhook:
   - Inbound: `POST https://<worker>/sms/inbound`
   - Status callback: `POST https://<worker>/sms/status`
5. Attach privacy + terms URLs from the Book AI site.
6. Submit A2P 10DLC brand + campaign for that subaccount. Wait for approval before any live client texts.
7. Seed artist-lane cell (owner’s personal number) for quote approvals. Quotes never auto-send.
8. Run the **Launch gate** below. Only then may the owner flip Sending ON.

## Launch gate: tool account verified

Mark each item pass in Settings → Launch readiness (or via `/api/tenants/:id/gates`).

1. **Twilio campaign approved** — A2P campaign status is Approved for this tenant’s brand.
2. **Creds set** — Subaccount SID + auth token (or API key) present; connection status shows connected.
3. **Webhook pointed** — Inbound + status callback URLs set on the number / messaging service.
4. **Inbound test lands in HQ** — A signed test inbound from a non-client number appears in the Inbox client lane.
5. **AI draft in voice** — Engine produces a reply in the artist’s voice (or template fallback) without inventing prices.
6. **Kill switch blocks** — With Sending OFF, an otherwise-sendable reply is queued as draft and **not** handed to Twilio.
7. **Owner-approved outbound test** — Owner flips Sending ON briefly, sends one test to their own phone, status callback records queued → sent → delivered. Then flip OFF again until go-live.
8. **Forced failure raises alert** — Simulated/forced failed send creates an HQ alert + banner.
9. **Backup restore tested** — Manual backup created; restore into a scratch tenant or verified payload round-trip.
10. **Stripe test deposit marked paid** — Test-mode Checkout (or simulated deposit) marks the appointment paid via webhook.

## Secrets (never commit, never print)

Shared/platform (wrangler secrets):

- `DEV_AUTH_SECRET`, `OWNER_PASSCODE_HASH`, `OWNER_PASSCODE_SALT`, `OWNER_CREDS_KEY` (AES key for `tenant_secrets`)
- Optional AI: `OPENAI_API_KEY` or `ANTHROPIC_API_KEY` or `XAI_API_KEY`
- Optional Stripe: `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `STRIPE_PRICE_ID` (billing) / deposit uses Checkout

Per-tenant Twilio (preferred): store under that tenant’s subaccount; platform fallback env names if a single-tenant deploy is used:

- `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`, `TWILIO_FROM` or `TWILIO_MESSAGING_SERVICE_SID`

## Go-live rule

Texts stay off until the owner completes the launch gate and explicitly turns **Sending: ON**. The two lanes (client vs artist) must never mix.

## SendBlue (second provider)
- Inbound: `POST https://<worker>/sendblue/inbound` (JSON). When `SENDBLUE_WEBHOOK_SECRET` is set, the `sb-signing-secret` header must equal it (constant-time compare) or the request gets 401. Unset = accepted with a warning log.
- Outbound replies are SIMULATED unless `SENDBLUE_SEND_ENABLED=true` AND `SENDBLUE_API_KEY_ID`/`SENDBLUE_API_SECRET` are set AND the per-artist kill switch is ON. Twilio is never used to answer a SendBlue inbound.
- Failed / uncertain SendBlue sends create a `tenant_alerts` row (HQ banner). Uncertain sends are never retried. Duplicate webhooks are dropped by `message_handle`.

## Texting & privacy compliance (Texas + multi-state, added Nov 5 2026 — additive; the 10-step Launch gate above is unchanged)

Code: `src/core/compliance.mjs`, `src/privacy.mjs`, migration `migrations/0005_compliance_privacy.sql`. Public policies: [/privacy](https://artisthq.ai/privacy) · [/terms](https://artisthq.ai/terms) · [/dpa](https://artisthq.ai/dpa) (Studio Data Processing Agreement).

| Control | How |
|---|---|
| Inbound-only consent | Clients are only ever texted after they text the studio first. Each first inbound records `cl.consent` (timestamp, source channel, number texted, CTA disclosure text) + an append-only `compliance_consent_ledger` row. Audit trail: `GET /api/tenants/:id/compliance/consent?phone=…`. |
| Unconsented-import ban | Manual entries and booking/CSV/historical imports are `optIn:false`, `consent.status='unconsented_import'`. Automation (follow-ups, nudges, offers) and HQ "sendable" refuse any phone without verified inbound consent (`unconsented-no-inbound`). Never upload or blast cold/purchased lists. |
| Texas SB 140 quiet hours | Automated follow-ups only Mon–Sat 9:00 AM–8:00 PM and **Sundays 12:00 PM–8:00 PM** (pre-noon Sunday rolls to 12:15 PM) when the studio or the recipient is in Texas. |
| Mini-TCPA (FL/OK/WA/MD) | Recipient area code → state + timezone. Automated sends only 8:00 AM–8:00 PM **recipient local time** (and studio local time), max **3 automated nudges / rolling 24h** per number (`daily-nudge-cap`). Re-checked at dispatch; deferred, never sent in quiet hours. |
| FCC revocation | `STOP`, `STOPALL`, `UNSUBSCRIBE`, `CANCEL`, `END`, `QUIT`, `OPTOUT`, `REVOKE` plus natural-language requests ("stop texting me", "take me off your list", "do not message me"…) → suppression, consent revoked + ledgered, pending jobs/follow-ups/escalations cancelled, no AI reply. Tattoo/scheduling phrases ("stop by Friday", "cancel my Friday appointment") do not trigger it. |
| Deletion (TDPSA) | Public form on /privacy → `POST /api/privacy/delete-request` (photos removed immediately; full deletion alerts the studio). Studio purge: `POST /api/tenants/:id/privacy/purge-client` (`photos_only` / `full_record`). Opt-outs and consent proof are always retained. |
| Media retention | Body/reference photos (incl. SendBlue/Twilio media URLs) only inside the studio's gated HQ; never public, never cross-tenant, never used to train shared AI models; scrubbed on deletion. See /dpa §3. |
| 18+ | Clients and bookings 18+ only (terms, privacy, DPA, bio CTA). |
| Per-studio A2P 10DLC | Every studio needs its **own** approved A2P 10DLC Brand + Campaign before Twilio SMS is enabled (Launch gate item 1). |
| D1 fault tolerance | Ledger/request writes are try/catch wrapped — a D1 hiccup can never block STOP or inbound handling. |

### Compliant Instagram / Bio CTA Copy (Texas TRAIGA & TCPA)

```
Text [Your HQ Number] with your tattoo idea, placement & size — replies assisted by Artist HQ AI. 18+ only. Msg & data rates apply; reply STOP to opt out. Privacy & Terms: artisthq.ai/privacy
```
