Skip to content

feat: auth provider contract — centralized identity + permissions (core)#138

Merged
olliethedev merged 3 commits into
mainfrom
feat/auth-provider-core
Jul 5, 2026
Merged

feat: auth provider contract — centralized identity + permissions (core)#138
olliethedev merged 3 commits into
mainfrom
feat/auth-provider-core

Conversation

@olliethedev

Copy link
Copy Markdown
Collaborator

Closes #128

Summary

  • Client contract (@btst/stack/context): optional auth prop on StackProvider taking a StackAuthProvider (getIdentity / can / loginPath). New useIdentity() and useCan() hooks and a <CanAccess resource action fallback loading> element-gating component. Without a provider, identity is null, all checks pass, and <CanAccess> renders children — behavior is identical to today.
  • Route gating mechanism: ComposedRoute accepts an optional permission={{ resource, action }}. With a provider configured, denied unauthenticated users are redirected to loginPath via the top-level router's navigate (from Top-level router adapter on StackProvider (framework presets for Link/navigate/refresh/Image) #127/feat: top-level router/api props on StackProvider with framework presets #134, window.location.assign fallback) while the route's LoadingComponent renders; denied authenticated users get an Unauthorized error thrown into the route's existing ErrorBoundary (same semantics as useRouteLifecycle). No plugin declares permissions yet — adoption happens per plugin in Phase 2: per-plugin adoption sweep of the new core primitives #136.
  • Server-side identity (@btst/stack/api): stack({ auth }) registers a lazy, per-request-memoized identity resolver keyed on the request's Headers instance (which better-call passes into every endpoint ctx). Lifecycle hooks read it with the new getRequestIdentity(ctx.headers) — the provider's getIdentity runs at most once per request. The provider is also exposed on the plugin StackContext. Without auth, the handler is returned untouched.
  • Docs: new "Auth Provider" concepts page with a better-auth wiring example for both client and server.

Scope notes

Test plan

  • pnpm build, pnpm typecheck, pnpm lint, pnpm -F @btst/stack knip pass locally
  • pnpm vitest run in packages/stack — 274 tests pass, including 25 new ones:
    • auth-provider.test.tsx: no-provider pass-through (incl. renderToString parity), identity resolution, can() allow/deny/error, <CanAccess> allow/deny/no-provider/loading, gated-route redirect to loginPath, authenticated-deny → ErrorBoundary
    • stack-auth.test.ts: memory-adapter stack() — per-request memoization (1 provider call for N reads), fresh resolution per request, ctx propagation, null without provider / outside a request
  • docs && pnpm build passes (new MDX page + AutoTypeTables)
  • Codegen E2E suites in CI (no template changes in this PR; must stay green)

Made with Cursor

Core-only phase of #128: optional auth providers on StackProvider (client)
and stack() (server). Adds useIdentity()/useCan() hooks, the <CanAccess>
element-gating component, a permission prop on ComposedRoute with loginPath
redirect through the top-level router, and per-request memoized identity
resolution exposed via getRequestIdentity() for lifecycle hooks. Fully
non-breaking: without a provider all code paths behave exactly as before.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented Jul 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
better-stack-docs Ready Ready Preview, Comment Jul 5, 2026 6:12pm
better-stack-playground Ready Ready Preview, Comment Jul 5, 2026 6:12pm

Request Review

Comment thread packages/stack/src/context/auth.tsx Outdated
Comment thread packages/stack/src/context/auth.tsx
Comment thread packages/stack/src/context/auth.tsx Outdated
Comment thread packages/stack/src/client/components/compose.tsx Outdated
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Shadcn registry validated — no registry changes detected.

- useCan no longer returns a result computed for a previous identity or
  check inputs: resolved state carries the identity/key it was computed
  for and anything else reads as pending, so <CanAccess>/RouteGuard never
  briefly expose the prior user's permissions after refetch()
- can() now receives the original params object (ref) instead of a
  JSON round-tripped copy, preserving Dates and other non-JSON values
- RouteGuard deny throws a generic "Unauthorized" and logs the
  resource/action detail as a dev-only warning

Co-authored-by: Cursor <cursoragent@cursor.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 330836a. Configure here.

Comment thread packages/stack/src/api/index.ts
Mirror the client StackAuthBoundary: a rejecting or throwing
getIdentity resolves getRequestIdentity to null (with a logged error)
instead of propagating and failing the whole request.

Co-authored-by: Cursor <cursoragent@cursor.com>
@olliethedev olliethedev merged commit 4213ae0 into main Jul 5, 2026
9 checks passed
@olliethedev olliethedev deleted the feat/auth-provider-core branch July 5, 2026 18:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auth provider contract: centralized identity + permissions (getIdentity/can) across client and backend

1 participant