← Blog
2026-05-22

Designing a Multi-Tenant Copy Trading Backend (Without Building It Yourself)

Provider isolation, per-tenant API keys, audit logs, and integrator hierarchies — the architecture problems you can skip by integrating CopyBridge.

Most SaaS platforms treat multi-tenancy as a solved problem: separate each customer's data, enforce permissions, done. Copy trading backends break that model. When one tenant's trading signal must replicate across hundreds of follower accounts in real-time—often spanning multiple integrators who resell your infrastructure—the architecture demands surgical precision. A misconfigured isolation boundary doesn't just leak data; it sends the wrong trades to the wrong accounts, triggering real financial loss.

This post walks through the invariants that keep a multi-tenant copy trading backend secure and correct, drawing from CopyBridge's architecture.

Why Copy Trading Breaks Standard Multi-Tenancy

Traditional SaaS multi-tenancy assumes clean tenant boundaries. Stripe isolates customer A's payments from customer B's. Notion keeps workspace data separate. The read-write patterns stay within tenant walls.

Copy trading platforms have cross-tenant workflows by design:

  • Provider → Follower replication: A signal provider (tenant A) publishes trades. Multiple followers (tenants B, C, D…) subscribe and execute those trades on their own broker accounts. The system must write trade instructions into follower accounts while preserving strict audit trails of who authorized the replication.

  • Integrator hierarchies: A white-label reseller (integrator) creates child signal providers under their namespace. The child provider sees only their own subscribers. Followers see only their own account state.

  • Webhook fan-out: When a provider's trade opens or closes, or a follower joins or leaves, the backend delivers one signed webhook to that provider's own configured endpoint — naming the affected followers in the payload rather than leaking data across tenants. Any further fan-out to individual followers is the provider's own concern, not CopyBridge's.

Standard row-level security (RLS) policies struggle here. Postgres RLS works elegantly when queries stay within a single tenant. But replicating a trade from provider p123 to follower f456 requires a single transaction that touches both tenants' rows. RLS can block or over-permit these cross-tenant writes.

Provider Isolation Invariants

Every copy trading backend must enforce these non-negotiable rules:

  1. A provider cannot read follower balances or positions. The provider publishes signals; followers execute them. The provider sees who its subscribers are and whether they are active; it never sees their balance, equity or open positions.

  2. A follower cannot read other followers' data. Two followers subscribed to the same provider must remain invisible to each other.

  3. Providers cannot modify follower risk settings. Followers set their own lot multiplier, max lot size, and per-symbol on/off toggles — all as inputs on their own Copier EA, never as server-side state a provider could reach. The provider's EA sends trade instructions; the follower's Copier EA applies those local rules before execution.

  4. Trade replication is append-only for audit. Once the system logs "provider p123 opened EUR/USD buy 1.0 lot at 12:34:56.789 UTC", that record is immutable.

CopyBridge implements these invariants at the application layer, not database constraints alone. Every API request carries context: X-API-Key for providers, X-Subscriber-Key for followers, X-Integrator-Key for white-label resellers. The authorization middleware checks not just "does this key exist?" but "does this key's role permit reading/writing this specific resource?"

Example: A provider requests GET /api/v1/admin/subscribers with its X-API-Key. The query is filtered by the provider id the key resolved to, so the response contains only that provider's followers, with fields like id, name, email, active and created_at. There is no balance or equity field on that response — or anywhere else in the follower API. A follower looking up their own record calls GET /api/v1/customer/:subscriberKey and gets provider_name, status and created_at. No exposure of other followers.

Integrator + Child Provider Hierarchy

White-label resellers need multi-level tenancy. An integrator creates child signal providers, each operating independently but under the integrator's namespace. The integrator bills the child providers (often through Whop seat licensing), monitors platform usage, and enforces compliance policies.

This creates a three-tier hierarchy:

Integrator (integrator_123)
├── Provider A (provider_a1)
│   ├── Follower 1
│   └── Follower 2
└── Provider B (provider_b1)
    ├── Follower 3
    ├── Follower 4
    └── Follower 5

Authorization rules cascade:

  • Integrator role: Read-only access to each child provider's record — name, email, plan, active, is_sandbox, created date. Can create, update and disable child providers. Cannot execute trades or modify follower settings.
  • Child provider role: Full control over their own signal publishing, webhook configuration, subscriber management. No visibility into sibling providers (provider A cannot see provider B's data).
  • Follower role: Manages their own subscription to one or more providers. No visibility into the integrator layer.

There is no separate integrators table. An integrator is a row in providers with is_integrator = true, and the hierarchy is expressed by two self-referencing foreign keys on that same table: parent_provider_id and created_by_integrator_id. Every integrator query filters on created_by_integrator_id, bound to the provider that the X-Integrator-Key header resolved to — so a child provider that the caller did not create is simply not in the result set. No raw SQL comes from API clients; every query is parameterized with the tenant id injected server-side.

Audit Log for Cross-Tenant Operations

When a provider's Master EA sends a trade signal that replicates to 50 followers, the audit trail must capture:

  1. Provider signal ingestion: Timestamp, symbol, direction, lot size, broker-assigned ticket ID.
  2. Replication decision per follower: Which followers had active subscriptions? Which ones passed symbol filters? Which ones hit risk limits and skipped the trade?
  3. Execution records per follower: Actual lot size after multiplier, execution price, broker-assigned ticket on follower's MT4/MT5 account, latency from signal to execution.
  4. Webhook delivery: For the provider's configured webhook URL, log the outbound HTTP request, response status and attempt count.

That is the standard to aim for. What CopyBridge itself stores is narrower, and the gap matters.

Provider-side signals live in the trades table. Outbound event deliveries are logged in webhook_deliveriesevent_id, event_type, payload, target_url, status, response_code, attempts, last_error, delivered_at — and tenant-scoped actions in audit_log, which records provider_id, actor_type, event_type, a JSON detail blob, plus IP and user agent.

There is no server-side record of follower execution. The Copier EA only ever issues GET requests to the API; it pulls the signal and fills the order on the follower's own broker account, and the resulting broker ticket, fill price and latency stay in the terminal. So the audit trail answers "what did the provider publish, and what did we deliver where" — it does not answer "at what price did follower 3 actually fill".

For integrators building copy trading SaaS, that boundary is the thing to design around: if you need per-follower execution records for compliance or dispute resolution, that data has to be collected in your own system, not queried out of CopyBridge.

Row-Level Security vs Application-Layer Guards

Postgres RLS tempts developers with declarative security: define policies once, database enforces them on every query. For single-tenant isolation, this works. For copy trading's cross-tenant workflows, RLS becomes a footgun.

Consider a trade replication transaction:

BEGIN;
-- Provider context
INSERT INTO signal_events (provider_id, symbol, lots, ...) VALUES ($1, $2, $3, ...);
-- Follower context (for each active subscriber)
INSERT INTO trade_executions (subscription_id, source_signal_id, follower_lots, ...) 
VALUES ($4, $5, $6, ...);
COMMIT;

If RLS policies enforce WHERE provider_id = current_setting('app.current_provider'), the second INSERT fails—it's writing to a follower's table with provider context set. You'd need to SET LOCAL the session variable mid-transaction, which complicates rollback logic and reintroduces SQL injection risks if variable values aren't sanitized.

CopyBridge uses application-layer authorization instead:

  1. Key lookup, not key parsing: The keys are plain UUIDs and carry no role, tenant or scope of their own — there is no pk_/sk_ prefix scheme to read. The header decides which table the value is looked up in: X-API-Key against providers.master_key, X-Subscriber-Key against followers.subscriber_key, X-Integrator-Key against providers.integrator_api_key. A value that does not match the UUID shape is rejected before it reaches SQL, so garbage produces a clean 401 rather than a 500.
  2. Tenant identity comes from the middleware, never the request: The lookup populates req.provider, req.follower or req.integrator with the row it resolved. Route handlers read the tenant id from there. A provider_id in a request body is never trusted as the tenant.
  3. Parameterized queries with tenant filters: Statements include WHERE provider_id = $1 bound to the id the middleware resolved, so a handler cannot widen its own scope by accident.

An integrator key carries an extra condition beyond existing: the row it matches must also have is_integrator = true and active = true, so revoking integrator status takes effect on the next request without rotating the key.

Webhook Security in Multi-Tenant Fans

Outbound webhooks in CopyBridge are scoped to the provider, not to each individual follower: a provider configures one webhook URL and gets one signing secret, and events about that provider's trades and follower lifecycle (a follower joining or leaving) are delivered there — with the affected followers named in the payload, not fanned out to a separate endpoint per follower. If a white-label integrator's own backend needs to notify each of its customers individually, that fan-out happens in the integrator's own system after receiving the provider-level event, not inside CopyBridge.

That single-endpoint-per-provider design is itself a multi-tenancy invariant worth stating plainly: the signing secret is per provider, not per subscription. A provider with 200 followers verifies every delivery — regardless of which followers it concerns — with the same secret; there's no per-follower secret to leak or rotate independently.

The signing construction (Standard Webhooks, HMAC-SHA256 over webhook-id.webhook-timestamp.raw_body, using the decoded whsec_... secret as the key) and a verified reference implementation are covered in the copy trading webhooks guide — reproducing a second copy here has already gone wrong once, so we link instead of duplicating it. The retry contract (two attempts, ~500 ms apart, no further automatic retry after both fail) is scoped per delivery, so one provider's webhook trouble never affects another tenant's deliveries — which is the actual multi-tenancy guarantee this section is about.

Testing Multi-Tenant Isolation

Sandbox mode is non-negotiable for copy trading backends. Integrators need to test provider→follower replication without real orders landing on a broker. CopyBridge's sandbox is a per-provider is_sandbox flag, not a separate simulation stack: the Master EA and Copier EA talk to the API as normal, trades are stored in the same trades table flagged is_sandbox = true, and the risk caps — the symbol whitelist and the lot ceiling — are bypassed so a test provider can stage data that production safety rules would otherwise block.

The part that keeps real orders off a broker is server-side: every endpoint the Copier EA polls filters sandbox rows out before they are returned, so the EA never receives one. The EA does also skip a trade marked is_sandbox, but that is a second layer that cannot fire through the current feed — which matters, because it means the guarantee does not depend on which EA version a follower happens to be running. That makes the copier's version a safety dependency, which is why turning sandbox mode on for a provider is refused while any of its followers is still running an out-of-date Copier EA — an older build would not know to skip.

Automated tests for multi-tenant isolation:

import requests

# Test: Provider cannot read other provider's subscribers
resp = requests.get('https://api.copybridge.io/api/v1/admin/subscribers', headers={'X-API-Key': 'provider_a_key'})
assert resp.status_code == 200
subscriber_ids = [s['id'] for s in resp.json()['subscribers']]

# Verify provider B's subscribers don't appear in provider A's response
resp_b = requests.get('https://api.copybridge.io/api/v1/admin/subscribers', headers={'X-API-Key': 'provider_b_key'})
subscriber_ids_b = [s['id'] for s in resp_b.json()['subscribers']]
assert len(set(subscriber_ids) & set(subscriber_ids_b)) == 0

Run these tests on every deploy. A regression that breaks tenant isolation must fail CI before reaching production.

Operational Observability

Multi-tenant backends need per-tenant and cross-tenant metrics. Providers want to see "How many of my signals replicated successfully in the last hour?" Integrators want "Which child provider has the highest follower churn rate?" Platform operators need "Are any tenants hitting rate limits or causing database load spikes?"

CopyBridge has no metrics pipeline of that kind today — there is no statsd or Prometheus emitter in the API. What exists is a request log line per call and the audit_log and webhook_deliveries tables described above, both keyed by provider_id. That is enough to answer per-tenant questions after the fact by querying, and not enough to alert on in real time. If you are building on top of CopyBridge and need live per-tenant dashboards, plan to derive them from the outbound webhook stream in your own system.