Why Multi-Tenancy Matters for Outbound Webhooks (And How to Implement It)

If you're building a platform that sends webhooks to your customers, you'll eventually hit a question that shapes your entire event delivery architecture: how do you cleanly separate one customer's webhook configuration, destinations, and event history from another's?

That's the multi-tenancy problem, and getting it wrong leads to tangled codebases, security risks, and operational headaches that compound as you scale. In this article, we'll break down what multi-tenancy means in the context of outbound webhooks, why it's a critical design decision, and how Hookdeck's open-source Outpost project makes it straightforward to implement.

What Is Multi-Tenancy in Outbound Webhooks?

Multi-tenancy, in general software terms, means a single deployment of your system serves multiple isolated customers (tenants). Each tenant gets their own logical slice of the system (their own data, configuration, and access boundaries) without needing a separate deployment.

Applied to outbound webhooks, multi-tenancy means that every resource involved in event delivery is scoped to a specific tenant. A tenant typically maps to a user, team, or organization in your product. Their webhook destinations, topic subscriptions, event logs, and delivery attempts all belong to them and only them.

Without multi-tenancy, you're left building ad hoc scoping into every query, every API call, and every piece of delivery logic. It's the kind of work that feels manageable at first and becomes a maintenance burden by the time you have a hundred customers, each with their own endpoints, retry states, and event histories.

Why Multi-Tenancy Is Important

Data isolation and security

The most obvious reason is security. When Customer A configures a webhook endpoint with authentication headers, Customer B should have no way to see, modify, or accidentally receive events meant for A. Tenant scoping enforces this boundary at the infrastructure level rather than relying on application-level checks scattered throughout your code.

Independent configuration per customer

Different customers have different needs. One might want events delivered to a webhook URL. Another might prefer AWS SQS or GCP Pub/Sub. Some want to subscribe to every event topic you offer; others care about a narrow subset. Multi-tenancy gives each customer their own configuration space, so these differences are handled cleanly rather than through a mess of conditionals.

Operational clarity

When something goes wrong (a destination is failing, retries are piling up, a customer reports missing events) you need to debug at the tenant level. Multi-tenancy gives you that natural boundary. You can inspect a single tenant's destinations, events, and delivery attempts without wading through every customer's data.

Scaling without architectural rewrites

A well-designed multi-tenant system lets you go from one customer to ten thousand without changing how your webhook infrastructure works. The tenant abstraction stays the same whether you have a handful of destinations or millions of events flowing through the system daily.

How Multi-Tenancy Works in Hookdeck Outpost

Hookdeck Outpost is an open-source, self-hostable outbound webhooks infrastructure written in Go. It handles event delivery to webhook endpoints and a range of native event destinations including AWS SQS, AWS Kinesis, AWS S3, GCP Pub/Sub, Azure Service Bus, RabbitMQ, and the Hookdeck Event Gateway. It's distributed under the Apache 2.0 license and designed for low-maintenance, high-throughput operation.

Multi-tenancy is a first-class concept in Outpost. Every resource (destinations, events, delivery attempts) is scoped to a tenant. Here's how the model works.

Tenants map to your existing identifiers

Outpost doesn't generate tenant IDs. You provide them, which means you can use whatever identifiers your application already has: a user ID, an organization slug, a team UUID. This eliminates the mapping layer you'd otherwise need between your system and your webhook infrastructure.

Creating a tenant is a single idempotent API call:

curl --location --request PUT 'localhost:3333/api/v1/tenants/acme-corp' \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer <API_KEY>'

If the tenant already exists, the call updates it. If it doesn't, it creates it. No separate "check then create" logic needed.

Tenants can carry metadata

Each tenant supports an arbitrary metadata object (key-value pairs for storing whatever contextual information your application needs). This is useful for tagging tenants with plan tiers, region identifiers, or any other data that helps with filtering and operational decisions:

curl --location --request PUT 'localhost:3333/api/v1/tenants/acme-corp' \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer <API_KEY>' \
  --data '{
    "metadata": {
      "plan": "enterprise",
      "region": "us-east-1"
    }
  }'

Events are published to specific tenants

When your application publishes an event, it includes the tenant_id in the event payload. Outpost then evaluates that event against the tenant's registered destinations and delivers it accordingly:

{
  "id": "evt_abc123",
  "tenant_id": "acme-corp",
  "topic": "invoice.paid",
  "time": "2025-06-01T08:23:36.082Z",
  "data": {
    "invoice_id": "inv_789",
    "amount": 9900,
    "currency": "usd"
  }
}

Each event is evaluated against all of the tenant's registered destinations. If a tenant has three destinations subscribed to the invoice.paid topic, the event fans out to all three, and each delivery is tracked independently.

Destinations, topics, and subscriptions are tenant-scoped

A destination belongs to a single tenant. Topics are defined at the system level, but subscriptions (which topics a destination listens to) are configured per destination, per tenant. This means two tenants can subscribe to entirely different sets of events without any interference.

Outpost supports a wide range of destination types per tenant, including traditional webhook URLs and cloud-native alternatives like AWS SQS, GCP Pub/Sub, and Azure Service Bus. Customers aren't locked into webhooks if their infrastructure is better suited to a message queue.

Tenant-scoped portals and JWT tokens

Outpost provides a user-facing portal where your customers can manage their own destinations, view event history, and inspect delivery attempts. Access to this portal is tenant-scoped: you generate a redirect URL or JWT token for a specific tenant, and that token only grants access to that tenant's resources.

# Get a portal redirect URL for a tenant
curl --request GET \
  'localhost:3333/api/v1/tenants/acme-corp/portal?theme=light' \
  --header 'Authorization: Bearer <API_KEY>'

This means you can embed webhook management into your product's UI without building the entire interface yourself, and without worrying about cross-tenant data leakage.

Single-tenant mode works too

If your application only has one tenant, or if you're using Outpost for internal event delivery, you don't need to ignore the tenant model. Just create a single tenant with a hardcoded ID like default or production. The same API and resource scoping works identically; you're just choosing not to create additional tenants. You can even use tenant IDs to separate environments (e.g., live and test) on the same Outpost deployment.

Managing Tenants via the API

Outpost's Tenant API covers the full lifecycle. Here's a quick summary of what's available:

Create or update a tenantPUT /tenants/{tenant_id} is idempotent. Call it with an optional metadata body. Returns 201 on creation, 200 on update.

Retrieve a tenantGET /tenants/{tenant_id} returns the tenant's full details including computed fields like destinations_count and the list of topics subscribed across all destinations.

List tenantsGET /tenants supports cursor-based pagination, sorting by created_at, and filtering by date range. Requires Redis with the RediSearch module.

Delete a tenantDELETE /tenants/{tenant_id} removes the tenant and all associated destinations. This is a destructive operation, so handle it carefully in your application logic.

Portal accessGET /tenants/{tenant_id}/portal returns a redirect URL with an embedded JWT for authenticating the tenant into the self-service portal.

JWT tokenGET /tenants/{tenant_id}/token returns a scoped JWT token for making safe browser-side API calls on behalf of the tenant.

Putting It All Together

Here's what a typical integration flow looks like:

  1. Register tenants as customers sign up in your product. Use your existing customer IDs as tenant IDs in Outpost.
  2. Configure destinations for each tenant, either programmatically through the API or by embedding the Outpost portal in your product's UI so customers can self-serve.
  3. Publish events from your application to your message queue or directly to Outpost's publish API, including the tenant_id in each event.
  4. Outpost handles the rest — evaluating topic subscriptions, fanning out events to matching destinations, managing retries with exponential backoff, and logging delivery attempts.

Because tenants are isolated at every layer, adding a new customer is just a PUT call. No infrastructure changes, no new deployments, no config files to update.

Conclusion

Multi-tenancy isn't a nice-to-have for outbound webhook systems, it's a structural requirement for any platform that delivers events to more than a handful of customers. Without it, you end up rebuilding tenant isolation logic across every component of your event delivery pipeline.

Hookdeck Outpost treats multi-tenancy as a core primitive rather than an afterthought. Tenants map directly to your application's existing identifiers, all resources are automatically scoped, and the API is designed so that going from one tenant to thousands requires zero architectural changes. Combined with support for multiple destination types, and a self-service portal, it's a practical foundation for outbound webhook infrastructure that scales with your platform.

Explore the Outpost documentation to get started, or check out the Tenant API reference for the full details.