Skip to content

[v3 auth] Preserve application identity types across client and server auth #172

Description

@olliethedev

Parent

Migration experience

A representative v2-to-v3 RC2 migration replaced scattered session checks with one application authorization policy shared by the client StackProvider and the server stack() configuration. The application identity included a role in addition to the standard id, name, email, and image fields.

At runtime this worked correctly. At authoring time, however, BTST widened the identity back to StackIdentity. Provider-specific fields are allowed through an index signature, but they become unknown. The migration therefore introduced an application identity extension and casts inside the permission policy to recover the role type.

That is an unfortunate place to lose type information: v3 centralizes authorization specifically so application policy can become easier to write, review, and reuse.

Problem

The identity type does not flow through the auth surface. A consumer can return a richer user from getIdentity, but the following public seams expose only the base StackIdentity:

  • client and server auth providers;
  • the identity argument passed to can;
  • StackProvider's auth context;
  • useIdentity() and permission hooks/components; and
  • auth bridges such as createBetterAuthProvider.

As a result, custom claims such as roles, tenant ids, account status, or permission sets require assertions or local wrapper types. Those assertions can drift between client and server and weaken the benefit of sharing one policy.

The base identity remains valuable as a portable minimum contract. The missing piece is a backward-compatible way to specialize it once and retain that type end to end.

What to build

Add an application identity generic, inference path, or equivalent typed factory that preserves a consumer's StackIdentity extension through both client and server auth APIs.

The preferred experience is to declare or infer the identity once. The same concrete identity should then be visible in getIdentity, can, useIdentity, useCan/CanAccess policy evaluation, request identity helpers, and official auth adapters without repeated generic arguments or casts at every call site.

The design must retain the current non-generic API as a compatible default. This issue should not couple the core package to Better Auth's session type or to any particular role model.

Definition of success

A consumer with an identity such as { id: string; role: "user" | "admin"; tenantId: string } can implement one shared authorization policy. Both the client and server versions of that policy receive the exact identity type, including nullable unauthenticated state, without as, index-access narrowing, or duplicated wrapper interfaces.

A mismatch between the object returned by getIdentity and the configured application identity type should fail during typecheck.

Acceptance criteria

  • The public client auth provider can preserve a consumer-defined identity subtype in getIdentity and can.
  • The public server auth provider and request identity lookup preserve the same subtype.
  • StackProvider, useIdentity, useCan, and CanAccess participate in the typed path without forcing repetitive annotations at each use site.
  • Official auth bridges preserve or infer the authenticated user's extension fields when their source client exposes those types.
  • null remains the only unauthenticated identity state and is represented accurately throughout the generic API.
  • Existing consumers that use plain StackIdentity compile unchanged.
  • Type tests demonstrate role and tenant fields flowing through client and server policies and include negative tests for incompatible identity returns.
  • Runtime behavior, identity memoization, and permissive behavior when auth is omitted remain unchanged.
  • The auth reference documents the base identity contract and the application-identity extension path with a client/server shared-policy example.
  • Package typecheck, auth unit tests, and docs build pass.

Blocked by

  • None — can start immediately.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions