Multi-Tenant UUIDs: Keys, Authorization, and Data Isolation

    14 October 2024Updated 10 September 2026
    6 min read
    Best practices
    Architecture
    uuid
    database
    migration

    An identifier is not an access policy

    A UUID identifies a tenant or resource. It does not prove who owns that resource or who may access it. Even a difficult-to-guess UUID can be copied from a link or log. Every request still needs authorization, as the OWASP IDOR prevention guide explains.

    In a shared database, tenant IDs give records a scope. Separate schemas or databases can provide additional boundaries, but the application must still route each authenticated request to the correct tenant.

    Choose the version for the workload

    UUID v4 is a useful choice when an identifier should not encode a timestamp. Python's uuid.uuid4() uses cryptographically secure randomness; it does not use the ordinary random module's getrandbits function. See the Python UUID documentation.

    python
    import uuid
    
    tenant_id = uuid.uuid4()
    document_id = uuid.uuid4()
    assert tenant_id.version == 4
    assert document_id.version == 4

    UUID v7 is standardized by RFC 9562 and includes a timestamp, which can help index locality but exposes approximate generation time. Version 1 also contains time and a node field that may derive from a MAC address. Choose deliberately when exposing identifiers publicly. The layouts are defined in RFC 9562.

    Keep uniqueness constraints even when collision probability is very small. Treat an identifier as public metadata, not as a password or bearer token.

    Model tenant-scoped relationships

    This PostgreSQL example keeps tenants, documents, and comments in shared tables:

    sql
    CREATE TABLE tenants (
      tenant_id uuid PRIMARY KEY,
      name text NOT NULL
    );
    
    CREATE TABLE documents (
      tenant_id uuid NOT NULL REFERENCES tenants (tenant_id),
      document_id uuid NOT NULL,
      title text NOT NULL,
      PRIMARY KEY (tenant_id, document_id)
    );
    
    CREATE TABLE comments (
      tenant_id uuid NOT NULL,
      comment_id uuid NOT NULL,
      document_id uuid NOT NULL,
      body text NOT NULL,
      PRIMARY KEY (tenant_id, comment_id),
      FOREIGN KEY (tenant_id, document_id)
        REFERENCES documents (tenant_id, document_id)
    );
    
    CREATE INDEX comments_document_idx
      ON comments (tenant_id, document_id);

    The composite foreign key requires the referenced document to exist in the comment's tenant. The supporting index helps lookups by document and checks when a parent changes. A foreign key on document_id alone would lose that explicit tenant relationship. PostgreSQL describes these rules in its constraint documentation.

    These keys do not filter a SELECT statement. A role with unrestricted read access can still read other tenants' rows. The schema also does not enforce global uniqueness of document_id by itself; its uniqueness is scoped to tenant_id.

    Derive tenant scope from verified identity

    After authenticating a request, the server must verify that the caller belongs to the selected tenant and has the required operation-level permissions. Do not trust a tenant ID supplied in a URL, header, or form without that check.

    Use bound parameters for the subsequent query:

    sql
    SELECT document_id, title
    FROM documents
    WHERE tenant_id = $1 AND document_id = $2;

    Here $1 is the tenant ID already authorized by the server and $2 is the requested document ID. Parameter binding prevents values from becoming SQL syntax; it does not decide whether the caller has permission. Apply the same scope to writes, exports, background jobs, and cache keys.

    Add database enforcement where appropriate

    PostgreSQL row-level security can enforce row policies in addition to application checks. Once enabled, a table with no applicable policy denies normal row access. Table owners normally bypass RLS unless FORCE ROW LEVEL SECURITY is used; superusers and BYPASSRLS roles still bypass it. Run application requests with a suitably restricted role and test the policies under that role. See PostgreSQL row security.

    If a policy depends on session tenant context, the trusted application must set it from verified membership. A client able to choose that context can choose another tenant. Connection pooling also requires careful transaction scoping so one request's tenant context cannot carry into another request.

    RLS configuration depends on your authentication and connection model. The schema above intentionally demonstrates keys and relationships only; it is not a complete access-control implementation.

    Test isolation with two tenants

    Create tenants A and B, with a document in each. Authenticate as a user who belongs only to A, then try B's actual document UUID. Verify that reads, edits, deletes, exports, and comment creation are denied without disclosing B's data. Test real identifiers, not only nonexistent ones.

    Separately, attempt to insert a comment scoped to A that references a document existing only in B. The composite foreign key should reject it. Then confirm that a correctly scoped comment succeeds. This tests relationship integrity independently of request authorization.

    Also test a revoked membership and consecutive requests for different tenants through the same connection pool. Cover background jobs and administrative paths explicitly; they often use different credentials or query code.

    Keep identifiers and routing understandable

    Store tenant_id and resource ID as separate fields. A display prefix such as org_ can help humans recognize an identifier, but it is not an access-control mechanism and makes the full string something other than a standard UUID.

    For distributed systems, decide whether records route by tenant or resource ID. The sharding guide explains that trade-off. Use native or binary UUID storage consistently, and the UUID validator to inspect example identifiers.

    Generate Your Own UUIDs

    Ready to put this knowledge into practice? Try our UUID generators:

    Summary

    Design tenant-scoped UUID keys and relationships without confusing identifiers with authorization. Includes a PostgreSQL schema and cross-tenant test scenarios.

    TLDR;

    UUIDs help applications generate identifiers independently. They do not enforce tenant isolation.

    Authenticate the caller, verify tenant membership and object permissions, scope queries, and enforce database policies where appropriate. Composite keys protect relationships; they do not filter reads.