Skip to content
DedicatedPHP Contact

Multi-tenant data isolation in PHP without leaks

Design a PHP SaaS that prevents cross-organization access through explicit context and controls across data, queues, cache, and testing.

Editorial diagram of a PHP SaaS with organizations isolated across databases, cache, files, and queues

Multi-tenant data isolation in PHP is not solved by adding a WHERE organization_id = ? condition on the main screen. A leak can originate in an API, an export, a cache, an attachment, a queue consumer, or a scheduled process. It can also occur when a legitimate administrator switches organizations and the system retains a previous context.

The architectural goal must be clear: no operation that reads, modifies, processes, or delivers customer information may be able to act without a verifiable organization scope. That scope must be propagated explicitly and validated at every relevant application boundary.

What a SaaS application must isolate

What a SaaS application must isolate — DedicatedPHP visual guide

The transactional model is only part of the risk surface. Inventory the resources that have an organizational owner and define for each one how its ownership is identified, stored, retrieved, deleted, and audited.

  • Transactional data: users, projects, orders, invoices, configurations, and relationships between entities.
  • Files and attachments: objects in external storage, thumbnails, generated documents, and their metadata.
  • Cache: query results, calculated permissions, sessions, API responses, and configuration data.
  • Search indexes: indexed documents, suggestions, and previously aggregated filters.
  • Asynchronous processing: queue jobs, retries, import batches, and notifications.
  • Operations and observability: logs, traces, metrics, support exports, and internal tools.

Not all resources require the same strategy. A public catalog may be shared, while an invoice, its PDF, and download logs must retain an unambiguous link to the organization. The decision must be documented to prevent a new entity from being created without ownership rules.

Choosing the data isolation model

There are three common models. None is universally superior: the choice depends on regulatory requirements, volume, operations, business model, and the team's ability to maintain the platform.

Shared database with organization key

All organizations share tables, and each record subject to isolation contains a key such as organization_id. It is the most straightforward approach for evolving the product and running global aggregate queries. In return, it requires extreme discipline: every query, relationship, index, cache entry, and task must respect the scope.

At a minimum, use foreign keys where appropriate, composite indexes that begin with organization_id, and composite unique constraints as well. For example, an order code that is unique within an organization must not be declared globally unique if that is not the business rule.

Separate schema per organization

Each customer operates in a different logical schema within the same database server. This reduces the risk of omitting a filter in separate tables, but complicates migrations, connections, analytics tools, and global queries. It is appropriate only if the database engine, framework, and day-to-day operations consistently support that pattern.

Database per organization

Separating databases provides a stronger boundary and can facilitate restores or moves of individual customers. It also increases the inventory of connections, migrations, backups, monitoring, and structural change deployments. It is particularly important to assess how global reporting, bulk changes, and error recovery will be performed.

Physical separation reduces certain classes of failure, but does not replace authorization, file controls, secret management, or context validation in shared services.

Reference architecture: explicit context at boundaries

The organization context must not be inferred from arbitrary parameters sent by the browser. It must be resolved from an authenticated and authorized source: a validated subdomain, a token with an appropriate audience, a user's membership, or an integration credential associated with a single organization.

In a PHP application, an entry layer can construct an immutable context object with the organization identifier, the actor, their permissions, and a request identifier. Controllers, console commands, and queue consumers receive that context or reconstruct it from verified data. Avoid mutable global variables that may persist improperly in long-running processes.

final class OrganizationContext {
    public function __construct(
        public readonly string $organizationId,
        public readonly string $actorId
    ) {}
}

Repositories must require the context to query or modify isolated entities. An interface that makes it difficult to omit is preferable to an implicit convention that depends on each developer's memory. Where possible, also enforce access policies in the domain layer: belonging to an organization does not automatically authorize every action within it.

Avoiding forgotten filters in queries and relationships

An isolated query must filter by organization before looking up business identifiers. Retrieving a record by id first and checking its owner afterward can cause exposure if the result is serialized, logged, or used before it is rejected.

  • Centralize queries in repositories or read services with methods that receive the context.
  • Prohibit direct access to isolated models from controllers, templates, and event consumers.
  • Review relationships: a lazily loaded relationship can bypass the filter applied to the main entity.
  • Use database constraints to prevent relationships between rows from different organizations when the model allows it.
  • Define conventions for migrations, test seeds, and analytical queries.

In database engines that provide row-level security policies, these can provide an additional defense. However, their adoption must include connection testing, role management, and review of administrative processes. It is not advisable to assume that a database policy automatically protects files, cache, or external indexes.

Risks outside the main web flow

Opaque identifiers reduce enumeration, but they do not authorize access. A UUID or random identifier must still be resolved within the active organization. Likewise, a signed download URL needs an object belonging to the correct scope, an appropriate expiration, and revocation rules when permissions change.

Cache keys must include the organization identifier and, when content depends on permissions, an additional role or authorization-version dimension. A key such as dashboard:summary is unsafe in a multi-tenant environment; a key with explicit scope also enables more precise invalidations.

Exports are especially sensitive because they are often executed outside the original request. Store who requested it, for which organization, which filters were approved, and where the result will be delivered. Do not send attachments or links to recipients calculated from unvalidated data.

Propagating context in APIs, webhooks, and queues

An API must derive the organization from the credential or verify that the requested resource belongs to the organization associated with that credential. Allowing an X-Organization-Id header may be valid for operators with explicit delegation, but it requires specific authorization, auditing, and an interface that makes the scope change visible.

Incoming webhooks must not trust an organization identifier included in the body without verifying the signature, sender, and prior association of the integration. For outgoing webhooks, generate events from already scoped data and avoid reusing payloads from a shared queue without validating the recipient.

Every asynchronous job must carry an organization identifier alongside the resource identifier and reconstruct the context before querying. The consumer must verify both values, even if the job was created internally. Retries, delayed jobs, and scheduled tasks need the same rule: no implicit request context is safely available.

Verifiable tests and diagnostic signals

The most important test is not that one organization can see its own data, but that it cannot read or modify another organization's data. Create two organizations with deliberately similar data and run integration tests against every entry point: web interface, API, commands, exports, downloads, and queue consumers.

  • Request a resource from organization B using a session or credential from organization A and expect a non-revealing response.
  • Attempt to update, delete, download, and export cross-organization resources, not just query them.
  • Verify that cache keys for A and B produce independent results.
  • Run a queue job with a resource from another organization and verify that it fails in a controlled manner.
  • Test restores, imports, and nightly tasks with data from more than one organization.
  • Log sensitive actions with actor, organization, resource, and outcome, without introducing unnecessary personal data into logs.

Property-based tests can complement manual cases: for any resource created under an organization, no actor without valid membership should be able to observe or alter it through an exposed route. This property must apply to future changes to endpoints and repositories.

Adoption plan for an existing application

If data is already mixed, do not start by rewriting the entire application. First inventory entities, flows, integrations, and administrative access. Then define ownership for each record and resolve ambiguous cases with reviewable business rules.

  1. Add the organization entity and ownership key to the target tables.
  2. Populate that key through a controlled migration and retain evidence of cases without reliable assignment.
  3. Introduce scoped repositories and cross-organization access tests in the most sensitive routes.
  4. Include scope in new cache entries, files, searches, and jobs.
  5. Progressively migrate legacy flows and block new context-free queries in code review.
  6. Enable stricter controls when metrics and tests demonstrate sufficient coverage.

Decisions worth documenting before scaling

Decisions worth documenting before scaling — DedicatedPHP visual guide

Before onboarding the next organization, document the selected model, the source of truth for context, administrative access exceptions, the identifier strategy, cache boundaries, file ownership, data recovery, log retention, and the procedure for a suspected cross-organization access.

Also determine who can act on behalf of another organization, how that delegation is approved, and how it is revoked. Multi-tenant data isolation in PHP is maintained through explicit decisions, repeatable technical constraints, and tests that turn an architectural promise into verifiable behavior.

Want to apply these ideas to your project?Let’s discuss your PHP platform.
View related service