
# Data isolation between environments

Every KeenAuth environment (`development`, `staging`, `production` in each of your projects) is a separate tenant. Users, organizations, applications, sessions, keys, logs and events of one environment are never visible to another environment, including another environment of your own account. This page explains how that boundary is enforced.

## Two independent lines of defense

Environments share a database schema, and every row that belongs to an environment carries that environment's identifier. Isolation is enforced twice, and both checks have to fail at the same time for data to cross the boundary:

1. **Typed environment scope in the application.** Every request is resolved to exactly one environment from its host name or its credential before any data is read. Code that reads or writes environment data cannot open a database transaction without that scope; a call without it is rejected before the query runs.
2. **PostgreSQL row-level security.** Every table that holds environment data has row-level security enabled and forced, so the policy applies to the table owner too. Each transaction sets the current environment, and a row is visible only when its environment matches. A transaction that has not set an environment sees no rows. No application or maintenance role is allowed to bypass row-level security.

Every environment-scoped table starts its primary key and indexes with the environment identifier, so the isolation condition does not add a separate lookup to queries.

## Lookups by token or key

Some requests arrive with only a secret value, for example an authorization code, a refresh token or an API key. KeenAuth finds such a value through a digest-only lookup and then compares the environment of the stored record with the environment of the request. If they differ, the value is treated as invalid.

## What you see when you cross the boundary

| Attempt | Response |
|---|---|
| Read a resource that belongs to another environment | `404 Not Found`; the response does not reveal that the resource exists |
| Use an access token issued by another environment | `401 Unauthorized`; each environment has its own issuer and its own signing keys (JWKS) |
| Authenticate with a client registered in another environment | `invalid_client` |

## Beyond the database

The same boundary applies to every other place where environment data lives:

- **Caches:** every cache key starts with the environment identifier.
- **Events and jobs:** every internal event carries its environment, and consumers set their database scope from it; every queued job carries the environment it belongs to.
- **Keys:** encryption and signing keys are separate for each environment.
- **Logs:** log lines carry the environment as a field, and the [environment log API](/docs/api/management/logs/list) returns only entries of the environment you call.
- **Background work:** maintenance jobs (retention clean-up, deletions) run environment by environment, each in its own scoped transaction.

Moving configuration from one environment to another (configuration promotion) copies settings only. Secrets and end-user or organization data are never part of a promotion bundle.

## How isolation is tested

A cross-tenant isolation test suite runs in KeenAuth's continuous integration and blocks a change when it fails. It checks that:

- every table that holds environment data has row-level security enabled and forced;
- each API surface returns `404` or `401` for resources, tokens and clients of another environment;
- background jobs and the log and event pipeline only touch the environment named in their input.

A schema check also rejects a new environment data table without the environment identifier column.

## Related

- [Test users and messages in non-production environments](/docs/guides/test-environments)
- [API versioning and deprecation policy](/docs/policies/versioning)
