Laravel 10 application that maintains a local, read-optimized mirror of HubSpot CRM and presents an authenticated sales dashboard.
composer install
cp .env.example .env
php artisan key:generateConfigure the database and a durable queue connection in .env, then set:
HUBSPOT_TOKEN=pat-na1-...
HUBSPOT_QUEUE=hubspot
QUEUE_CONNECTION=databaseRun the included migrations:
php artisan migrateGrant read access for owners and each enabled CRM object: companies, contacts, deals, tasks, calls, meetings, emails, notes, products, line items, and quotes. Grant the matching CRM schema/property read scopes and deal pipeline read access. In HubSpot's current scope names these are generally crm.objects.<object>.read, crm.schemas.<object>.read, and crm.objects.owners.read; the exact list exposed by HubSpot varies by subscription and enabled products.
Start with only the resources enabled in config/hubspot.php. A missing scope produces a 403 and is intentionally not retried.
# Incremental sync of everything
php artisan hubspot:sync
# One resource
php artisan hubspot:sync contacts
# Configured groups: reference, sales, activities, commerce, relationships
php artisan hubspot:sync sales
# Ignore the local cursor
php artisan hubspot:sync contacts --full
# Inspect local synchronization state
php artisan hubspot:statusAll sync commands dispatch jobs; they do not perform API work in the console process.
Run at least one dedicated worker:
php artisan queue:work --queue=hubspot --tries=3Production cron must invoke Laravel's scheduler every minute:
* * * * * cd /path/to/hubspotSync && php artisan schedule:run >> /dev/null 2>&1The schedule refreshes deals every 5 minutes, companies/contacts and activities every 10 minutes, relationships every 30 minutes, commerce hourly, and reference metadata daily at 02:00. Multi-resource runs are chained in the configured synchronization order. Scheduler overlap locks, single-server scheduling, and unique resource jobs prevent duplicate concurrent imports. Use a shared cache driver when running multiple application servers.
Expose this HTTPS endpoint publicly and enter the exact URL as the private app's webhook target:
POST https://your-domain.example/api/webhooks/hubspot
Then configure:
HUBSPOT_CLIENT_SECRET=the_secret_from_the_private_app_auth_tab
HUBSPOT_WEBHOOK_URL=https://your-domain.example/api/webhooks/hubspotSubscribe in HubSpot to creation, property-change, and deletion events for the CRM objects you need. Contacts, companies, deals, tasks, calls, meetings, emails, notes, products, line items, and quotes are recognized. Unsupported events are recorded as ignored.
The endpoint validates both current v3 signatures and the v1 signatures used by legacy private-app webhook subscriptions. It stores each HubSpot event ID once, immediately returns HTTP 202, and queues an authoritative API fetch before upserting the local record. Deletion events mark the local row archived. Keep the hubspot queue worker running for webhook processing.
GET /dashboard is available immediately. Add the application's preferred authentication middleware before exposing it outside a trusted network.
The dashboard is guaranteed never to call HubSpot. Its controller delegates to a local database query, and the view renders only synchronized Eloquent data. HubSpot outages or rate limits therefore cannot block a dashboard request.
php artisan test
./vendor/bin/pint --testFocused tests fake Laravel HTTP and verify bearer authentication, 100-record cursor pagination, incremental Search API filters, 429/5xx retries, and command job dispatch.