Operations Manual
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
- Build branded proposals (CVB, RB, HE) with an interactive rate card
- Generate and email PDF proposals directly to clients
- Track proposal status through the full sales lifecycle
- Manage contacts, companies, and prospects
- Manage project delivery details (deliverables per proposal)
- Run invoicing and payment tracking
- Webinar scheduling and calendar view
- Full automated daily backup to AWS S3
2. Infrastructure & Services
| Layer | Service | Purpose |
|---|---|---|
| Source code | GitHub (jspears-innovate/proposal-builder) | All HTML, JS, Netlify functions |
| Hosting + serverless functions | Netlify | Serves the static site; runs backend functions |
| Database + authentication | Supabase (nglakwuutsgbvpwwntaz) | All app data, user auth |
| Email delivery | Resend | Sends proposal PDFs and accounting emails |
| Backup storage | AWS S3 | Daily JSON backups + archived proposal PDFs |
Netlify Serverless Functions
| Function | Trigger | Purpose |
|---|---|---|
send-proposal.js | Manual | Emails proposal PDF to client |
send-accounting.js | Manual | Emails accepted proposal to accounting |
send-invoices.js | Manual | Sends invoice emails |
send-commission-report.js | Manual | Sends commission report |
mailchimp-subscribe.js | Manual | Mailchimp list subscription |
backup-export-to-s3.js | Manual (Admin button) | Uploads JSON backup to S3 |
backup-pdfs-to-s3.js | Manual (Admin button) | Archives proposal PDFs to S3 |
s3-backup-manager.js | Manual (Admin button) | Lists S3 backups; generates presigned download URLs |
scheduled-backup.js | Daily 2:00 AM UTC + Manual | Full backup: JSON + PDFs to S3; sends confirmation email |
contact-form.js | HTTP 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.js | Scheduled (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
- Fill in client info — name, company, contact, presenter, audience
- Select services from the Rate Card (CVB, RB, or HE brand)
- Investment Summary auto-calculates totals
- Add custom line items if needed
- Preview the proposal
- Send Proposal — generates PDF and emails it to the client; archives the PDF URL to the proposal record
- Send to Accounting — emails the accepted proposal with full detail to the accounting team
Key Features
- Multi-brand support: CVB / RB / HE with separate pricing
- Qty, monthly, and multi-instance service types
- Billing option selector & pricing validity statement
- PDF generation via browser print-to-PDF
- Proposal status tracking (Draft → Sent → Accepted / Declined)
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
- Bar chart — one bar per month showing total order value. Click a bar or use the month picker to drill into that month's orders.
- Order cards — each card shows the company, proposal ID, description, line items, and Order Total. Cards with an existing invoice show the invoice number, a PDF download button, and a Cancel Invoice button.
- Unscheduled section — orders without a scheduled billing date appear in a collapsible dark section at the bottom.
- Accepted / Sent toggle — switch between showing only Accepted proposals or including Sent proposals.
- Company filter — narrow the view to a specific company.
Creating Invoices
- Select a month from the bar chart or month picker
- Check the Create Invoice box on each order card that needs an invoice
- The bottom action bar appears — click Create Invoices
- Select an invoice date and confirm
- 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:
- A 💰 Unapplied Payment Credit row shows the credit amount available
- Check Apply unapplied payment credit to deduct it from the invoice before creation
- A Balance Due row shows the net amount — the invoice and PDF will use this reduced figure
- If no invoice has been created yet, the credit checkbox only appears on uninvoiced cards
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.
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:
- The page shows a lock overlay
- Click Send Access Code — a 6-digit code is emailed to
Accounting@innovatehealthcare.com - Enter the code (expires in 10 minutes)
- Access is granted for 4 hours — the page auto-locks after the session expires or the tab is closed
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:
- # — installment number
- Paid — checkbox to mark/unmark the invoice as paid (requires entering payment details)
- Description — installment name and Bill Date
- Amount — current amount from the proposal
- Invoice — linked invoice number, invoice date (click to edit), and payment summary
Payment Details Modal
Click any yellow (paid) invoice row to open the Payment Details modal, which shows and allows editing of:
- Payment Date
- Check / Ref #
- Amount Received
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 #.
- Click + Add Payment to record a new payment
- If the amount matches an unpaid invoice for that proposal, the invoice is automatically marked paid
- Payments recorded here feed into the Commissions PDF report
Reports
- 📄 PDF Report — date-range report of all payments received (from paid invoices + ad-hoc recorded payments), with deduplication to avoid double-counting. Set the From/To month range before generating.
- 📊 Excel Report — same data as PDF Report in
.xlsxformat: Company, Invoice #, Invoice Date, Payment Date, Invoice Total, Amount Paid. Filter by month range. - 📄 Commissions — PDF commission report from ad-hoc recorded payments, grouped by presenter with commission calculations. Filter by month using the All Months dropdown.
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
- Status tracking — per-item status selector (Not Started, In Progress, Scheduled, Completed, On Hold, Cancelled, Not Using). Status column is 160px wide to accommodate labels.
- Owner assignment — assign a team member per deliverable row
- Date range — start/end date fields per deliverable
- Notes sub-row — a slim inline notes field sits below each deliverable row; hidden in print unless it has content
- Month pills — for month-based services, click the calendar icon to assign scheduled months. Month-qty items (e.g. Topic Portals selected for 3 months) display as a single row labeled "Portal Name — 3 months" rather than three separate rows.
- Group header styling — when a proposal group is expanded, the header bar turns dark navy with white text so it visually anchors the open panel
- Owner filter — filter the board by assigned owner using the toolbar dropdown
- Accepted-only toggle — show only accepted proposals
- Add to Calendar (📅) — each deliverable row has a calendar checkbox in the last column. Check it to flag the deliverable for calendar export. The default checked/unchecked state per catalog item type is stored in settings — items with a date range and a checked box can be bulk-exported to a calendar file.
- Print — print-friendly layout; empty notes rows are hidden automatically
Advertising Items Excluded
The following advertising-only catalog sections are intentionally excluded from the Projects board — they are tracked on the Advertising page instead:
- Newsletter Banner Ads
- Website Banner Ads
- Dedicated Email Promotions
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):
- Dedicated Email Promotions
- Website Banner Ads
- Newsletters
- 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:
| Icon | Action |
|---|---|
| 🏢 | 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
- First Name, Last Name (required)
- Email, Phone
- Company (required), Job Title
- Publication of Interest — CVB / RB / HE (required)
- How Can We Help? — interest selector (required)
- Conferences of Interest — checkboxes that animate in based on the selected publication
- Additional Comments
Behaviour
- POSTs to
/.netlify/functions/contact-form - On success: form is replaced with a thank-you message inside the iframe (no page redirect)
- On error: toast notification appears inside the iframe
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.
- Sections — top-level groupings (e.g. "Digital Advertising")
- Items — individual services with name, description, price, and enabled/disabled toggle per brand
- Item types: checkbox, quantity, monthly, multi-instance
- Click Save to write to Supabase and localStorage
- 🖨 Print Proof — opens a print-ready view of all brands + Billing Options, Terms, Pricing Validity, Benefits, and Invoicing. Includes Export to Excel.
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.
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
| Setting | Description |
|---|---|
| Audience Drop-Down Options | Custom audience options in the proposal builder's audience selector |
| Brand Colors | Hex color values for CVB, RB, HE — applied to proposal PDFs and UI accents |
| Whitepaper Defaults | Default whitepaper rate used in proposals |
| Benefits of Partnership | Bullet list of partnership benefits shown in proposals |
| Pricing Validity | Bullet list of pricing validity statements |
| Invoicing | Bullet list of invoicing terms shown in proposals |
| Project Owners | People who can be assigned as owners on Project Detail bullets |
| Proposal Statuses | Custom status labels for the proposal lifecycle |
| Project Status Options | Custom status labels for project tracking |
| User Groups | Which users belong to which access groups (Sales, Finance, Operations, Admin) |
| Billing | Billing options with ID, label, and line items |
| Users / Presenters | Sales 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:
| Column | Description |
|---|---|
| Name | Newsletter issue name or title |
| Owner | Team member responsible for this issue |
| Due Date | Issue due date — used by the Newsletter Reminder function to determine which reminders to send |
| Send Date | Scheduled send date |
| Frequency | Daily / Weekly / Monthly |
| Status | Draft / Scheduled / Sent |
- Click + Add Newsletter under any brand to add a new row
- Click Save Newsletters to write all changes to Supabase
- 🖨 Print — opens a print-ready Newsletter Schedule view for all brands
- Export to Excel — downloads
IH-Newsletters.xlsx
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:
- Mailchimp — API Key — stored as
mailchimp_api_keyin the Supabasesettingstable - Mailchimp — Data Center — stored as
mailchimp_dc(e.g.us21)
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 Selected | Notification | Supabase Contact | Mailchimp |
|---|---|---|---|
| Media Kit / Advertising Information | Sales@ | ✅ Created | IH Master List (interest groups + automation tag) + Brand list (interest groups) |
| Custom Marketing Programs / Webinars | Sales@ | ✅ Created | IH Master List (interest groups + automation tag) + Brand list (interest groups) |
| Publishing a Press Release | ManagingEditor@ | ✅ Created | IH Master List (interest groups, no automation tag) |
| Billing or Accounting Issue | Accounting@ | ❌ Skipped | None |
| Editorial Correction or Inquiry | ManagingEditor@ | ❌ Skipped | None |
| Submit a News Tip | ManagingEditor@ | ❌ Skipped | None |
| Other | ClientServices@ | ❌ Skipped | None |
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:
- CVB — Cardiovascular Business interest group
- RB — Radiology Business interest group
- HE — HealthExec interest group
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).
4.7 Data & Backup Management
| Button | What it does |
|---|---|
| ⬆ Sync to Cloud | Pushes all localStorage settings to Supabase (use after offline edits) |
| ⬇ Export Backup | Downloads a complete .json snapshot to your computer |
| ↑ Restore from File | Restores from a .json file on your computer |
| ▶ Run Full Backup Now | Triggers the full scheduled backup manually (JSON + PDFs to S3 + email) |
| 📄 Export Data → S3 | Uploads a JSON-only snapshot to S3 (no PDFs, faster) |
| 📑 Backup PDFs → S3 | Archives all proposal PDFs to S3 |
| ☁ Restore from S3 | Browse 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:
- Keeps the keeper's field values; fills any blanks from the duplicate records
- Unions all tags from all records
- Re-points any proposals whose
clientEmailmatches a duplicate → keeper's email - Deletes the duplicate records
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:
- Exact — case-insensitive name match
- Fuzzy — strips parentheticals (
(CT),(Cardiology)) and legal suffixes (Inc.,LLC, etc.) then matches - Slash/Pipe — takes the first segment of
Philips / Biotel→ triesPhilips - Prefix drop — progressively removes trailing words to catch sub-brand names like
Fujifilm Medical Systems USA→Fujifilm
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.
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.
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:
- Existing company (matched by name): blank fields are filled; existing values are never overwritten; tags are unioned
- New name: a new company record is created
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:
- Email match (case-insensitive)
- 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
| Table | Purpose |
|---|---|
proposals | All proposal records (JSONB data blob + metadata) |
contacts | Contact database |
companies | Company database |
users | Presenters / sales staff list |
settings | Key-value store for app settings (catalogs, billing, terms, etc.) |
catalog_sections | Rate Card sections — one row per section per brand (CVB/RB/HE) |
catalog_items | Rate Card items — linked to catalog_sections by section_id |
sow_bullets | SOW bullet points — linked to catalog_sections by section_id |
project_detail_bullets | Project Details bullets — linked to catalog_sections by catalog_section_id |
invoices | Invoice records |
Key Relationships
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
| Data | Tables / Source |
|---|---|
| Proposals | proposals |
| Contacts | contacts |
| Companies | companies |
| Presenters | users |
| App settings | settings |
| Rate Card | catalog_sections, catalog_items |
| SOW bullets | sow_bullets |
| Project Detail bullets | project_detail_bullets |
| Archived PDFs | Downloaded 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.
6.3 Manual Backup — Run Full Backup Now
Use after major data changes (recovery operation, bulk import, schema change).
- Go to Admin → Data & Backup Management
- Click ▶ Run Full Backup Now
- If prompted for the Backup Secret, enter the value from Netlify env vars (
BACKUP_SECRET) — saved in your browser after first entry - Wait for the success toast (typically 15–30 seconds)
- 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).
- Go to Admin → Data & Backup Management
- Click 📄 Export Data → S3
- 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.
- Go to Admin → Data & Backup Management
- Click ⬇ Export Backup
- File downloads immediately as
proposal-builder-backup-YYYY-MM-DD.json
6.6 Restore from S3
Use to recover from data loss or roll back to an earlier state.
- First, run ⬇ Export Backup (desktop) to save a current snapshot as a safety net
- Go to Admin → Data & Backup Management
- Click ☁ Restore from S3
- The modal lists all S3 backups, newest first, with date, size, and timestamp
- Click the backup you want to restore from
- Read the confirmation summary carefully — it shows proposal/contact/section counts
- Click Restore to confirm
- Wait for the log to complete (typically 10–30 seconds)
- Verify: check Rate Card, SOW, Project Details, and a sample proposal
What Restore Does
| Data | Behavior |
|---|---|
| Proposals, Contacts, Companies | Upserted — existing data is merged, nothing deleted |
| Users / Presenters | Fully replaced |
| Settings | Upserted by key |
| Relational tables (catalog, SOW, project detail bullets) | Fully replaced — delete all → re-insert from backup |
6.7 Restore from File (Desktop)
- Go to Admin → Data & Backup Management
- Click ↑ Restore from File
- Select your
.jsonbackup file - Review the confirmation summary and click Restore
Behavior is identical to Restore from S3 — same merge/replace logic.
6.8 Backup PDFs Only
- Go to Admin → Data & Backup Management
- Click 📑 Backup PDFs → S3
- Wait for the toast — shows PDFs backed up and skipped (no archived PDF = skipped)
6.9 S3 Backup File Structure
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.
- Export to Desktop — download a local
.jsonsnapshot - Export Data → S3 — upload an immediate cloud snapshot
- Verify the toast shows expected counts (proposals, contacts, sections)
- If PDFs were involved — run Backup PDFs → S3
- Optionally run a restore test on a non-production day
7. Environment Variables Reference
Netlify Environment Variables
Set in Netlify → Site configuration → Environment variables.
| Variable | Required | Description |
|---|---|---|
RESEND_API_KEY | ✅ | Resend email API key |
SUPABASE_URL | ✅ | Supabase project URL (for backup function) |
SUPABASE_SERVICE_ROLE_KEY | ✅ | Service role key — bypasses RLS for backup |
SUPABASE_ANON_KEY | Fallback | Anon key — used if service role key not set |
AWS_S3_BUCKET | ✅ | S3 bucket name |
AWS_S3_REGION | ✅ | S3 region (e.g. us-east-1) |
S3_ACCESS_KEY_ID | ✅ | AWS access key ID |
S3_SECRET_ACCESS_KEY | ✅ | AWS secret access key |
BACKUP_SECRET | ✅ | Random string — authenticates manual backup triggers from browser |
BACKUP_NOTIFY_EMAIL | ✅ | Email address for backup success/failure notifications |
MAILCHIMP_API_KEY | Contact form | Mailchimp API key — fallback if not stored in Supabase settings. Prefer setting via Admin → API Keys in the app. |
MAILCHIMP_DC | Contact form | Mailchimp data center prefix (e.g. us1, us21) — fallback if not stored in settings |
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.
| Variable | Location | Description |
|---|---|---|
SUPABASE_URL | db.js lines 4–5 | Supabase project URL |
SUPABASE_KEY | db.js lines 4–5 | Supabase anon/public key |
8. Access & User Groups
| Group | Pages |
|---|---|
| Sales | Home, Builder, Proposals, Contacts, Prospects, Companies, Contact, Company, Lead |
| Finance | Home, Proposals, Invoices, Payments, Orders |
| Operations | Home, Proposals, Projects, Calendar, Webinars |
| Admin | All pages |
Manage access at Admin → Settings → User Groups. Changes take effect on the user's next login.
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
- Edit files locally in the
proposal-builderfolder - Test in the browser
- Commit:
git add <files> && git commit -m "Description" - 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.
10.1 Prerequisites
| Service | What you need |
|---|---|
| GitHub | Account with permission to create repos |
| Netlify | Account (free tier is sufficient) |
| Supabase | Account — create a new project |
| Resend | Account with a verified sending domain |
| AWS | IAM 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
- Go to app.supabase.com → New project
- Wait for provisioning, then go to Project Settings → API and note the Project URL, anon key, and service_role key
- 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
- Authentication → Providers — ensure Email is enabled
- Authentication → Users → Invite user — invite each user by email
- Storage → New bucket — name:
proposals, set to Public
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
- Go to resend.com → Domains → Add Domain
- Add DNS records (SPF, DKIM, DMARC) — wait for verification
- Go to API Keys → Create API Key — copy the key (
re_...) - 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
- S3 → Create bucket — block all public access ON (access is via signed URLs)
- IAM → Users → Create user — attach
AmazonS3FullAccessor a scoped bucket policy - Security credentials → Create access key — copy Access Key ID and Secret
10.7 Step 6 — Create & Configure Netlify Site
- app.netlify.com → Add new site → Import an existing project
- Connect GitHub → select repo. Build settings auto-detected from
netlify.toml. - Click Deploy site
- Go to Site configuration → Environment variables and add all variables:
| Variable | Value |
|---|---|
RESEND_API_KEY | From Resend API Keys |
SUPABASE_URL | Your Supabase project URL |
SUPABASE_SERVICE_ROLE_KEY | Supabase → Project Settings → API → service_role |
SUPABASE_ANON_KEY | Supabase → Project Settings → API → anon/public |
AWS_S3_BUCKET | Your S3 bucket name |
AWS_S3_REGION | e.g. us-east-1 |
S3_ACCESS_KEY_ID | From AWS IAM user |
S3_SECRET_ACCESS_KEY | From AWS IAM user |
BACKUP_SECRET | Any random string (e.g. openssl rand -hex 24) |
BACKUP_NOTIFY_EMAIL | Email 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
- Login page loads at the site URL
- Can log in with a test user account
- Redirected to Home after login
Rate Card & Catalog
- Admin → Rate Card shows all sections and items with prices
- Admin → SOW shows bullets
- Admin → Project Details shows bullets
Proposal Builder
- Can create a new proposal
- Rate Card items selectable, investment summary calculates correctly
- Can save a proposal — appears in Proposals list
- Send Proposal — PDF email arrives at client address
- Send to Accounting — accounting email arrives
Backup
- Admin → ▶ Run Full Backup Now completes successfully
- Confirmation email received at BACKUP_NOTIFY_EMAIL
- Admin → ☁ Restore from S3 — backup appears in the list
Projects
- Projects board loads
- Deliverables panel shows bullets for accepted proposals
10.10 Migrating from the Existing Environment
- Run a full backup on the old site — Admin → ▶ Run Full Backup Now
- Export to desktop — Admin → ⬇ Export Backup — save a local
.json - Stand up the new environment following Steps 1–7 above
- Restore the backup on the new site — Admin → ↑ Restore from File
- Re-invite all users via Supabase → Authentication → Users (auth users do not transfer between Supabase projects)
- Verify the checklist in Step 8
- Update DNS to point to the new Netlify site
Appendix — Quick Reference Card
Daily / Weekly Tasks
| Task | Where |
|---|---|
| Check last backup ran | Admin → Data & Backup Management → "Last backup" banner |
| Run an on-demand backup | Admin → ▶ Run Full Backup Now |
| Edit Rate Card pricing | Admin → Rate Card |
| Edit SOW bullets | Admin → SOW |
| Edit Project Details | Admin → Project Details |
| Add a new user | Admin → Settings → User Groups |
If Something Goes Wrong
| Problem | First Step |
|---|---|
| Data appears missing or wrong | Export current state first, then assess |
| Rate Card / SOW / Project Details blank | Check Supabase → catalog_sections table has rows |
| Backup failed | Check Netlify → Functions → scheduled-backup → Logs |
| Can't log in | Check Supabase → Authentication → Users |
| PDF email not sending | Check Netlify → Functions → send-proposal → Logs; verify RESEND_API_KEY |
| Projects board shows no bullets | Check project_detail_bullets table has rows with catalog_section_id set |
| Payments access code email not arriving | Check 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 rows | Ensure invoices are marked Paid (not just payment-date entered). Filter by the correct month first. |
| Topic Portals showing multiple rows on Projects | Expected 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 contact | Check 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 applied | Check 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 duplicates | Caused 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 Contacts | Contact 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. |