feat: Replace Generic CRUD Adapters with Domain-Driven Authentication Methods #1920

Closed
opened 2026-03-13 09:12:45 -05:00 by GiteaMirror · 3 comments
Owner

Originally created by @LoisDuplain on GitHub (Sep 13, 2025).

Is this suited for github?

  • Yes, this is suited for github

Better-auth currently uses 8 generic CRUD methods (create, update, findOne, findMany, delete, deleteMany, count, transaction) that can operate on ANY table with ANY conditions. This is like giving someone a master key to your entire database when they only need access to authentication data.

Concrete issues we're experiencing:

  1. Security Concerns

    • Adapters can theoretically query any table: adapter.findOne({ model: "sensitive_data", where: [...] })
    • No compile-time restrictions on which fields can be accessed
    • Generic where clauses allow arbitrary field queries
    • Increased SQL injection attack surface due to dynamic query building
  2. Developer Experience Pain

    • String-based model names ("user", "session") prone to typos with no IDE support
    • Heavy use of runtime type casting (as any, <T>) without compile-time guarantees
    • Complex 1000+ line abstraction layer that's hard to understand and debug
    • Unclear what database operations are actually needed for auth
  3. Performance Issues

    • Generic methods cannot optimize for specific authentication patterns
    • No guidance on which fields need indexing for auth operations
    • N+1 query problems due to generic patterns
    • Transaction overhead for operations that don't need it
  4. Plugin Complexity

    • Organization plugin creates 900+ lines wrapping generic methods
    • All plugins use the same generic interface, losing domain knowledge
    • Unclear dependencies between core auth and plugin requirements

Example of Current Pain

// Current: Error-prone, no type safety, can access anything
const user = await adapter.findOne({
  model: "user",  // String - typo prone
  where: [{ field: "email", value: email }]  // Can query ANY field
})

// What if someone writes:
const sensitiveData = await adapter.findOne({
  model: "admin_secrets",  // Nothing stops this!
  where: [{ field: "password", value: "anything" }]
})

Describe the solution you'd like

Replace generic CRUD with explicit, domain-driven authentication methods.

Proposed Core Interface (Only 10 Methods!)

interface CoreAuthAdapter {
  // User Operations (5 methods)
  createUser(data: CreateUserData): Promise<User>
  findUserById(id: string): Promise<User | null>
  findUserByEmail(email: string): Promise<User | null>
  updateUser(id: string, data: Partial<User>): Promise<User>
  deleteUser(userId: string): Promise<void>
  
  // Session Operations (3 methods)
  createSession(data: CreateSessionData): Promise<Session>
  findSessionByToken(token: string): Promise<SessionWithUser | null>
  deleteSession(sessionId: string): Promise<void>
  
  // OAuth Operations (2 methods)
  createUserWithOAuthAccount(user: CreateUserData, account: CreateAccountData): Promise<{user: User, account: Account}>
  findAccountByProvider(userId: string, providerId: string): Promise<Account | null>
}

Key Benefits

  1. Enhanced Security

    • Explicit boundaries: Each method has a clear, limited scope
    • No arbitrary queries: Cannot access unexpected tables or fields
    • Compile-time validation: TypeScript enforces correct usage
    • Reduced attack surface: No dynamic query building
  2. Better Developer Experience

    • Self-documenting: findUserByEmail(email) vs findOne({ model: "user", where: [...] })
    • IDE support: Full autocomplete and type checking
    • Easier to implement: 10 specific methods vs understanding generic system
    • Simpler debugging: Stack traces show exact auth operations
  3. Improved Performance

    • Database-specific optimizations: Adapters can optimize for specific operations
    • Clear indexing requirements: Know exactly which fields need indexes
    • Atomic operations: createUserWithOAuthAccount() eliminates transaction complexity
    • Reduced overhead: No generic query builders for simple operations
  4. Plugin Modularity

    • Optional implementation: Only implement methods for plugins you use
    • Clear contracts: Each plugin defines its adapter interface
    • Graceful degradation: Plugins can detect available features and adapt

Example Transformation

// Before: Generic, error-prone
const user = await adapter.create({
  model: "user",
  data: { email, name, emailVerified: false }
})

// After: Explicit, type-safe
const user = await adapter.createUser({
  email,
  name, 
  emailVerified: false
})

Plugin Support

// Organization plugin - only implement if using
interface OrganizationAdapter {
  createOrganization(data: CreateOrgData): Promise<Organization>
  findOrganizationById(id: string): Promise<Organization | null>
  addMemberToOrganization(orgId: string, userId: string, role: string): Promise<Member>
  // ... only what you need
}

// Plugins check if methods exist and adapt accordingly
if (!('createOrganization' in adapter)) {
  console.log('Organization features disabled - adapter does not support them')
}

Describe alternatives you've considered

1. Incremental Type Safety Improvements

  • What: Add better TypeScript types to existing generic methods
  • Why rejected: Still allows arbitrary queries, doesn't solve security or performance issues
  • Assessment: Addresses symptoms, not the root cause

2. Adapter Method Restrictions

  • What: Add runtime validation to limit which models/fields can be accessed
  • Why considered: Could improve security without changing interface
  • Why rejected: Complex validation logic, no compile-time safety, still maintains abstraction overhead

3. Wrapper Abstraction Layer

  • What: Keep generic methods but add domain-specific wrappers
  • Why considered: Backwards compatible, gradual migration path
  • Why rejected: Adds more complexity instead of removing it, doesn't solve performance issues

4. Plugin-Specific Adapter Interfaces

  • What: Each plugin defines its own adapter interface, core stays generic
  • Why considered: Addresses plugin concerns without changing core
  • Why rejected: Core authentication is too important for generic abstractions; inconsistent patterns

5. Query Builder Pattern

  • What: Replace generic methods with a fluent query builder API
  • Why considered: More expressive than current approach
  • Why rejected: Still allows arbitrary queries, adds complexity, doesn't improve security

6. Status Quo with Documentation

  • What: Keep current system but improve documentation and examples
  • Why considered: Zero implementation cost
  • Why rejected: Doesn't address fundamental architectural issues; security and performance problems remain

Why Domain-Driven Approach Wins: It's the only solution that simultaneously improves security, developer experience, performance, and maintainability while actually reducing complexity.

Additional context

Migration Strategy (Non-Breaking)

  1. Phase 1: Implement new interface alongside existing one
  2. Phase 2: Update official adapters (Prisma, Drizzle, Kysely, MongoDB)
  3. Phase 3: Gradual community migration with tooling support
  4. Phase 4: Deprecate old interface in future major version

Real-World Impact

Current Complexity:

  • 8 generic methods + 1000+ lines of abstraction
  • String-based models with runtime type casting
  • Complex plugin wrappers (org plugin: ~900 lines)

Proposed Simplicity:

  • 10 explicit methods with clear purposes
  • Compile-time type safety throughout
  • Plugins define exactly what they need

Community Benefits

  • Easier custom adapters: Clear contract vs understanding generic system
  • Better plugin ecosystem: Each plugin can optimize for its needs
  • Improved security posture: Compile-time validation catches issues early
  • Enhanced debugging: Specific method names in stack traces

Technical Precedents

  • Domain Repository Pattern: Widely adopted in .NET, Java ecosystems
  • GraphQL Resolvers: Explicit methods for specific data needs
  • REST vs Generic Query APIs: REST won for similar reasons (clarity, performance, security)

Open Questions for Discussion

  1. Should we provide automatic migration tooling?
  2. Which adapters should be prioritized for the new interface?
  3. How should plugins declare their adapter requirements?
  4. What's the ideal timeline for deprecating the old interface?
Originally created by @LoisDuplain on GitHub (Sep 13, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### Is your feature request related to a problem? Please describe. Better-auth currently uses 8 generic CRUD methods (`create`, `update`, `findOne`, `findMany`, `delete`, `deleteMany`, `count`, `transaction`) that can operate on ANY table with ANY conditions. This is like giving someone a master key to your entire database when they only need access to authentication data. **Concrete issues we're experiencing:** 1. **Security Concerns** - Adapters can theoretically query any table: `adapter.findOne({ model: "sensitive_data", where: [...] })` - No compile-time restrictions on which fields can be accessed - Generic `where` clauses allow arbitrary field queries - Increased SQL injection attack surface due to dynamic query building 2. **Developer Experience Pain** - String-based model names (`"user"`, `"session"`) prone to typos with no IDE support - Heavy use of runtime type casting (`as any`, `<T>`) without compile-time guarantees - Complex 1000+ line abstraction layer that's hard to understand and debug - Unclear what database operations are actually needed for auth 3. **Performance Issues** - Generic methods cannot optimize for specific authentication patterns - No guidance on which fields need indexing for auth operations - N+1 query problems due to generic patterns - Transaction overhead for operations that don't need it 4. **Plugin Complexity** - Organization plugin creates 900+ lines wrapping generic methods - All plugins use the same generic interface, losing domain knowledge - Unclear dependencies between core auth and plugin requirements ### Example of Current Pain ```typescript // Current: Error-prone, no type safety, can access anything const user = await adapter.findOne({ model: "user", // String - typo prone where: [{ field: "email", value: email }] // Can query ANY field }) // What if someone writes: const sensitiveData = await adapter.findOne({ model: "admin_secrets", // Nothing stops this! where: [{ field: "password", value: "anything" }] }) ``` ### Describe the solution you'd like **Replace generic CRUD with explicit, domain-driven authentication methods.** ### Proposed Core Interface (Only 10 Methods!) ```typescript interface CoreAuthAdapter { // User Operations (5 methods) createUser(data: CreateUserData): Promise<User> findUserById(id: string): Promise<User | null> findUserByEmail(email: string): Promise<User | null> updateUser(id: string, data: Partial<User>): Promise<User> deleteUser(userId: string): Promise<void> // Session Operations (3 methods) createSession(data: CreateSessionData): Promise<Session> findSessionByToken(token: string): Promise<SessionWithUser | null> deleteSession(sessionId: string): Promise<void> // OAuth Operations (2 methods) createUserWithOAuthAccount(user: CreateUserData, account: CreateAccountData): Promise<{user: User, account: Account}> findAccountByProvider(userId: string, providerId: string): Promise<Account | null> } ``` ### Key Benefits 1. **Enhanced Security** - **Explicit boundaries**: Each method has a clear, limited scope - **No arbitrary queries**: Cannot access unexpected tables or fields - **Compile-time validation**: TypeScript enforces correct usage - **Reduced attack surface**: No dynamic query building 2. **Better Developer Experience** - **Self-documenting**: `findUserByEmail(email)` vs `findOne({ model: "user", where: [...] })` - **IDE support**: Full autocomplete and type checking - **Easier to implement**: 10 specific methods vs understanding generic system - **Simpler debugging**: Stack traces show exact auth operations 3. **Improved Performance** - **Database-specific optimizations**: Adapters can optimize for specific operations - **Clear indexing requirements**: Know exactly which fields need indexes - **Atomic operations**: `createUserWithOAuthAccount()` eliminates transaction complexity - **Reduced overhead**: No generic query builders for simple operations 4. **Plugin Modularity** - **Optional implementation**: Only implement methods for plugins you use - **Clear contracts**: Each plugin defines its adapter interface - **Graceful degradation**: Plugins can detect available features and adapt ### Example Transformation ```typescript // Before: Generic, error-prone const user = await adapter.create({ model: "user", data: { email, name, emailVerified: false } }) // After: Explicit, type-safe const user = await adapter.createUser({ email, name, emailVerified: false }) ``` ### Plugin Support ```typescript // Organization plugin - only implement if using interface OrganizationAdapter { createOrganization(data: CreateOrgData): Promise<Organization> findOrganizationById(id: string): Promise<Organization | null> addMemberToOrganization(orgId: string, userId: string, role: string): Promise<Member> // ... only what you need } // Plugins check if methods exist and adapt accordingly if (!('createOrganization' in adapter)) { console.log('Organization features disabled - adapter does not support them') } ``` ### Describe alternatives you've considered ### 1. **Incremental Type Safety Improvements** - **What**: Add better TypeScript types to existing generic methods - **Why rejected**: Still allows arbitrary queries, doesn't solve security or performance issues - **Assessment**: Addresses symptoms, not the root cause ### 2. **Adapter Method Restrictions** - **What**: Add runtime validation to limit which models/fields can be accessed - **Why considered**: Could improve security without changing interface - **Why rejected**: Complex validation logic, no compile-time safety, still maintains abstraction overhead ### 3. **Wrapper Abstraction Layer** - **What**: Keep generic methods but add domain-specific wrappers - **Why considered**: Backwards compatible, gradual migration path - **Why rejected**: Adds more complexity instead of removing it, doesn't solve performance issues ### 4. **Plugin-Specific Adapter Interfaces** - **What**: Each plugin defines its own adapter interface, core stays generic - **Why considered**: Addresses plugin concerns without changing core - **Why rejected**: Core authentication is too important for generic abstractions; inconsistent patterns ### 5. **Query Builder Pattern** - **What**: Replace generic methods with a fluent query builder API - **Why considered**: More expressive than current approach - **Why rejected**: Still allows arbitrary queries, adds complexity, doesn't improve security ### 6. **Status Quo with Documentation** - **What**: Keep current system but improve documentation and examples - **Why considered**: Zero implementation cost - **Why rejected**: Doesn't address fundamental architectural issues; security and performance problems remain **Why Domain-Driven Approach Wins**: It's the only solution that simultaneously improves security, developer experience, performance, and maintainability while actually reducing complexity. ### Additional context ### Migration Strategy (Non-Breaking) 1. **Phase 1**: Implement new interface alongside existing one 2. **Phase 2**: Update official adapters (Prisma, Drizzle, Kysely, MongoDB) 3. **Phase 3**: Gradual community migration with tooling support 4. **Phase 4**: Deprecate old interface in future major version ### Real-World Impact **Current Complexity**: - 8 generic methods + 1000+ lines of abstraction - String-based models with runtime type casting - Complex plugin wrappers (org plugin: ~900 lines) **Proposed Simplicity**: - 10 explicit methods with clear purposes - Compile-time type safety throughout - Plugins define exactly what they need ### Community Benefits - **Easier custom adapters**: Clear contract vs understanding generic system - **Better plugin ecosystem**: Each plugin can optimize for its needs - **Improved security posture**: Compile-time validation catches issues early - **Enhanced debugging**: Specific method names in stack traces ### Technical Precedents - **Domain Repository Pattern**: Widely adopted in .NET, Java ecosystems - **GraphQL Resolvers**: Explicit methods for specific data needs - **REST vs Generic Query APIs**: REST won for similar reasons (clarity, performance, security) ### Open Questions for Discussion 1. Should we provide automatic migration tooling? 2. Which adapters should be prioritized for the new interface? 3. How should plugins declare their adapter requirements? 4. What's the ideal timeline for deprecating the old interface?
GiteaMirror added the adapter label 2026-03-13 09:12:45 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Sep 13, 2025):

Hi @LoisDuplain! I'm Dosu and I’m helping the better-auth team.

Your proposal to replace generic CRUD adapters with domain-driven authentication methods is well-supported by both architectural analysis and community feedback. The current generic adapter interface in better-auth is flexible but introduces security risks, type safety issues, and complexity, as it allows arbitrary access to any table or field via string-based model names and dynamic where clauses. This has been a recurring concern in the community, especially for supporting NoSQL databases and for simplifying adapter implementations—see issue #113 and issue #1435.

There is strong support for moving to explicit, domain-driven interfaces (e.g., createUser, findUserByEmail), which would improve security, developer experience, and performance, and make it easier to support a wider range of databases. Community proposals also suggest modularizing adapters by data type (user, session, account), which aligns with your suggested interface and would lower the barrier for custom adapters and plugin development.

However, this migration will be non-trivial. It will require introducing new contracts, refactoring the core abstraction layer, updating all official adapters, and coordinating plugin changes. There are no existing migration tools or official plans for this transition, but there is clear interest in migration tooling and phased migration strategies, as seen in issue #2402 and related threads.

For next steps, consider drafting a design RFC to formalize the new interface, prototyping a pilot adapter, and gathering community feedback on migration tooling, adapter prioritization, and plugin requirements. A phased migration—supporting both interfaces in parallel before deprecating the old one—has precedent and community backing.

If you want more details on migration strategies, community proposals, or technical precedents, let me know!

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

@dosubot[bot] commented on GitHub (Sep 13, 2025): <!-- Greeting --> Hi @LoisDuplain! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> Your proposal to replace generic CRUD adapters with domain-driven authentication methods is well-supported by both architectural analysis and community feedback. The current generic adapter interface in better-auth is flexible but introduces security risks, type safety issues, and complexity, as it allows arbitrary access to any table or field via string-based model names and dynamic where clauses. This has been a recurring concern in the community, especially for supporting NoSQL databases and for simplifying adapter implementations—see [issue #113](https://github.com/better-auth/better-auth/issues/113) and [issue #1435](https://github.com/better-auth/better-auth/issues/1435). There is strong support for moving to explicit, domain-driven interfaces (e.g., `createUser`, `findUserByEmail`), which would improve security, developer experience, and performance, and make it easier to support a wider range of databases. Community proposals also suggest modularizing adapters by data type (user, session, account), which aligns with your suggested interface and would lower the barrier for custom adapters and plugin development. However, this migration will be non-trivial. It will require introducing new contracts, refactoring the core abstraction layer, updating all official adapters, and coordinating plugin changes. There are no existing migration tools or official plans for this transition, but there is clear interest in migration tooling and phased migration strategies, as seen in [issue #2402](https://github.com/better-auth/better-auth/issues/2402) and related threads. For next steps, consider drafting a design RFC to formalize the new interface, prototyping a pilot adapter, and gathering community feedback on migration tooling, adapter prioritization, and plugin requirements. A phased migration—supporting both interfaces in parallel before deprecating the old one—has precedent and community backing. If you want more details on migration strategies, community proposals, or technical precedents, let me know! <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/ca372e97-b5f8-47c8-b98b-c277671c0e7f?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/ca372e97-b5f8-47c8-b98b-c277671c0e7f?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/ca372e97-b5f8-47c8-b98b-c277671c0e7f?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/ca372e97-b5f8-47c8-b98b-c277671c0e7f?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/ca372e97-b5f8-47c8-b98b-c277671c0e7f?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/ca372e97-b5f8-47c8-b98b-c277671c0e7f?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/ca372e97-b5f8-47c8-b98b-c277671c0e7f?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/4638)
Author
Owner

@maulik13 commented on GitHub (Sep 14, 2025):

The examples you have mentioned on accessing anything from adapter is a backend code that is a responsibility of the developer. This should be an acceptable risk. Consider that the same developer who wrote a query on admin_secrets table using adapter could do the same using any orm or db query. Typos and security concern (from your example) are not really as severe as they sound.

There is definitely some benefits to the approach you suggest. It does provide more concrete intention of each action, and also provides easier reusability. Some of the points about type handling are also valid. It it is hard to replace adapter pattern to support multiple databases since we do not want plugin authors to deal with it. Having wrapper methods for some of the core parts (user, session, etc.) would be useful which can be done without a huge change.

It would be useful to see an example implementation of one part of the library (e.g. organization plugin) to be really able to see the benefits.

@maulik13 commented on GitHub (Sep 14, 2025): The examples you have mentioned on accessing anything from adapter is a backend code that is a responsibility of the developer. This should be an acceptable risk. Consider that the same developer who wrote a query on `admin_secrets` table using adapter could do the same using any orm or db query. Typos and security concern (from your example) are not really as severe as they sound. There is definitely some benefits to the approach you suggest. It does provide more concrete intention of each action, and also provides easier reusability. Some of the points about type handling are also valid. It it is hard to replace adapter pattern to support multiple databases since we do not want plugin authors to deal with it. Having wrapper methods for some of the core parts (user, session, etc.) would be useful which can be done without a huge change. It would be useful to see an example implementation of one part of the library (e.g. organization plugin) to be really able to see the benefits.
Author
Owner

@Bekacru commented on GitHub (Sep 14, 2025):

This isn’t a security risk as @maulik13 mentioned, but your solution doesn’t address the risk either.

The reason we don’t do it this way is because it would require every plugin to implement 20 different queries across every ORM flavor just to make those db queries. As you can imagine, that’s not practical. If this were a library like next auth, where there’s only a small set of queries that mostly stay the same, then it’d be doable. But in our case, we have way too many queries to manage like that.

@Bekacru commented on GitHub (Sep 14, 2025): This isn’t a security risk as @maulik13 mentioned, but your solution doesn’t address the risk either. The reason we don’t do it this way is because it would require every plugin to implement 20 different queries across every ORM flavor just to make those db queries. As you can imagine, that’s not practical. If this were a library like next auth, where there’s only a small set of queries that mostly stay the same, then it’d be doable. But in our case, we have way too many queries to manage like that.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#1920