Event:user.created· Destinations:Slack (per-env channel)HubSpot (sandbox vs prod)Webhook (test harness)
The problem
Integration code and test data do not mix well. The moment you wire up a production CRM or a shared Slack channel, every feature branch, integration test, and staging deploy becomes a chance to fire off a real side effect by accident.
Teams usually work around this with feature flags, env-specific configs, or "only run this in prod" branches. Something eventually slips through — a worker fires from staging, a developer on a dev box hits HubSpot, a load test floods a shared channel, and someone has to go clean up the CRM.
The cleaner pattern is to treat environments the same way you treat customers: separate workspaces. Each environment has its own publishable key, its own connections (usually pointing at sandbox accounts), and its own event history — so there is nothing to leak even when the code behaves identically.
The event flow
Meshes receives the source event once, maps it to the right destinations per workspace, and keeps delivery visible when downstream APIs fail.
Event payload
user.created{
"user_id": "usr_homer",
"email": "homer@springfield-npp.dev",
"account_id": "acc_springfield",
"environment": "staging",
"created_at": "2026-04-17T12:00:00Z"
}Each environment workspace points at its own Slack channel — #dev-alerts, #staging-alerts, #alerts — with its own OAuth token.
Noisy test data stays in the channel built to absorb it; production alerts stay trustworthy because only real events reach them.
Dev and staging workspaces connect to a HubSpot sandbox; the production workspace connects to your real HubSpot account.
Test contacts and failed-delivery replays stay inside the sandbox environment with no chance of polluting the real CRM.
Non-production workspaces can route the same event to a local test harness or QA environment via webhook without affecting any other destination.
Engineers can capture, diff, and verify outgoing events against real production rules without mocking the entire delivery layer.
How Meshes handles it
Instead of maintaining separate workers, retry logic, and visibility per destination, Meshes gives you one event path, destination-aware routing, and built-in delivery guarantees.
From your product
user.createdenters Meshes onceimport MeshesEventsClient from '@mesheshq/events';
// WORKSPACE_PUBLISHABLE_KEY is set per environment:
// dev → pk_dev_... → HubSpot sandbox + #dev-alerts
// stage → pk_stage_... → Salesforce sandbox + #staging-alerts
// prod → pk_prod_... → HubSpot prod + #alerts
const meshes = new MeshesEventsClient(
process.env.WORKSPACE_PUBLISHABLE_KEY!,
);
await meshes.emit({
event: 'user.created',
resource: 'membership_level',
resource_id: 'starter',
payload: {
user_id: 'usr_homer',
email: 'homer@springfield-npp.dev',
account_id: 'acc_springfield',
environment: process.env.APP_ENV,
created_at: new Date().toISOString(),
},
});Across destinations
On every delivery
Why this matters
Even when code behaves identically across environments, the connections behind a dev or staging workspace point at sandbox accounts — there is nothing to hit in prod.
Production delivery logs only contain production events. Staging replays and load tests stay confined to their own workspace history.
The same workspace model that isolates your customers also isolates your environments. You do not learn two different patterns to ship customer-facing integrations and keep your own stack safe.
Related
Docs
See how to spin up per-environment workspaces with their own connections and publishable keys.
Open linkDocs
See the publishable-key pattern that lets you point the same client at dev, staging, or production.
Open linkDocs
Keep rules in sync across environments while letting each workspace own its connections.
Open linkUse Case
See the same workspace primitive applied to external customer tenancy.
Open linkBlog
Why the workspace boundary solves both customer isolation and environment isolation.
Open linkCompare
Compare per-workspace environment isolation to rolling env flags and branch logic in your own integration code.
Open link