# Buddy CRM — Deployment Runbook

Last updated: 2026-05-06

How to deploy, update, and troubleshoot Buddy CRM. Written for someone who has never worked on this codebase before.

---

## Prerequisites

You need accounts and access to these services:

| Service | What for | URL |
|---|---|---|
| **GitHub** | Source of truth, push triggers deploy | `github.com/nownz-marc/buddy-crm` |
| **Netlify** | Hosting, functions, env vars, deploy logs | https://app.netlify.com |
| **Supabase** | Database (apply migrations, query data, toggle flags) | https://supabase.com/dashboard |
| **Microsoft Azure** | App registration (SSO + Graph) — see Max / Platform team | https://portal.azure.com |
| **Google Cloud** | Gemini API key | https://console.cloud.google.com |
| **Mailchimp** | API key | https://mailchimp.com |
| **Gmail** | App Password for SMTP notifications | https://mail.google.com |

---

## How the App is Deployed

Buddy CRM is a **static site** — HTML, CSS, and JS files are served directly from the repo root. There is no build step.

**Netlify Functions** in `netlify/functions/` are bundled by Netlify on each deploy using esbuild (configured in [netlify.toml](../netlify.toml)).

### Deploy Trigger

The Netlify site is connected to the GitHub repo. **Pushing to `main` triggers an auto-deploy.** Each commit becomes its own immutable deploy in the Netlify Deploys list — rolling back is "Publish deploy" on a previous entry.

If you ever need a manual deploy (e.g. the GitHub integration is broken), you can drag the project folder onto Netlify Drop as a fallback.

---

## Deploying a Code Change (Normal Path)

```bash
git add -A
git commit -m "feat: ..."
git push origin main
```

Netlify picks up the push, builds in ~30s, and publishes. Verify on the production URL.

> **No staging environment.** All deploys go straight to production. If a change is risky, gate it behind a `feature_flag` row that's `enabled=false` for everyone except super_admin until you're confident.

---

## Rolling Back a Deployment

1. Netlify → Site → Deploys
2. Find the last known-good deploy
3. Click it → "Publish deploy"

Netlify keeps every previous deploy indefinitely. Rollback is instantaneous and reversible (publish-forward to the latest deploy when you're ready).

---

## Hotfix Workflow

Same as a normal deploy. Push directly to `main`. There is no staging gate.

If the bug is on production right now and the fix isn't ready, **roll back first** (above), then build the fix on a branch, then push.

---

## Applying a Database Migration

Migrations are SQL files in the repo root, named `supabase-migration-*.sql`. Every file is **idempotent** (`CREATE … IF NOT EXISTS`, `ALTER … ADD COLUMN IF NOT EXISTS`, etc.) — safe to re-run.

1. Open the file in your editor
2. Supabase Dashboard → SQL Editor → paste → Run
3. Verify by querying any new table or column

The migrations currently in repo root (apply in any order — they're idempotent):

- `supabase-migration-opportunities.sql`
- `supabase-migration-opportunity-line-items.sql`
- `supabase-migration-opportunity-contact-role.sql`
- `supabase-migration-quote-versioning.sql`
- `supabase-migration-saved-reports.sql`
- `supabase-migration-report-builder-flag.sql`
- `supabase-migration-reports-2-entity-expand.sql`
- `supabase-migration-email-sync-2-1.sql`
- `supabase-migration-email-sync-2-2.sql` (encrypted token storage)
- `supabase-migration-email-sync-3.sql` (Layer 2 — folder + subscriptions)
- `supabase-migration-calendar-sync-phase-3.sql`
- `supabase-migration-meeting-outcomes.sql`
- `supabase-migration-salesperson-targets.sql`
- `supabase-migration-skill-queue.sql`

---

## First-Time Setup (New Environment)

### Step 1: Get the source code

```bash
git clone https://github.com/nownz-marc/buddy-crm.git
cd buddy-crm
npm install   # installs the 8 backend deps used by Netlify Functions
```

Or grab the latest weekly snapshot from [SharePoint](https://nownz.sharepoint.com/:f:/s/Tools/IgCJmDrcuGh5Q4cFoyy7dnVEAdL2H95BDPF2Pi-xFQjCyrM?e=CpyqwP).

### Step 2: Connect Netlify to the repo

1. https://app.netlify.com → "Add new site" → "Import an existing project"
2. Choose the GitHub repo
3. Build command: leave empty (no build step)
4. Publish directory: `.` (repo root)
5. Functions directory: `netlify/functions` (already in [netlify.toml](../netlify.toml))

### Step 3: Set environment variables

All credentials are in the company password manager — please contact Marketing for the Account Access and Passwords sheet.

In Netlify → Site configuration → Environment variables, set every variable listed in [docs/environment-variables.md](environment-variables.md). The critical-path subset:

```
SUPABASE_URL              = https://<project-ref>.supabase.co
SUPABASE_SERVICE_KEY      = <service-role key>
GEMINI_API_KEY            = <Google API key>
MAILCHIMP_API_KEY         = <Mailchimp key>
MAILCHIMP_SERVER_PREFIX   = <e.g. us1>
ALLOWED_ORIGINS           = https://<your-netlify-url>,https://<original-buddy-url>
```

For Outlook email + calendar:

```
MS_CLIENT_SECRET          = <server-side OAuth secret>
EMAIL_TOKEN_KEY           = <32-byte hex — AES-256-GCM key for refresh tokens>
```

For skill-queue notifications and the MCP/service path:

```
GMAIL_USER, GMAIL_APP_PASSWORD
BUDDY_SERVICE_KEY
```

Optional (have hardcoded fallbacks in `supabase-client.js`):

```
AZURE_TENANT_ID, AZURE_CLIENT_ID
ALLOWED_ORIGIN  (legacy single-value form; ALLOWED_ORIGINS supersedes it)
```

### Step 4: Configure Azure App Registration

The Azure App Registration is managed by Max / Platform team. For a new environment they need to:

1. Add the new Netlify URL as a **redirect URI** in the App Registration
2. Confirm the Microsoft Graph **delegated permissions** include `User.Read`, `Mail.ReadWrite`, `Calendars.Read`, `offline_access` (those are the scopes Buddy CRM requests). For Calendar write-back (Phase 3, parked), this would need upgrading to `Calendars.ReadWrite` and a fresh admin-consent prompt.
3. If a new client secret was issued for the server-side Outlook OAuth flow, hand it to you to put into `MS_CLIENT_SECRET`.

### Step 5: Set up Supabase

If you're connecting to the existing shared Supabase project, you only need the URL and service key in env vars (Step 3 above) — every CRM table already exists.

If you're creating a fresh project from scratch:

1. Create the project at https://supabase.com/dashboard
2. Apply every migration listed in the previous section via the SQL Editor
3. Manually create the legacy tables that pre-date the CRM fork (no migration file exists for these): `leads`, `lead_activities`, `salespeople`, `pricebook`, `feature_flags`, `user_permissions`, `quote_buddy_quotes`, `mrr_months`, `mrr_entries`, `mrr_audit`, plus the four reference tables (`lead_sources`, `lead_regions`, `lead_products`, `lead_lost_reasons`). Column lists are in [database-schema.md](database-schema.md).
4. Copy the project URL + service-role key into Netlify env vars

### Step 6: Push and verify

1. Push to `main` (or trigger a manual deploy if you haven't connected GitHub yet)
2. Visit the Netlify URL → expect a Microsoft login redirect
3. Sign in → expect the Sales Home dashboard
4. Open browser DevTools → Network → check that `/.netlify/functions/feature-flags` returns 200
5. Pick any feature page (e.g. `/leads`) and confirm data renders

---

## Adding a New HTML Page

1. Create the `.html` file in the project root
2. Add the standard script loading order in `<head>`:
   ```html
   <script src="https://alcdn.msauth.net/browser/2.38.0/js/msal-browser.min.js"></script>
   <script src="shared/auth.js"></script>
   <script src="shared/features.js"></script>
   <link rel="stylesheet" href="shared/styles.css">
   <script defer src="shared/nav.js"></script>
   <script defer src="shared/utils.js"></script>
   ```
3. Add a nav entry in `shared/nav.js` (the `NAV_ITEMS` array)
4. Add a feature flag if the page should be gated (insert via SQL in the `feature_flags` table)
5. Optionally add a clean URL rewrite in `_redirects`
6. Commit + push

---

## Adding or Modifying a Netlify Function

1. Create or edit a `.js` file in `netlify/functions/`
2. Import the shared client:
   ```js
   const { getSupabase, getUserFromToken, respond, setOrigin } = require('./supabase-client');
   ```
3. Export a handler:
   ```js
   exports.handler = async (event) => {
       setOrigin(event);
       if (event.httpMethod === 'OPTIONS') return respond(200, {});
       const user = await getUserFromToken(event.headers.authorization);
       if (!user) return respond(401, { error: 'Unauthorized' });
       // ... business logic
       return respond(200, result);
   };
   ```
4. Commit + push — Netlify auto-bundles with esbuild

Functions are accessible at `/.netlify/functions/{filename-without-extension}`.

If your function needs more than 10s to run (e.g. PDF generation), set a custom timeout in [netlify.toml](../netlify.toml). Example: `quote-generate` is set to 26s.

---

## Troubleshooting

### "Unauthorized" on every API call
- Check `SUPABASE_URL` and `SUPABASE_SERVICE_KEY` are set in Netlify env vars
- Check Netlify function logs for `[Auth] Token verification failed` messages
- The user's Microsoft ID token may have expired — log out and back in

### Page shows blank white screen
- `shared/auth.js` hides the page with `html:not(.authenticated) { display: none }` until auth resolves
- If MSAL fails, check the browser console for `[Auth]` messages
- Hard refresh, or clear sessionStorage and reload

### "Feature not available" / page won't render
- The `feature_flags` row may have `enabled=false` or `min_permission` higher than the user's
- Department restriction: if `departments` is non-empty and the user isn't in `salespeople.department`, they're blocked (super_admin bypasses)
- Use [manage-features.html](../manage-features.html) (super_admin only) or run an UPDATE in Supabase SQL Editor

### A function returns 500
- Netlify → Functions → click the function name → view real-time logs
- Common causes: missing env var, Supabase column mismatch, malformed JWT

### CORS errors in browser console
- Verify `ALLOWED_ORIGINS` env var includes the deployed URL exactly (scheme + host, no trailing slash)
- Multiple origins are comma-separated. The legacy `ALLOWED_ORIGIN` (singular) is still honoured

### "MSAL: interaction_in_progress"
- A login redirect started but didn't complete
- Clear sessionStorage / localStorage for the site
- Or open an incognito window

### Supabase connection errors
- Free-tier projects pause after 7 days of inactivity — Supabase Dashboard → Project → Restore
- Verify URL + service key are correct
- Check https://status.supabase.com/

### Outlook email/calendar isn't syncing
- Check `email_sync_log` for recent rows — if missing, the scheduled function isn't running
- Check `user_graph_tokens` for the BDM in question — `last_sync_error` will tell you why
- If `EMAIL_TOKEN_KEY` was rotated without a re-encryption pass, **all** stored refresh tokens become unreadable. Don't rotate this key casually
- For webhook drops: subscriptions in `email_subscriptions` expire ~3 days; the poller renews any with `expires_at` within 24h. If the poller's been down, manually re-create via `auth-outlook.js`'s subscribe flow

### Calendar v1 — meetings don't render on `/calendar`
- See `docs/handover-2026-05-06.md` § 1.1 for the active timezone-bug investigation
- Likely fix: add `Prefer: outlook.timezone="UTC"` header to the Graph fetch in `calendar-sync.js:152-156`

---

## DNS and Domain

Current production domain is the Netlify-provided subdomain (set in `ALLOWED_ORIGINS`).

To add a custom domain:
1. Netlify → Site settings → Domain management → Add custom domain
2. Update DNS records at your registrar (CNAME or A record per Netlify's instructions)
3. Netlify provisions SSL automatically (Let's Encrypt)
4. Update `ALLOWED_ORIGINS` env var to include the new domain
5. Ask Max / Platform team to add the new origin as a redirect URI on the Azure App Registration
