Carrying Security Intent from the Database to GraphQL

In my previous post about the TiDB-GraphQL project, I covered the idea of treating the database schema as a design surface rather than an implementation detail. Security is one area where that idea becomes concrete.

How Database Access Is Commonly Handled

Many modern applications use a shared-credential database access model, where user identity is resolved at the application boundary (with mechanisms like OIDC and JWTs). The application then connects using a single database identity, through a connection pool, to the database.

In this model authorization is enforced in application code, middleware, or policy layers. By the time a query reaches the database, the database itself typically has no awareness of the end user. All user context has already been resolved elsewhere.

This model works well. It scales, fits managed database offerings, and keeps operational complexity of the database low. The tradeoff is that the database, the store of your application’s data, becomes largely passive from a security perspective.

Earlier Experiences with Database-Enforced Security

Earlier in my career, I worked on systems where security was handled differently. Rigorous access control at the database layer was required by our customers. In this model the users identity was explicitly used to connect with the database, and permissions were enforced directly through grants and schema design. In short, the database determined what each user could access, and those rules applied consistently regardless of how the data was reached.

That exact model does not translate cleanly to modern, cloud-native systems. Per-user database connections do not scale well, and they complicate the ability to use tools like connection pools. However, the underlying idea stuck with me. The database does not have to be a passive participant in security.

Preserving Security Intent

When access rules are enforced at the database layer, they become part of the schema’s intent. Tables, views, roles, and grants together describe not just structure, but who is allowed to see what.

One of my rules of thumb is that security controls should ideally be applied at the right level, and with the right granularity. In the case of the data held in a database, ensuring that the access to the data is enforced close to the data drives makes it much easier to manage that control. Without it, similar checks have to be reimplemented in multiple places, and the database no longer reflects the full set of assumptions about data access.

For this project, I was interested in seeing how we could enable access-control that is applied at the database level, to be surfaced up to the application itself. The goal is to ensure database-level access intent is preserved without abandoning modern authentication patterns, shared connection pools, or compromising the user experience?

Applying This in TiDB-GraphQL

TiDB-GraphQL supports two models for managing data access.

First, a shared-credential database access model can be used out of the box. This is a familiar pattern, and is easy to get up and running with.

The second approach is using TiDB’s Role Based Access Control to manage the access to the database. To deliver this, it integrates with modern identity mechanisms (like OIDC and JWTs), and it continues to rely on pooled database connections. What changes is how authenticated identity is carried from the application to the database.

With the RBAC integrated model, authorized users are mapped to database roles, and all the queries and mutations execute within that role context by switching roles on pooled connections. This means the database’s existing RBAC model is used to authorize data-level access, while the application remains responsible for authentication.

In practice, this means:

  • Identity is handled using standard authentication mechanisms
  • Database connections remain pooled and shared
  • Authorization is enforced using database roles
  • GraphQL reflects what the database permits, rather than redefining those rules again

A High-Level Architecture

At a high level, the flow looks like this:

  1. A user authenticates (using OIDC or a similar mechanism)
  2. TiDB-GraphQL validates the bearer token and loads claim data
  3. TiDB-GraphQL obtains DB connection from the DB pool and switches the connection to that role with SET ROLE
  4. Resolvers execute SQL under that role. TiDB enforces table/column access.

In this second model, the database enforces access directly, and the API surfaces the results. You can read more about this approach in the TiDB-GraphQL project’s authentication architecture doc page.

Some Tradeoffs

This approach introduces its own constraints. Role management requires care. Schema design and RBAC need to be treated as first-class concerns. Some authorization logic moves closer to the data layer, which may be unfamiliar for teams used to handling everything in application code.

For many applications, traditional a shared-credential approach will remain the suitable choice. However, for systems where data-level security matters, and where the database already encodes meaningful access boundaries, this approach offers an interesting alternative.