Overview
Once you’ve configured OAuth, you can connect the Form Builder to your HubSpot account. This guide covers the connection flow, session management, and troubleshooting.
Initial Connection
Start the Application
Ensure both the frontend and backend servers are running: # Terminal 1 - Backend
cd main/server
npm run dev
# Terminal 2 - Frontend
cd main/frontend
npm run dev
Open http://localhost:5173 in your browser.
Click Connect to HubSpot
On the main interface, click the “Connect to HubSpot” button. This initiates the OAuth flow by redirecting to: GET /oauth/hubspot/install
Authorize the Application
You’ll be redirected to HubSpot’s authorization page where you can:
Review the requested permissions (scopes)
Select which HubSpot account to connect (if you have multiple)
Authorize or deny access
The app requests access to: forms, content, and forms-uploaded-files.
Automatic Redirect
After authorizing, HubSpot redirects back to your application: http://localhost:3001/oauth/hubspot/callback?code=xxx&state=xxx
The backend automatically:
Validates the state parameter
Exchanges the authorization code for tokens
Stores tokens in memory
Redirects you back to the frontend
Connection Confirmed
You’ll be redirected to the frontend with connection confirmation: http://localhost:5173/?connected=true&portalId=12345678
The interface will update to show:
Your connected portal ID
Form selection dropdown
Logout button
Connection Flow Details
Authorization Endpoint
The connection starts at the /oauth/hubspot/install endpoint:
// From oauth.ts:46-51
router . get ( '/hubspot/install' , ( _req : Request , res : Response ) => {
const state = crypto . randomBytes ( 24 ). toString ( 'hex' );
stateStore . set ( state , Date . now ());
const url = buildAuthorizeUrl ( state );
res . redirect ( url );
});
What happens:
Generates a random state token (24 bytes, hex-encoded)
Stores state with current timestamp
Builds authorization URL with client ID, scopes, and state
Redirects user to HubSpot
Authorization URL Structure
// From oauth.ts:33-44
function buildAuthorizeUrl ( state : string ) {
const clientId = getEnv ( 'HUBSPOT_CLIENT_ID' );
const redirectUri = getEnv ( 'HUBSPOT_REDIRECT_URI' );
const scope = getEnv ( 'HUBSPOT_SCOPES' );
const params = new URLSearchParams ({
client_id: clientId ,
redirect_uri: redirectUri ,
scope ,
state ,
});
return ` ${ HUBSPOT_AUTH_URL } ? ${ params . toString () } ` ;
}
Example URL:
https://app.hubspot.com/oauth/authorize?
client_id=abc123-def456
&redirect_uri=http://localhost:3001/oauth/hubspot/callback
&scope=forms%20content%20forms-uploaded-files
&state=a1b2c3d4e5f6...
Callback Handler
After authorization, HubSpot calls back with an authorization code:
// From oauth.ts:53-59
router . get ( '/hubspot/callback' , async ( req : Request , res : Response ) => {
const code = String ( req . query . code || '' );
const state = String ( req . query . state || '' );
if ( ! code || ! state || ! stateStore . has ( state )) {
return res . status ( 400 ). json ({ error: 'Invalid state or code' });
}
stateStore . delete ( state );
// ... token exchange continues
});
Validation steps:
Extract code and state from query parameters
Verify state exists in the state store
Delete state (one-time use)
Proceed with token exchange
Token Exchange
The authorization code is exchanged for access tokens:
// From oauth.ts:68-80
const body = new URLSearchParams ({
grant_type: 'authorization_code' ,
client_id: clientId ,
client_secret: clientSecret ,
redirect_uri: redirectUri ,
code ,
});
const tokenRes = await fetch ( HUBSPOT_TOKEN_URL , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/x-www-form-urlencoded' },
body ,
});
Request to:
POST https://api.hubapi.com/oauth/v1/token
Response includes:
access_token - Used for API requests
refresh_token - Used to obtain new access tokens
expires_in - Token lifetime in seconds (typically 21600 = 6 hours)
hub_id - Your HubSpot portal ID
Token Storage
// From oauth.ts:89-97
const record : TokenRecord = {
accessToken: tokenJson . access_token ,
refreshToken: tokenJson . refresh_token ,
expiresAt: Date . now () + tokenJson . expires_in * 1000 ,
portalId: tokenJson . hub_id ,
};
const key = String ( record . portalId || 'default' );
tokenStore . set ( key , record );
Tokens are stored in a Map indexed by portal ID, allowing support for multiple connected accounts.
Frontend Redirect
After successful token exchange, the user is redirected back to the frontend:
// From oauth.ts:99-107
const frontendUrl = getOptionalEnv ( 'FRONTEND_URL' );
if ( frontendUrl ) {
const url = new URL ( frontendUrl );
url . searchParams . set ( 'connected' , 'true' );
if ( record . portalId ) {
url . searchParams . set ( 'portalId' , String ( record . portalId ));
}
return res . redirect ( url . toString ());
}
Checking Connection Status
The frontend can check if the user is connected:
// From oauth.ts:118-121
router . get ( '/hubspot/status' , ( _req : Request , res : Response ) => {
const hasAny = tokenStore . size > 0 ;
res . json ({ connected: hasAny });
});
Request:
GET http://localhost:3001/oauth/hubspot/status
Response:
This endpoint checks if ANY tokens exist, not if a specific user is connected. For multi-user applications, you’ll need to implement user-specific session management.
Session Management
Accessing Tokens
The forms API uses tokens from the store:
// From forms.ts:44-47
function getAccessToken () {
const token = Array . from ( tokenStore . values ())[ 0 ];
return token ?. accessToken ?? null ;
}
This retrieves the first available token. For production, implement proper user-session mapping.
Token Expiration
Tokens are stored with expiration time:
expiresAt : Date . now () + tokenJson . expires_in * 1000
The current implementation does not automatically refresh tokens. You should implement token refresh logic before the expiresAt time.
Logout/Disconnect
Users can disconnect and clear their session:
// From oauth.ts:123-126
router . post ( '/hubspot/logout' , ( _req : Request , res : Response ) => {
tokenStore . clear ();
res . json ({ success: true , message: 'Session closed successfully' });
});
Request:
POST http://localhost:3001/oauth/hubspot/logout
Response:
{
"success" : true ,
"message" : "Session closed successfully"
}
This clears ALL tokens from memory. In a multi-user application, you should only clear the current user’s tokens.
Using Cloudflare Tunnels
For testing on mobile devices or sharing with others, use Cloudflare tunnels:
# Terminal 1 - Backend
cd main/server
npm run dev
# Terminal 2 - Backend Tunnel
cloudflared tunnel --url http://localhost:3001
# Output: https://abc-123.trycloudflare.com
# Terminal 3 - Frontend
cd main/frontend
npm run dev
# Terminal 4 - Frontend Tunnel
cloudflared tunnel --url http://localhost:5173
# Output: https://xyz-789.trycloudflare.com
Update environment variables:
# main/server/.env
HUBSPOT_REDIRECT_URI=https://abc-123.trycloudflare.com/oauth/hubspot/callback
FRONTEND_URL=https://xyz-789.trycloudflare.com/
# main/frontend/.env
VITE_API_BASE=https://abc-123.trycloudflare.com
Update HubSpot OAuth app:
Add the tunnel callback URL to your app’s Redirect URIs
Troubleshooting
”Not allowed by CORS”
Cause: Frontend URL doesn’t match CORS configuration.
Solution: The server allows localhost:5173 and *.trycloudflare.com:
// From index.ts:12-16
const allowedOrigins = [ 'http://localhost:5173' ];
if ( ! origin || allowedOrigins . includes ( origin ) || origin . endsWith ( '.trycloudflare.com' )) {
callback ( null , true );
}
Ensure your frontend URL matches one of these patterns.
”Invalid state or code”
Causes:
State parameter expired or doesn’t exist
Authorization code is missing
Trying to reuse an authorization code
Solutions:
Clear browser cache and cookies
Start the OAuth flow again from the beginning
Verify HUBSPOT_REDIRECT_URI matches your HubSpot app configuration exactly
”Token exchange failed”
Causes:
Incorrect Client ID or Client Secret
Mismatched redirect URI
Authorization code expired (60-second lifetime)
Solution: Check the error details:
// From oauth.ts:82-85
if ( ! tokenRes . ok ) {
const text = await tokenRes . text ();
return res . status ( 500 ). json ({ error: 'Token exchange failed' , details: text });
}
The details field contains HubSpot’s error message.
Connection Lost After Server Restart
Cause: Tokens are stored in memory and cleared on restart.
Solution: For production, implement persistent storage:
Database (PostgreSQL, MySQL)
Redis
Session store with disk persistence
”Not connected to HubSpot” on API Calls
Cause: Token store is empty.
Check:
GET http://localhost:3001/oauth/hubspot/status
If connected: false, reconnect via the OAuth flow.
Next Steps
HubSpot API Reference Learn about the API endpoints used by the Form Builder
OAuth Setup Review OAuth configuration and security best practices