This guide covers deploying the HubSpot Form Builder for production use, including environment configuration, security best practices, and advanced testing setups.
Development vs Production
Understand the key differences between development and production environments:
Aspect Development Production URLs http://localhost:3001https://your-domain.comCORS Permissive (localhost + Cloudflare) Restricted to specific domains OAuth Redirect http://localhost:3001/oauth/...https://your-domain.com/oauth/...HTTPS Optional Required Environment Variables .env files in repoSecure environment config Error Logging Console logs Production logging service Token Storage In-memory (session-based) Persistent database
Production Environment Setup
Prepare your environment for production deployment.
1. Environment Variables
Update your .env files for production:
Backend (server/.env)
# Server Configuration
PORT=3001
NODE_ENV=production
# HubSpot OAuth
HUBSPOT_CLIENT_ID=your-production-client-id
HUBSPOT_CLIENT_SECRET=your-production-client-secret
HUBSPOT_REDIRECT_URI=https://your-domain.com/oauth/hubspot/callback
HUBSPOT_SCOPES=forms content forms-uploaded-files
# Frontend URL (for CORS)
FRONTEND_URL=https://your-domain.com
# Security
SESSION_SECRET=generate-a-secure-random-string
Never commit production .env files to version control. Use environment variables in your hosting platform instead.
Frontend (frontend/.env.production)
# API Configuration
VITE_API_BASE=https://your-domain.com
Build command :
Vite automatically uses .env.production during build.
2. CORS Configuration
Update CORS settings for production domains:
Current development config (server/src/index.ts:10-24):
app . use (
cors ({
origin : ( origin , callback ) => {
const allowedOrigins = [ 'http://localhost:5173' ];
if ( ! origin || allowedOrigins . includes ( origin ) || origin . endsWith ( '.trycloudflare.com' )) {
callback ( null , true );
} else {
callback ( new Error ( 'Not allowed by CORS' ));
}
},
credentials: true ,
}),
);
Production config :
app . use (
cors ({
origin : ( origin , callback ) => {
const allowedOrigins = [
process . env . FRONTEND_URL ,
'https://your-domain.com' ,
'https://www.your-domain.com' ,
]. filter ( Boolean );
if ( allowedOrigins . includes ( origin )) {
callback ( null , true );
} else {
callback ( new Error ( 'Not allowed by CORS' ));
}
},
credentials: true ,
}),
);
Remove the .trycloudflare.com wildcard in production for security.
3. HubSpot OAuth App Configuration
Update your HubSpot OAuth app for production:
Navigate to OAuth Apps
Go to Settings → Integrations → Private Apps in your HubSpot portal.
Create Production App
Create a new OAuth app for production (keep development app separate). Name : “Form Builder (Production)”
Configure Redirect URI
Set the redirect URI to: https://your-domain.com/oauth/hubspot/callback
Must match exactly with HUBSPOT_REDIRECT_URI in your backend .env.
Set Scopes
Required scopes:
forms - Read HubSpot forms
content - For CMS module deployment
forms-uploaded-files - For file uploads in forms (if needed)
Copy Credentials
Copy the Client ID and Client Secret to your production environment variables. Store these securely - never commit to version control.
Hosting Options
Choose a hosting platform for your Form Builder:
Option 1: Vercel (Recommended)
Pros :
Simple deployment from GitHub
Automatic HTTPS
Environment variable management
Serverless functions for backend
Setup :
# Install Vercel CLI
npm i -g vercel
# Deploy
vercel --prod
vercel.json :
{
"builds" : [
{ "src" : "main/frontend/package.json" , "use" : "@vercel/static-build" },
{ "src" : "main/server/src/index.ts" , "use" : "@vercel/node" }
],
"routes" : [
{ "src" : "/oauth/(.*)" , "dest" : "main/server/src/index.ts" },
{ "src" : "/api/(.*)" , "dest" : "main/server/src/index.ts" },
{ "src" : "/(.*)" , "dest" : "main/frontend/dist/$1" }
]
}
Option 2: AWS (EC2 + S3)
Backend : Deploy to EC2 instance
Use PM2 to keep Node.js running
Configure NGINX as reverse proxy
Enable HTTPS with Let’s Encrypt
Frontend : Deploy to S3 + CloudFront
Build with npm run build
Upload dist/ folder to S3
Configure CloudFront for CDN
Pros :
Simple configuration
Automatic deployments from Git
Built-in database options (for future token persistence)
App spec :
name : hubspot-form-builder
services :
- name : backend
environment_slug : node-js
github :
repo : your-username/hubspot-form-builder
branch : main
deploy_on_push : true
source_dir : /main/server
envs :
- key : HUBSPOT_CLIENT_ID
value : ${HUBSPOT_CLIENT_ID}
http_port : 3001
- name : frontend
environment_slug : node-js
build_command : npm run build
source_dir : /main/frontend
envs :
- key : VITE_API_BASE
value : ${BACKEND_URL}
static_sites :
- name : app
build_output_dir : dist
Testing on Multiple Devices with Cloudflare Tunnels
For development and QA testing across devices, use Cloudflare Tunnels.
Why Use Cloudflare Tunnels?
Test on mobile : Access your local dev server from your phone
Test on tablets : Check responsive layouts on iPads
Share with team : Let colleagues test without deploying
Free : No cost for temporary tunnels
Setup Cloudflare Tunnels
Install Cloudflared
macOS :brew install cloudflare/cloudflare/cloudflared
Windows :
Download from Cloudflare Downloads Linux :wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared-linux-amd64.deb
Start Backend Server
cd main/server
npm run dev
# Backend running on http://localhost:3001
Tunnel Backend
Open a new terminal: cloudflared tunnel --url http://localhost:3001
Output :INF +--------------------------------------------------------------------------------------------+
INF | Your quick Tunnel has been created! Visit it at (it may take some time to be reachable): |
INF | https://backend-xyz123.trycloudflare.com |
INF +--------------------------------------------------------------------------------------------+
Copy this URL - this is your backend tunnel.
Update Frontend .env
Edit main/frontend/.env: VITE_API_BASE=https://backend-xyz123.trycloudflare.com
Start Frontend Server
cd main/frontend
npm run dev
# Frontend running on http://localhost:5173
Tunnel Frontend
Open another new terminal: cloudflared tunnel --url http://localhost:5173
Output :INF | https://frontend-abc789.trycloudflare.com |
This is your public URL - accessible from any device.
Update HubSpot Redirect URI
In HubSpot OAuth app settings: Redirect URI: https://backend-xyz123.trycloudflare.com/oauth/hubspot/callback
Also update server/.env: HUBSPOT_REDIRECT_URI=https://backend-xyz123.trycloudflare.com/oauth/hubspot/callback
Test on Other Devices
Open https://frontend-abc789.trycloudflare.com on:
Your phone
Tablet
Colleague’s computer
Everything works as if it’s deployed!
Important Notes About Tunnels
Tunnel URLs change each time you run cloudflared tunnel. You’ll need to update .env and HubSpot settings each session.
For persistent tunnels with stable URLs, create a named tunnel: cloudflared tunnel create form-builder
cloudflared tunnel route dns form-builder backend.your-domain.com
cloudflared tunnel run form-builder
See Cloudflare Docs for details.
Switching Between Localhost and Tunnels
To revert to localhost :
Stop Cloudflared processes
Update frontend/.env:
VITE_API_BASE=http://localhost:3001
Update server/.env:
HUBSPOT_REDIRECT_URI=http://localhost:3001/oauth/hubspot/callback
Restart dev servers
See the Cloudflare configuration doc for a detailed guide.
Security Best Practices
1. Keep Secrets Secure
Use Environment Variables
Never hardcode:
HubSpot Client ID/Secret
OAuth tokens
API keys
Database credentials
Use :
.env files (local dev, gitignored)
Hosting platform environment variables (production)
Secret management services (AWS Secrets Manager, etc.)
Rotate Credentials Regularly
Change HubSpot Client Secret every 90 days
Regenerate OAuth tokens on security incidents
Use different credentials for dev/staging/production
Only allow specific domains: const allowedOrigins = [
'https://your-domain.com' ,
'https://www.your-domain.com' ,
];
Never use origin: '*' in production.
2. Token Storage
Current implementation (server/src/oauth.ts):
Tokens stored in-memory on the server
Lost on server restart
Single-user session only
Production recommendation :
Store tokens in a database (PostgreSQL, MongoDB, Redis)
Associate tokens with user sessions
Implement token refresh logic
Encrypt tokens at rest
Example upgrade :
// Token storage with database
import { db } from './database' ;
type TokenStore = {
userId : string ;
accessToken : string ;
refreshToken : string ;
expiresAt : Date ;
};
async function storeToken ( userId : string , tokenData : TokenStore ) {
await db . tokens . upsert ({
where: { userId },
data: tokenData ,
});
}
async function getToken ( userId : string ) {
const token = await db . tokens . findUnique ({ where: { userId } });
// Refresh if expired
if ( token && token . expiresAt < new Date ()) {
return await refreshToken ( token . refreshToken );
}
return token ;
}
3. HTTPS Configuration
Always use HTTPS in production :
Protects OAuth tokens in transit
Required by HubSpot for OAuth redirects
Prevents man-in-the-middle attacks
Setup with Let’s Encrypt (if self-hosting):
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d your-domain.com
Hosting platforms (Vercel, Netlify, etc.) provide HTTPS automatically.
4. Rate Limiting
Protect your API from abuse:
import rateLimit from 'express-rate-limit' ;
const limiter = rateLimit ({
windowMs: 15 * 60 * 1000 , // 15 minutes
max: 100 , // Limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again later.' ,
});
app . use ( '/api/' , limiter );
app . use ( '/oauth/' , limiter );
Validate all inputs on the backend:
import { z } from 'zod' ;
const formSchemaValidator = z . object ({
id: z . string (). uuid (),
name: z . string (). min ( 1 ). max ( 255 ),
fields: z . array ( z . object ({
name: z . string (),
type: z . string (),
required: z . boolean (),
})),
});
app . post ( '/api/generate' , ( req , res ) => {
try {
const validated = formSchemaValidator . parse ( req . body );
// Proceed with validated data
} catch ( error ) {
res . status ( 400 ). json ({ error: 'Invalid input' });
}
});
Monitoring and Logging
Application Logging
Implement structured logging:
import winston from 'winston' ;
const logger = winston . createLogger ({
level: 'info' ,
format: winston . format . json (),
transports: [
new winston . transports . File ({ filename: 'error.log' , level: 'error' }),
new winston . transports . File ({ filename: 'combined.log' }),
],
});
if ( process . env . NODE_ENV !== 'production' ) {
logger . add ( new winston . transports . Console ({
format: winston . format . simple (),
}));
}
// Use throughout app
logger . info ( 'Form schema fetched' , { formId , userId });
logger . error ( 'OAuth error' , { error: err . message });
Error Tracking
Integrate error tracking service:
Sentry :
import * as Sentry from '@sentry/node' ;
Sentry . init ({
dsn: process . env . SENTRY_DSN ,
environment: process . env . NODE_ENV ,
});
app . use ( Sentry . Handlers . errorHandler ());
LogRocket (for frontend):
import LogRocket from 'logrocket' ;
LogRocket . init ( 'your-app-id' );
Future: HubSpot CLI Integration
The project roadmap includes direct HubSpot CLI integration for automated deployment.
Planned Feature
Instead of manually uploading modules:
# Future CLI command
npm run deploy:hubspot
This would:
Generate the module files
Authenticate with HubSpot CLI
Upload to Design Manager automatically
Watch for changes and auto-deploy
HubSpot CLI Setup (Manual Alternative)
You can already use HubSpot CLI manually:
Install HubSpot CLI
npm install -g @hubspot/cli
Authenticate
Follow prompts to connect to your portal.
Extract Generated Module
After downloading the ZIP from Form Builder: unzip contact-form.zip -d ./contact-form.module
Upload to HubSpot
hs upload ./contact-form.module modules/contact-form.module
Watch for Changes
hs watch modules/contact-form.module modules/contact-form.module
Now any local edits auto-sync to HubSpot.
Frontend
Code splitting : Vite handles this automatically
Lazy load components : Use React.lazy() for heavy components
Memoization : Use React.memo() for preview components
Debounce drag events : Already implemented in @dnd-kit
Backend
Cache HubSpot API responses : Store form schemas in Redis (TTL: 1 hour)
Connection pooling : If using a database for tokens
Compression : Enable gzip compression
import compression from 'compression' ;
app . use ( compression ());
Deployment Checklist
Before going live:
Next Steps
Building Forms Back to form building basics
Exporting Modules Review module export process