Multi-tenant estimate and lead capture system for service businesses.
- TypeScript backend with Express
- PostgreSQL
- Reusable widget
- Vite demo-site
- Vite portal-site
- Resend-backed lead notification emails
backend/- API, application layer, domain, and persistencewidget/- embeddable estimate widgetdemo-site/- public-only demo and showcase host for the widgetportal-site/- authenticated client portal frontenddocs/- milestone notes and implementation documentationrender.yaml- Render backend deployment configvercel.json- Vercel demo-site deployment config
The backend now has structured observability and operational health probes without changing existing API contracts.
- Added JSON structured logging across backend request handling and runtime events.
- Added request ID middleware with
X-Request-Idresponse headers. - Added request start/end logging with status code and duration for all routes.
- Added business event logs for login, lead creation, pricing config version creation/activation, and lead email delivery outcomes.
- Added
/health,/health/db, and/health/emailendpoints for operational diagnostics. - Added a centralized error handler with structured error logging.
Milestone notes:
docs/milestones/observability-and-operational-hardening.mddocs/milestones/backend-critical-flow-tests.mddocs/milestones/http-only-cookie-auth.mddocs/milestones/portal-site-split.mddocs/milestones/pricing-config-versioning.mddocs/milestones/client-settings-onboarding.mddocs/milestones/client-dashboard-auth-v1.mddocs/milestones/lead-email-notifications.md
Backend:
cd backend
$env:DATABASE_URL="postgresql://postgres:postgres@localhost:5434/estimate_engine"
$env:WIDGET_ORIGIN="http://localhost:4173"
$env:PORTAL_ORIGIN="http://localhost:4174"
$env:RESEND_API_KEY="re_xxxxx"
$env:LEAD_NOTIFICATION_FROM_EMAIL="Estimate Engine <alerts@example.com>"
$env:CLIENT_PORTAL_SESSION_TTL_HOURS="168"
$env:CLIENT_PORTAL_COOKIE_SECURE="false"
$env:CLIENT_PORTAL_COOKIE_SAME_SITE="lax"
npm install
npm run devDemo-site:
cd demo-site
$env:VITE_API_BASE_URL="http://localhost:3000"
$env:VITE_CLIENT_ID="demo"
npm install
npm run devPortal-site:
cd portal-site
$env:VITE_API_BASE_URL="http://localhost:3000"
$env:VITE_DEFAULT_CLIENT_ID="demo"
$env:VITE_PORTAL_TITLE="Estimate Engine Client Portal"
npm install
npm run devThe backend is configured for Render with the repo-managed render.yaml file.
Render service settings from render.yaml:
rootDir: backendbuildCommand: npm install --include=dev && npm run buildstartCommand: npm run start
Required environment variables:
DATABASE_URLWIDGET_ORIGINPORTAL_ORIGIN
Recommended environment variables:
NODE_ENV=productionPORTPGSSLMODE=requireif your hosted PostgreSQL provider requires SSLRESEND_API_KEYto enable lead notification emailsLEAD_NOTIFICATION_FROM_EMAILfor the sender identity used by ResendLEAD_NOTIFICATION_TIMEOUT_MS=5000CLIENT_PORTAL_SESSION_TTL_HOURS=168CLIENT_PORTAL_COOKIE_SECURE=trueCLIENT_PORTAL_COOKIE_SAME_SITE=laxfor same-site deployments, ornonefor cross-site frontend/backend deploymentsCLIENT_PORTAL_COOKIE_NAME=estimate_engine_portal_sessionCLIENT_PORTAL_DEMO_RESET_CLIENT_ID=demoto restrict the demo reset action to the shared demo tenantCLIENT_PORTAL_DEMO_RESET_COMPANY_NAME,CLIENT_PORTAL_DEMO_RESET_PHONE,CLIENT_PORTAL_DEMO_RESET_NOTIFICATION_EMAIL, andCLIENT_PORTAL_DEMO_RESET_LOGO_URLif you want custom demo reset defaultsCLIENT_PORTAL_DEMO_RESET_ESTIMATOR_CONFIGwith a JSON object for the demo tenant pricing baseline
Example values:
DATABASE_URL=postgresql://USER:PASSWORD@HOST:5432/DBNAMEWIDGET_ORIGIN=https://your-demo-site.vercel.appPORTAL_ORIGIN=https://your-portal-site.vercel.app
Create the Render service:
- Push this repo to GitHub.
- In Render, choose
New +->Blueprint. - Connect the repo and deploy the root
render.yaml. - Enter values for
DATABASE_URL,WIDGET_ORIGIN, andPORTAL_ORIGINwhen prompted.
Build verification command:
cd backend
npm install
npm run build
npm testHosted database migration command:
psql "postgresql://USER:PASSWORD@HOST:5432/DBNAME" -f backend/db/migrations/001_initial.sql -f backend/db/migrations/002_lead_notifications.sql -f backend/db/migrations/003_client_portal_auth.sql -f backend/db/migrations/004_client_settings_onboarding.sql -f backend/db/migrations/005_config_versioning_and_audit.sqlIncremental migration for an existing deployed database:
psql "postgresql://USER:PASSWORD@HOST:5432/DBNAME" -f backend/db/migrations/002_lead_notifications.sql
psql "postgresql://USER:PASSWORD@HOST:5432/DBNAME" -f backend/db/migrations/003_client_portal_auth.sql
psql "postgresql://USER:PASSWORD@HOST:5432/DBNAME" -f backend/db/migrations/004_client_settings_onboarding.sql
psql "postgresql://USER:PASSWORD@HOST:5432/DBNAME" -f backend/db/migrations/005_config_versioning_and_audit.sqlConfigure a tenant notification recipient:
psql "postgresql://USER:PASSWORD@HOST:5432/DBNAME" -c "UPDATE clients SET notification_email = 'owner@example.com' WHERE name = 'demo';"Bootstrap a client portal user:
cd backend
$env:DATABASE_URL="postgresql://USER:PASSWORD@HOST:5432/DBNAME"
$env:CLIENT_ID="demo"
$env:CLIENT_USER_EMAIL="owner@example.com"
$env:CLIENT_USER_FULL_NAME="Demo Owner"
$env:CLIENT_USER_PASSWORD="change-me-123"
npm run create:client-userThe demo-site is configured for Vercel with the repo-managed vercel.json file.
Vercel config from vercel.json:
- installs dependencies from
demo-site/ - builds
demo-site/ - serves
demo-site/dist
This app is intentionally public-only and should be used as the sales/demo surface plus sample widget host.
Required Vercel environment variables:
VITE_API_BASE_URL
Recommended Vercel environment variables:
VITE_CLIENT_ID=demoVITE_LAUNCHER_LABEL=Launch Demo WidgetVITE_MODAL_TITLE=Owned Estimate Demo
Example production value:
VITE_API_BASE_URL=https://your-backend.onrender.com
Deploy with the Vercel CLI:
npm install -g vercel
vercel link
vercel env add VITE_API_BASE_URL production
vercel env add VITE_CLIENT_ID production
vercel env add VITE_LAUNCHER_LABEL production
vercel env add VITE_MODAL_TITLE production
vercel --prodLocal production-style build verification:
cd demo-site
$env:VITE_API_BASE_URL="http://localhost:3000"
$env:VITE_CLIENT_ID="demo"
npm install
npm run buildDeploy portal-site/ as a separate frontend project.
Suggested project settings:
- root directory:
portal-site - install command:
npm install - build command:
npm run build - output directory:
dist
Required environment variables:
VITE_API_BASE_URL
Recommended environment variables:
VITE_DEFAULT_CLIENT_ID=demoVITE_PORTAL_TITLE=Estimate Engine Client Portal
Local production-style build verification:
cd portal-site
$env:VITE_API_BASE_URL="http://localhost:3000"
$env:VITE_DEFAULT_CLIENT_ID="demo"
$env:VITE_PORTAL_TITLE="Estimate Engine Client Portal"
npm install
npm run buildAfter both deployments are live:
- Open the deployed demo-site URL.
- Confirm only the public estimator/demo content is shown there.
- Confirm the launcher button is visible.
- Open the widget.
- Confirm the estimate form loads without errors.
- Submit an estimate and confirm the result renders.
- Continue to the lead form and submit a lead.
- Confirm the success state renders.
- Confirm the browser can call
GET /client-config?clientId=demosuccessfully. - Confirm the new lead exists in PostgreSQL.
- Confirm the configured client inbox receives the new lead notification email.
- Open the deployed portal-site URL and sign in there.
- Confirm the new lead appears in the dashboard.
- Update company settings in the portal and confirm the estimator still works with the same tenant slug.
- Change the pricing config JSON, save it, and confirm the portal shows a new active config version plus history entry.
- Submit another lead and confirm PostgreSQL stores the newer
config_version_id. - Refresh the portal-site and confirm the session is still resolved from the HttpOnly cookie via
GET /auth/me. - Sign out and confirm the portal returns to the login screen and
POST /auth/logoutclears the cookie-backed session.
Example PostgreSQL verification command:
psql "postgresql://USER:PASSWORD@HOST:5432/DBNAME" -c "SELECT leads.id, leads.email, leads.config_version_id, client_config_versions.version_number, leads.estimate_data->>'total' AS total, leads.created_at FROM leads JOIN client_config_versions ON client_config_versions.id = leads.config_version_id ORDER BY leads.id DESC LIMIT 10;"Authenticated dashboard endpoints:
POST /auth/loginGET /auth/mePOST /auth/logoutGET /me/leads?limit=25GET /portal/clientPUT /portal/clientPOST /portal/demo/resetfor restoring the shared demo tenant to a clean default state
Pricing versioning notes:
GET /client-config?clientId=...now resolves the active immutable config version for the tenant.POST /estimateresponses includeconfigVersion.idandconfigVersion.versionNumber.POST /leadspersists the config version used for the estimate.
Operational endpoints:
GET /healthreturns process-level liveness dataGET /health/dbchecks PostgreSQL connectivityGET /health/emailreports whether lead email delivery is configured
Operational notes:
/health/emailreturns503whenRESEND_API_KEYorLEAD_NOTIFICATION_FROM_EMAILis missing./health/dbreturns503when the backend cannot complete a database probe.- Existing product API contracts are unchanged; these are additive endpoints for diagnostics.
Run the backend test suite:
cd backend
npm install
npm testCurrent automated coverage:
POST /auth/login,GET /auth/me, andPOST /auth/logout- unauthenticated rejection for protected portal routes
- pricing config version creation rules
- unchanged pricing saves not creating a new config version
- changed pricing saves creating a new active config version
- lead creation storing
config_version_id - tenant isolation for leads and client settings
Test design notes:
- The suite uses the real SQL migration files from
backend/db/migrations/. - Tests run against a deterministic pg-compatible in-memory database via
pg-mem. - The backend app is instantiated per test, and test data is reseeded each time to keep flows isolated and repeatable.
Backend logging now emits structured JSON lines.
Included fields:
timestampleveleventrequestId- request method/path metadata where available
Request lifecycle coverage:
- every request logs
request_started - every request logs
request_completedwith status code and duration - application errors log through the centralized error handler
Key business events:
portal_login_succeededlead_createdconfig_version_createdconfig_version_activatedlead_notification_sentlead_notification_failed
Local development defaults:
PORTAL_ORIGIN=http://localhost:4174CLIENT_PORTAL_COOKIE_SECURE=falseCLIENT_PORTAL_COOKIE_SAME_SITE=lax
Production guidance:
- Use
HttpOnlycookies withSecure=true. - Use
CLIENT_PORTAL_COOKIE_SAME_SITE=laxwhen backend and portal share the same site. - Use
CLIENT_PORTAL_COOKIE_SAME_SITE=nonewhen backend and portal are on different sites and credentialed cross-origin requests are required. PORTAL_ORIGINmust be the exact portal frontend origin because credentialed CORS cannot use*.