📋 Operations Manual
Innovate Healthcare Catalyst — Proposal Builder
← Back to Admin

Operations Manual

Proposal Builder Platform  ·  Last updated June 2026

1. System Overview

The Proposal Builder is a web-based sales and operations platform used by Innovate Healthcare to create and manage sponsorship proposals, track contacts and companies, manage projects, and handle invoicing. It is a static HTML/JavaScript application hosted on Netlify, backed by Supabase (database + authentication) and AWS S3 (backup storage).

Key Capabilities

2. Infrastructure & Services

LayerServicePurpose
Source codeGitHub (jspears-innovate/proposal-builder)All HTML, JS, Netlify functions
Hosting + serverless functionsNetlifyServes the static site; runs backend functions
Database + authenticationSupabase (nglakwuutsgbvpwwntaz)All app data, user auth
Email deliveryResendSends proposal PDFs and accounting emails
Backup storageAWS S3Daily JSON backups + archived proposal PDFs

Netlify Serverless Functions

FunctionTriggerPurpose
send-proposal.jsManualEmails proposal PDF to client
send-accounting.jsManualEmails accepted proposal to accounting
send-invoices.jsManualSends invoice emails
send-commission-report.jsManualSends commission report
mailchimp-subscribe.jsManualMailchimp list subscription
backup-export-to-s3.jsManual (Admin button)Uploads JSON backup to S3
backup-pdfs-to-s3.jsManual (Admin button)Archives proposal PDFs to S3
s3-backup-manager.jsManual (Admin button)Lists S3 backups; generates presigned download URLs
scheduled-backup.jsDaily 2:00 AM UTC + ManualFull backup: JSON + PDFs to S3; sends confirmation email
contact-form.jsHTTP POST (website form)Receives InnovateHealthcare.com contact form submissions; creates contact in Supabase; sends notification email; subscribes to Mailchimp IH Master List and brand list with interest groups
newsletter-reminders.jsScheduled (weekly)Sends newsletter assignment reminder emails to owners for newsletters due in the coming period; deduplicates by email address to avoid multiple sends per person

3. Pages & Features

3.1 Home (index.html)

Dashboard / landing page after login. Shows navigation to all sections based on the user's access group.

3.2 Proposal Builder (builder.html)

The core proposal creation tool.

Workflow

  1. Fill in client info — name, company, contact, presenter, audience
  2. Select services from the Rate Card (CVB, RB, or HE brand)
  3. Investment Summary auto-calculates totals
  4. Add custom line items if needed
  5. Preview the proposal
  6. Send Proposal — generates PDF and emails it to the client; archives the PDF URL to the proposal record
  7. Send to Accounting — emails the accepted proposal with full detail to the accounting team

Key Features

3.3 Proposals (proposals.html)

Table view of all saved proposals. Filter by status, search by client name or company. Actions per row: open in builder, view archived PDFs, delete.

3.4 Contacts (contacts.html)

Full contact database. Each contact has name, title, company, email, phone, LinkedIn, tags, and brand affiliation (CVB / RB / HE). Supports search, CSV import, and company linking.

Click the ✏ pencil icon on any contact row to open an inline edit modal — fields include First Name, Last Name, Title, Email, Phone, LinkedIn, and Notes. Changes save directly to Supabase and the table re-renders without a page reload.

3.5 Contact Detail (contact.html)

Individual contact record with full edit form, proposal history, and company link.

3.6 Companies (companies.html / company.html)

Company database with name, website, address, industry, notes, and associated contacts and proposals.

3.7 Orders (orders.html)

Monthly billing view showing all accepted proposals organized by their scheduled installment month. Primary tool for generating and emailing invoices.

Layout

Creating Invoices

  1. Select a month from the bar chart or month picker
  2. Check the Create Invoice box on each order card that needs an invoice
  3. The bottom action bar appears — click Create Invoices
  4. Select an invoice date and confirm
  5. Invoices are created in Supabase and automatically emailed to accounting as PDFs

Unapplied Payment Credits

When a proposal has a payment recorded on the Payments page that has not yet been matched to a paid invoice, the order card shows a credit option below the Order Total:

3.8 Invoices (invoices.html)

Invoice management generated from accepted proposals. Track paid / unpaid / cancelled status, mark as paid, and email invoices to clients.

Editing Invoice Amounts

Invoice amounts are stored when the invoice is created and do not automatically update if the proposal is later repriced. To correct an amount, click the amount value in the invoice row — an inline number field opens. Enter the correct amount and press Enter to save it to Supabase.

For proposals that have been repriced, also check the Payments page — a mismatch warning appears there with a one-click sync button (see §3.8b).

3.8b Payments (payments.html)

Payment tracking and commission reporting. Shows all proposals with payment activity grouped by company, with full billing schedules, invoice status, and recorded payments.

Access Gate

The Payments page is protected by a one-time access code. On every visit:

  1. The page shows a lock overlay
  2. Click Send Access Code — a 6-digit code is emailed to Accounting@innovatehealthcare.com
  3. Enter the code (expires in 10 minutes)
  4. Access is granted for 4 hours — the page auto-locks after the session expires or the tab is closed
The access code is generated and validated via the send-access-code Netlify function, which uses RESEND_API_KEY and SUPABASE_SERVICE_KEY. The code is stored temporarily in the settings table under key ih_pay_access_code and invalidated after first use.

Billing Schedule & Invoice Status

Expanding a proposal shows a table with one row per installment. Each row shows:

Payment Details Modal

Click any yellow (paid) invoice row to open the Payment Details modal, which shows and allows editing of:

This data is pulled from and saved directly to the invoices table in Supabase. The payment summary (date · check# · amount) updates on the row immediately after saving.

Invoice Amount Mismatch

If a proposal was repriced after its invoice was created, an amber warning badge appears on the installment row showing the stored invoice amount vs. the current proposal amount. Click the badge to update the invoice to the current amount in one step.

Recorded Payments (Ad-hoc)

The Recorded Payments section below the installment table allows entering payments that aren't tied to a specific invoice — useful for partial payments, deposits, or payments received outside the standard invoice flow. Each entry has: Date, Amount, and Check / Ref #.

Reports

Commission rates per presenter are set in Admin → Settings → Users / Presenters — each presenter card has a Commission % field (default 4%).

3.9 Projects (projects.html)

Project delivery board. Each accepted proposal becomes a project with a deliverables panel showing the Project Details bullets for each selected Rate Card section.

Key Features

Advertising Items Excluded

The following advertising-only catalog sections are intentionally excluded from the Projects board — they are tracked on the Advertising page instead:

3.10 Calendar, Webinars, Advertising, Lead

Additional operational views: Calendar — webinar/event scheduling; Webinars — webinar management; Lead — lead/prospect tracking.

Advertising (advertising.html)

Campaign management board showing advertising placements by section. Section order (left to right / top to bottom):

  1. Dedicated Email Promotions
  2. Website Banner Ads
  3. Newsletters
  4. Topic Portals

To reorder sections, edit the SECTIONS array in advertising.html (search for SECTIONS = [).

3.11 Tradeshow Pages (acc.html, rsna.html, etc.)

Twenty dedicated pages — one per conference — for managing exhibitor contacts at each show. Access via Admin → Tradeshows. Each page is keyed to a show tag (e.g. ACC, RSNA) that links contacts and companies to that show.

Exhibitor Table

Shows all companies registered as exhibitors for that show. Each company row lists its booth number and assigned contacts. The Booth # displays as plain text with a pencil icon — click to edit inline, then click away or press Enter to save. This keeps the table compact without a permanent input field.

Unassigned Contacts

Below the exhibitor table, the Unassigned Contacts section lists all contacts tagged with the show's tag but not yet assigned to an exhibitor row. Each contact row has four icon buttons on the right:

IconAction
🏢Add the contact's company to the exhibitor list for this show
Assign the contact to their company's exhibitor row
Open the contact edit modal (same fields as Contacts page)
Remove the show tag from the contact (removes them from this page)

Contacts appear in Unassigned when they have the show tag but their company is either not an exhibitor or not yet added to that show's list. Use the 🏢 button to add the company first, then ➕ to assign.

3.12 Contact Form Embed (contact-form-embed.html)

A standalone, embeddable contact form designed to be iframed into InnovateHealthcare.com. Styled to match the Innovate site (green #2d7a3a primary, system sans-serif, white background).

Fields

Behaviour

Embed Code

<iframe src="https://catalyst.innovatehealthcare.com/contact-form-embed.html"
  width="100%" height="700" frameborder="0"></iframe>

4. Admin Panel — Full Reference

Access: Admin (admin.html) — requires Admin group membership. The panel is divided into tabs: Rate Card, SOW, Project Details, Terms, and Settings.

4.1 Rate Card

Defines all billable services across three brands: CVB, RB, and HE.

Important: Rate Card section names are the link between Rate Card → SOW bullets → Project Details bullets. Renaming a section updates all three automatically via UUID foreign keys — no manual sync needed.

4.2 Statement of Work (SOW)

Defines the bullet points that appear in the SOW section of each proposal, organized by Rate Card section and brand. Editing CVB bullets auto-propagates to RB and HE. Changes auto-save per section. Print Proof available.

4.3 Project Details

Defines the deliverable bullet points that appear on the Projects board for each Rate Card section. Bullets have text, bold flag, and owner assignment. CVB is the master — edits propagate to RB and HE automatically. Print Proof available.

How it connects to Projects: When a proposal is accepted and becomes a project, the Projects board shows the Project Details bullets for each Rate Card section that was included in the proposal.

4.4 Terms

Defines the Terms & Conditions sections that appear in proposals. Each term has a Title and Body. Add / edit / reorder / delete. Print Proof available.

4.5 Settings

SettingDescription
Audience Drop-Down OptionsCustom audience options in the proposal builder's audience selector
Brand ColorsHex color values for CVB, RB, HE — applied to proposal PDFs and UI accents
Whitepaper DefaultsDefault whitepaper rate used in proposals
Benefits of PartnershipBullet list of partnership benefits shown in proposals
Pricing ValidityBullet list of pricing validity statements
InvoicingBullet list of invoicing terms shown in proposals
Project OwnersPeople who can be assigned as owners on Project Detail bullets
Proposal StatusesCustom status labels for the proposal lifecycle
Project Status OptionsCustom status labels for project tracking
User GroupsWhich users belong to which access groups (Sales, Finance, Operations, Admin)
BillingBilling options with ID, label, and line items
Users / PresentersSales staff list shown in the proposal builder. Each entry has: Name, Title, Company, Email, and Commission % (default 4%) — used in the Excel commission report on the Payments page.

4.5b Newsletters (Admin Accordion)

The Newsletters accordion in Admin manages the newsletter schedule for all three brands. Each brand (CVB, RB, HE) has its own table of newsletter issues with the following columns:

ColumnDescription
NameNewsletter issue name or title
OwnerTeam member responsible for this issue
Due DateIssue due date — used by the Newsletter Reminder function to determine which reminders to send
Send DateScheduled send date
FrequencyDaily / Weekly / Monthly
StatusDraft / Scheduled / Sent
The Newsletter Reminder Netlify function reads this schedule and sends reminder emails to owners for newsletters due in the coming period. It consolidates by email address — owners listed under multiple newsletters receive a single combined email.

4.5c Global Settings — Tradeshow Tasks

Within the Global Settings group in Admin, Tradeshow Tasks is a collapsible section that defines the standard task checklist template applied to each tradeshow. Tasks entered here appear as a reusable task list on each tradeshow page. Click ▼ expand to open the editor, add or reorder tasks, then save.

4.5d Global Settings — API Keys

The API Keys section within Global Settings stores third-party API credentials used by Netlify functions. Currently:

These values are read server-side by contact-form.js and mailchimp-subscribe.js — they are never exposed in the browser URL. Enter keys only here; never share them in chat or email.

4.6 Contact Form Flow

Accessible at Admin → Contact Form Flow. A visual flowchart showing how submissions from the InnovateHealthcare.com contact form are processed — which email is notified, whether a Supabase contact record is created, and how Mailchimp subscriptions are applied per interest type.

Routing by Interest

Interest SelectedNotificationSupabase ContactMailchimp
Media Kit / Advertising InformationSales@✅ CreatedIH Master List (interest groups + automation tag) + Brand list (interest groups)
Custom Marketing Programs / WebinarsSales@✅ CreatedIH Master List (interest groups + automation tag) + Brand list (interest groups)
Publishing a Press ReleaseManagingEditor@✅ CreatedIH Master List (interest groups, no automation tag)
Billing or Accounting IssueAccounting@❌ SkippedNone
Editorial Correction or InquiryManagingEditor@❌ SkippedNone
Submit a News TipManagingEditor@❌ SkippedNone
OtherClientServices@❌ SkippedNone

Mailchimp Interest Groups

For CRM interests (Media Kit, Custom Marketing, Press Release), the function sets Mailchimp interest groups (not tags) on the IH Master List based on the Publication of Interest selected:

For Media Kit and Custom Marketing interests, the corresponding brand list (CVB / RB / HE) also receives a subscription with newsletter interest groups (Daily News, Weekly News, Monthly News, Announcements) set as Mailchimp interest groups — not tags. The media kit request date is written to the appropriate Mailchimp merge field (MMERGE7 / MMERGE8 / MMERGE10).

Note: The Contact Form Flow page also contains a reference table of all Mailchimp interest group IDs for the IH Master List and each brand list — useful when configuring automations in Mailchimp.

4.7 Data & Backup Management

ButtonWhat it does
⬆ Sync to CloudPushes all localStorage settings to Supabase (use after offline edits)
⬇ Export BackupDownloads a complete .json snapshot to your computer
↑ Restore from FileRestores from a .json file on your computer
▶ Run Full Backup NowTriggers the full scheduled backup manually (JSON + PDFs to S3 + email)
📄 Export Data → S3Uploads a JSON-only snapshot to S3 (no PDFs, faster)
📑 Backup PDFs → S3Archives all proposal PDFs to S3
☁ Restore from S3Browse and restore from any S3 backup

4.8 Data Cleanup

The Data Cleanup accordion in Admin contains all tools for maintaining contact and company data quality. Panels can be dragged to reorder — the order is saved to Supabase and shared across all users.

Company Duplicate Detector

Scans all companies and groups records with similar names, matching websites, or matching phone numbers. Each group is presented as a table — click a row to select the record to keep, then click Merge →. The merge operation re-points all contacts, proposals, invoices, and tradeshow registrations to the keeper record and deletes the duplicates.

Find Duplicate Companies

Groups companies by identical name (case-insensitive). Same workflow: select keeper → Merge →. Uses dbMergeCompanies() which handles all FK re-pointing and tag merging automatically.

Find Duplicate Contacts

Groups contacts by matching email (primary) or matching first + last name (secondary). Select the record to keep, click Merge →. The merge:

Auto-classify Company Statuses

Sets company status automatically from proposal history — companies with Accepted proposals become Active, companies with Sent proposals become Proposal. Only upgrades statuses; never downgrades a status that is already set higher.

Fix Broken Parent Links

Finds and removes invalid parent_company_id references — e.g. a company pointing to a parent that no longer exists. Safe to run at any time.

Company Status CSV Import

Paste a CSV with company name and status columns to bulk-set company statuses. Useful for importing status data from an external CRM or spreadsheet.

Company Parent Relationships

Tools for managing parent/child company relationships (org chart). Assign, move, or remove parent company links in bulk.

Find Missing Websites

Lists all companies that have no website on file, so they can be prioritised for Apollo enrichment or manual entry.

Link Contacts → Companies (FK Backfill)

Sets the company_id foreign key on all contacts that are linked only by text company name. Matching runs in three passes:

  1. Exact — case-insensitive name match
  2. Fuzzy — strips parentheticals ((CT), (Cardiology)) and legal suffixes (Inc., LLC, etc.) then matches
  3. Slash/Pipe — takes the first segment of Philips / Biotel → tries Philips
  4. Prefix drop — progressively removes trailing words to catch sub-brand names like Fujifilm Medical Systems USAFujifilm

Results are reported as: N linked (exact) · N linked (fuzzy) · N no match · N no company name set.

The no-match list renders as clickable blue chips. Click a chip to create that company and immediately link all waiting contacts to it via FK. The new company's DB-confirmed UUID is used as the FK — no orphaned links.

Recommended workflow: Run Find Duplicate Companies first to consolidate duplicates, then run Link Contacts → Companies to set FKs. Run it again after creating missing companies from the no-match chips.

Apollo Enrichment — Enrich Companies

Bulk-enriches all companies via the Apollo Organization Enrich API. Fills: website, phone, address, city, state, zip, industry. Uses the company's domain (website field) if set, otherwise falls back to the company name. Processes in batches of 10 with a live progress bar. Requires an Apollo API key saved in Admin → API Keys → Company Enrichment Key.

Apollo Enrichment — Enrich Contacts

Bulk-enriches all contacts via the Apollo People Match API. Fills: title, phone, LinkedIn URL. Matches by email first; falls back to first name + last name + company name. Same batching and API key requirement as company enrichment.

API key security: Apollo API keys are stored in the Supabase settings table and passed server-side via Netlify functions — never in the browser URL. Enter keys only through Admin → API Keys, never paste them in chat or email.

Export Companies to Excel

Downloads all companies as IH-Companies.xlsx with columns: Name, Website, Phone, Address, City, State, Zip, Industry, Status, Sales Person, Tags.

Export Contacts to Excel

Downloads all contacts as IH-Contacts.xlsx with columns: First Name, Last Name, Title, Email, Phone, LinkedIn, Company, Agency, Tags.

Import Companies from Excel

Upload an .xlsx, .xls, or .csv file to bulk-create or update companies. First row must be headers. Required column: Name (also accepts "Company" or "Company Name"). Optional columns are matched case-insensitively and include common aliases (e.g. "Salesperson" → Sales Person). Behaviour on match:

A 5-row preview is shown before import. Progress is displayed during import (batches of 50). Results show: N created · N updated · N skipped.

Import Contacts from Excel

Upload an .xlsx, .xls, or .csv file to bulk-create or update contacts. Required columns: First Name + Last Name. Deduplication order:

  1. Email match (case-insensitive)
  2. First + Last + Company match (fallback for contacts without email)

On match: blank fields filled, existing values kept, tags unioned. Company FK (company_id) is automatically resolved if the company name exists in the database. Same preview + batch import flow as Companies.

5. Data Architecture

Supabase Tables

TablePurpose
proposalsAll proposal records (JSONB data blob + metadata)
contactsContact database
companiesCompany database
usersPresenters / sales staff list
settingsKey-value store for app settings (catalogs, billing, terms, etc.)
catalog_sectionsRate Card sections — one row per section per brand (CVB/RB/HE)
catalog_itemsRate Card items — linked to catalog_sections by section_id
sow_bulletsSOW bullet points — linked to catalog_sections by section_id
project_detail_bulletsProject Details bullets — linked to catalog_sections by catalog_section_id
invoicesInvoice records

Key Relationships

catalog_sections (id, brand, name, sort_order) │ ├── catalog_items (section_id → catalog_sections.id) │ Each item = one billable service in the Rate Card │ ├── sow_bullets (section_id → catalog_sections.id) │ Each bullet = one SOW deliverable line │ └── project_detail_bullets (catalog_section_id → catalog_sections.id) Each bullet = one project deliverable with owner

Brand architecture: Each Rate Card section exists three times in catalog_sections — once per brand (CVB, RB, HE). Items and bullets are stored separately per brand, linked by UUID. Renaming a section updates all three brand rows automatically.

Project Details propagation: When editing Project Details bullets for a CVB section, the system automatically writes identical bullets to the matching RB and HE rows so all brands stay in sync.

6. Backup & Restore Procedures

6.1 What Gets Backed Up

DataTables / Source
Proposalsproposals
Contactscontacts
Companiescompanies
Presentersusers
App settingssettings
Rate Cardcatalog_sections, catalog_items
SOW bulletssow_bullets
Project Detail bulletsproject_detail_bullets
Archived PDFsDownloaded from Supabase Storage URLs → re-uploaded to S3

6.2 Automated Daily Backup

The scheduled backup runs automatically every day at 2:00 AM UTC. It fetches all 9 tables from Supabase, uploads a timestamped JSON file to S3, downloads and re-uploads all archived proposal PDFs, then sends a summary email to BACKUP_NOTIFY_EMAIL.

If it fails: An alert email is sent. Check Netlify → Functions → scheduled-backup → Logs for the error detail.

6.3 Manual Backup — Run Full Backup Now

Use after major data changes (recovery operation, bulk import, schema change).

  1. Go to Admin → Data & Backup Management
  2. Click ▶ Run Full Backup Now
  3. If prompted for the Backup Secret, enter the value from Netlify env vars (BACKUP_SECRET) — saved in your browser after first entry
  4. Wait for the success toast (typically 15–30 seconds)
  5. A confirmation email is sent to BACKUP_NOTIFY_EMAIL

6.4 Manual Backup — Export Data → S3

Faster option for a data-only snapshot (no PDF archiving, no email).

  1. Go to Admin → Data & Backup Management
  2. Click 📄 Export Data → S3
  3. Wait for the success toast (~5–10 seconds)

When to use: After catalog/settings changes, before a restore test, or when the full backup is unavailable.

6.5 Manual Backup — Export to Desktop (Local)

Downloads a .json file directly to your computer. Use as a safety net before any restore operation.

  1. Go to Admin → Data & Backup Management
  2. Click ⬇ Export Backup
  3. File downloads immediately as proposal-builder-backup-YYYY-MM-DD.json
Note: This reads directly from Supabase, not localStorage — it always reflects the true cloud state.

6.6 Restore from S3

Use to recover from data loss or roll back to an earlier state.

  1. First, run ⬇ Export Backup (desktop) to save a current snapshot as a safety net
  2. Go to Admin → Data & Backup Management
  3. Click ☁ Restore from S3
  4. The modal lists all S3 backups, newest first, with date, size, and timestamp
  5. Click the backup you want to restore from
  6. Read the confirmation summary carefully — it shows proposal/contact/section counts
  7. Click Restore to confirm
  8. Wait for the log to complete (typically 10–30 seconds)
  9. Verify: check Rate Card, SOW, Project Details, and a sample proposal

What Restore Does

DataBehavior
Proposals, Contacts, CompaniesUpserted — existing data is merged, nothing deleted
Users / PresentersFully replaced
SettingsUpserted by key
Relational tables (catalog, SOW, project detail bullets)Fully replaced — delete all → re-insert from backup
Important: Because catalog tables are fully replaced, any Rate Card or SOW edits made after the backup date will be lost. Always export a current backup first.

6.7 Restore from File (Desktop)

  1. Go to Admin → Data & Backup Management
  2. Click ↑ Restore from File
  3. Select your .json backup file
  4. Review the confirmation summary and click Restore

Behavior is identical to Restore from S3 — same merge/replace logic.

6.8 Backup PDFs Only

  1. Go to Admin → Data & Backup Management
  2. Click 📑 Backup PDFs → S3
  3. Wait for the toast — shows PDFs backed up and skipped (no archived PDF = skipped)

6.9 S3 Backup File Structure

s3://[AWS_S3_BUCKET]/ ├── backups/ │ ├── 2026-05-02/ │ │ ├── proposal-builder-backup-2026-05-02-02-00-00.json ← daily auto │ │ └── proposal-builder-backup-2026-05-02-14-30-00.json ← manual run │ └── 2026-05-01/ │ └── proposal-builder-backup-2026-05-01-02-00-00.json └── proposals/ ├── {proposal-uuid}/ │ ├── sent.pdf │ └── accepted.pdf └── {proposal-uuid}/ └── sent.pdf

Multiple backups per day are kept — each run produces a new timestamped file. No backups are ever overwritten or deleted automatically.

6.10 Backup Checklist — After Major Changes

Run after: bulk data import, schema change, recovery operation, or major catalog edit.

7. Environment Variables Reference

Netlify Environment Variables

Set in Netlify → Site configuration → Environment variables.

VariableRequiredDescription
RESEND_API_KEYResend email API key
SUPABASE_URLSupabase project URL (for backup function)
SUPABASE_SERVICE_ROLE_KEYService role key — bypasses RLS for backup
SUPABASE_ANON_KEYFallbackAnon key — used if service role key not set
AWS_S3_BUCKETS3 bucket name
AWS_S3_REGIONS3 region (e.g. us-east-1)
S3_ACCESS_KEY_IDAWS access key ID
S3_SECRET_ACCESS_KEYAWS secret access key
BACKUP_SECRETRandom string — authenticates manual backup triggers from browser
BACKUP_NOTIFY_EMAILEmail address for backup success/failure notifications
MAILCHIMP_API_KEYContact formMailchimp API key — fallback if not stored in Supabase settings. Prefer setting via Admin → API Keys in the app.
MAILCHIMP_DCContact formMailchimp data center prefix (e.g. us1, us21) — fallback if not stored in settings
Mailchimp keys in the app: The Mailchimp API key and data center are primarily stored in the Supabase settings table (keys mailchimp_api_key and mailchimp_dc) and managed via Admin → API Keys. The Netlify env vars above serve as a fallback if the settings table values are absent.

Client-Side (in db.js)

These are intentionally in client-side code — they are scoped to Row Level Security policies.

VariableLocationDescription
SUPABASE_URLdb.js lines 4–5Supabase project URL
SUPABASE_KEYdb.js lines 4–5Supabase anon/public key

8. Access & User Groups

GroupPages
SalesHome, Builder, Proposals, Contacts, Prospects, Companies, Contact, Company, Lead
FinanceHome, Proposals, Invoices, Payments, Orders
OperationsHome, Proposals, Projects, Calendar, Webinars
AdminAll pages

Manage access at Admin → Settings → User Groups. Changes take effect on the user's next login.

Backup Secret: The BACKUP_SECRET is stored in localStorage in the Admin's browser after the first successful entry. Find the value at Netlify → Site configuration → Environment variables → BACKUP_SECRET.

9. Deployment & Code Changes

  1. Edit files locally in the proposal-builder folder
  2. Test in the browser
  3. Commit: git add <files> && git commit -m "Description"
  4. Push: git push — Netlify auto-deploys within ~1 minute

Check Netlify → Deploys for deploy status. After any schema change in Supabase: update db.js, update scheduled-backup.js if new tables need backing up, update dbFullBackup() / dbFullRestore(), run a manual backup, commit and push.

10. Standing Up a New Environment

Use this section to deploy the Proposal Builder from scratch — migration, staging environment, or catastrophic recovery.

Estimated time: 45–90 minutes  ·  Prerequisites: Access to GitHub, Netlify, Supabase, Resend, and AWS

10.1 Prerequisites

ServiceWhat you need
GitHubAccount with permission to create repos
NetlifyAccount (free tier is sufficient)
SupabaseAccount — create a new project
ResendAccount with a verified sending domain
AWSIAM user with S3 read/write access to your backup bucket

10.2 Step 1 — Get the Source Code

# Option A — Clone existing repo
git clone https://github.com/jspears-innovate/proposal-builder.git
cd proposal-builder

# Option B — New repo from downloaded ZIP
cd proposal-builder
git init
git add .
git commit -m "Initial commit"
git remote add origin https://github.com/YOUR-ORG/YOUR-REPO.git
git push -u origin main

10.3 Step 2 — Create Supabase Project & Run Schema SQL

  1. Go to app.supabase.comNew project
  2. Wait for provisioning, then go to Project Settings → API and note the Project URL, anon key, and service_role key
  3. Go to SQL Editor and run the schema below
-- proposals
create table proposals (
  id uuid primary key default gen_random_uuid(),
  created_at timestamptz default now(), updated_at timestamptz default now(),
  client_name text, client_company text, status text default 'Draft',
  data jsonb, pdf_sent_url text, pdf_accepted_url text
);
alter table proposals enable row level security;
create policy "Auth users full access" on proposals for all using (auth.role() = 'authenticated');

-- settings
create table settings (key text primary key, value jsonb, updated_at timestamptz default now());
alter table settings enable row level security;
create policy "Auth users full access" on settings for all using (auth.role() = 'authenticated');

-- users (presenters)
create table users (
  id uuid primary key default gen_random_uuid(),
  name text, title text, company text, email text, sort_order integer default 0,
  created_at timestamptz default now()
);
alter table users enable row level security;
create policy "Auth users full access" on users for all using (auth.role() = 'authenticated');

-- contacts
create table contacts (
  id uuid primary key default gen_random_uuid(),
  first_name text, last_name text, email text, phone text, title text,
  company text, agency text, linkedin text, notes text,
  pub_cvb boolean default false, pub_rb boolean default false, pub_he boolean default false,
  tags text[] default '{}', updated_at timestamptz default now()
);
alter table contacts enable row level security;
create policy "Auth users full access" on contacts for all using (auth.role() = 'authenticated');

-- companies
create table companies (
  id uuid primary key default gen_random_uuid(),
  name text, website text, phone text, address text, city text,
  state text, zip text, industry text, notes text,
  updated_at timestamptz default now()
);
alter table companies enable row level security;
create policy "Auth users full access" on companies for all using (auth.role() = 'authenticated');

-- invoices
create table invoices (
  id uuid primary key default gen_random_uuid(),
  proposal_id uuid references proposals(id) on delete set null,
  company text, label text, amount numeric(10,2), status text default 'pending',
  paid boolean default false, paid_at timestamptz, cancelled_at timestamptz,
  created_at timestamptz default now(), updated_at timestamptz default now()
);
alter table invoices enable row level security;
create policy "Auth users full access" on invoices for all using (auth.role() = 'authenticated');

-- catalog_sections (one row per section per brand: cvb / rb / he)
create table catalog_sections (
  id uuid primary key default gen_random_uuid(),
  brand text not null, name text not null, sort_order integer default 0,
  hint text default '', badge text default '', updated_at timestamptz default now()
);
alter table catalog_sections enable row level security;
create policy "Auth users full access" on catalog_sections for all using (auth.role() = 'authenticated');

-- catalog_items
create table catalog_items (
  id uuid primary key default gen_random_uuid(),
  section_id uuid references catalog_sections(id) on delete cascade,
  brand text, name text, description text, price numeric(10,2) default 0,
  enabled boolean default true, type text default 'checkbox', sort_order integer default 0,
  suffix text default '', qty_label text default '', qty_max integer,
  multi_instance boolean default false, has_month_picker boolean default false,
  exclusive boolean default false, updated_at timestamptz default now()
);
alter table catalog_items enable row level security;
create policy "Auth users full access" on catalog_items for all using (auth.role() = 'authenticated');

-- sow_bullets
create table sow_bullets (
  id uuid primary key default gen_random_uuid(),
  section_id uuid references catalog_sections(id) on delete cascade,
  text text, bold boolean default false, sort_order integer default 0,
  updated_at timestamptz default now()
);
alter table sow_bullets enable row level security;
create policy "Auth users full access" on sow_bullets for all using (auth.role() = 'authenticated');

-- project_detail_bullets
create table project_detail_bullets (
  id uuid primary key default gen_random_uuid(),
  catalog_section_id uuid references catalog_sections(id) on delete cascade,
  text text, bold boolean default false, owner text default '',
  sort_order integer default 0, updated_at timestamptz default now()
);
alter table project_detail_bullets enable row level security;
create policy "Auth users full access" on project_detail_bullets for all using (auth.role() = 'authenticated');

-- ─────────────────────────────────────────────────────────────────────────────
-- Data API grants (required for new Supabase projects created after May 30 2026)
-- Without these, PostgREST / supabase-js returns a "42501" permission error.
-- ─────────────────────────────────────────────────────────────────────────────
grant select, insert, update, delete on public.proposals             to anon, authenticated, service_role;
grant select, insert, update, delete on public.settings              to anon, authenticated, service_role;
grant select, insert, update, delete on public.users                 to anon, authenticated, service_role;
grant select, insert, update, delete on public.contacts              to anon, authenticated, service_role;
grant select, insert, update, delete on public.companies             to anon, authenticated, service_role;
grant select, insert, update, delete on public.invoices              to anon, authenticated, service_role;
grant select, insert, update, delete on public.catalog_sections      to anon, authenticated, service_role;
grant select, insert, update, delete on public.catalog_items         to anon, authenticated, service_role;
grant select, insert, update, delete on public.sow_bullets           to anon, authenticated, service_role;
grant select, insert, update, delete on public.project_detail_bullets to anon, authenticated, service_role;

Authentication & Storage

10.4 Step 3 — Update db.js

// db.js lines 4-5
const SUPABASE_URL = 'https://YOUR-NEW-PROJECT.supabase.co';
const SUPABASE_KEY = 'YOUR-NEW-ANON-KEY';
git add db.js && git commit -m "Update Supabase credentials" && git push

10.5 Step 4 — Set Up Resend

  1. Go to resend.comDomains → Add Domain
  2. Add DNS records (SPF, DKIM, DMARC) — wait for verification
  3. Go to API Keys → Create API Key — copy the key (re_...)
  4. If the domain changes, update the from: field in: send-proposal.js, send-accounting.js, send-invoices.js, scheduled-backup.js

10.6 Step 5 — Set Up AWS S3

  1. S3 → Create bucket — block all public access ON (access is via signed URLs)
  2. IAM → Users → Create user — attach AmazonS3FullAccess or a scoped bucket policy
  3. Security credentials → Create access key — copy Access Key ID and Secret

10.7 Step 6 — Create & Configure Netlify Site

  1. app.netlify.comAdd new site → Import an existing project
  2. Connect GitHub → select repo. Build settings auto-detected from netlify.toml.
  3. Click Deploy site
  4. Go to Site configuration → Environment variables and add all variables:
VariableValue
RESEND_API_KEYFrom Resend API Keys
SUPABASE_URLYour Supabase project URL
SUPABASE_SERVICE_ROLE_KEYSupabase → Project Settings → API → service_role
SUPABASE_ANON_KEYSupabase → Project Settings → API → anon/public
AWS_S3_BUCKETYour S3 bucket name
AWS_S3_REGIONe.g. us-east-1
S3_ACCESS_KEY_IDFrom AWS IAM user
S3_SECRET_ACCESS_KEYFrom AWS IAM user
BACKUP_SECRETAny random string (e.g. openssl rand -hex 24)
BACKUP_NOTIFY_EMAILEmail for backup notifications

After adding variables: Deploys → Trigger deploy to redeploy.

10.8 Step 7 — Seed Initial Data

Option A — Restore from backup (recommended for migrations): Log in as Admin → Admin → ↑ Restore from File — select your .json backup. This restores all proposals, contacts, catalog, and settings.

Option B — Fresh start: Log in as Admin → Admin → Database Migration → ⚡ Run Catalog Migration. Then manually configure pricing, SOW bullets, and Project Details.

10.9 Verification Checklist

Authentication

Rate Card & Catalog

Proposal Builder

Email

Backup

Projects

10.10 Migrating from the Existing Environment

  1. Run a full backup on the old site — Admin → ▶ Run Full Backup Now
  2. Export to desktop — Admin → ⬇ Export Backup — save a local .json
  3. Stand up the new environment following Steps 1–7 above
  4. Restore the backup on the new site — Admin → ↑ Restore from File
  5. Re-invite all users via Supabase → Authentication → Users (auth users do not transfer between Supabase projects)
  6. Verify the checklist in Step 8
  7. Update DNS to point to the new Netlify site

Appendix — Quick Reference Card

Daily / Weekly Tasks

TaskWhere
Check last backup ranAdmin → Data & Backup Management → "Last backup" banner
Run an on-demand backupAdmin → ▶ Run Full Backup Now
Edit Rate Card pricingAdmin → Rate Card
Edit SOW bulletsAdmin → SOW
Edit Project DetailsAdmin → Project Details
Add a new userAdmin → Settings → User Groups

If Something Goes Wrong

ProblemFirst Step
Data appears missing or wrongExport current state first, then assess
Rate Card / SOW / Project Details blankCheck Supabase → catalog_sections table has rows
Backup failedCheck Netlify → Functions → scheduled-backup → Logs
Can't log inCheck Supabase → Authentication → Users
PDF email not sendingCheck Netlify → Functions → send-proposal → Logs; verify RESEND_API_KEY
Projects board shows no bulletsCheck project_detail_bullets table has rows with catalog_section_id set
Payments access code email not arrivingCheck Netlify → Functions → send-access-code → Logs; verify RESEND_API_KEY is set and Accounting@innovatehealthcare.com is a valid recipient in Resend
Excel Report shows no rowsEnsure invoices are marked Paid (not just payment-date entered). Filter by the correct month first.
Topic Portals showing multiple rows on ProjectsExpected behaviour was fixed — portals use qty-as-months and collapse to a single row. If still duplicating, check that the catalog item has qtyLabel: 'Months'.
Website contact form submission not creating a Supabase contactCheck Netlify → Functions → contact-form → Logs. Common causes: Supabase service role key missing/wrong; interest was non-CRM (Billing, Editorial, News Tip, Other — these intentionally skip contact creation); UUID generation failure.
Contact form Mailchimp subscription not appliedCheck that mailchimp_api_key and mailchimp_dc are set in Admin → API Keys (or as Netlify env vars). Verify the list IDs and interest group IDs in contact-form.js match the Mailchimp account.
Newsletter reminder emails sending duplicatesCaused by the same person listed under different name spellings in owner settings — each spelling got a separate send. Fixed: function now consolidates by email address before sending. Verify all owner names in Admin → Settings → Project Owners have consistent spelling.
Tradeshow page contact not appearing in Unassigned ContactsContact must have the show's tag (e.g. SBI) set. Check the contact's tags in Contacts → edit modal. Also confirm the contact-form.js conference tag mapping matches the show's SHOW_KEY.