better-auth

📁 blogic-cz/blogic-marketplace 📅 1 day ago
1
总安装量
1
周安装量
#76299
全站排名
安装命令
npx skills add https://github.com/blogic-cz/blogic-marketplace --skill better-auth

Agent 安装分布

mcpjam 1
claude-code 1
replit 1
junie 1
windsurf 1
zencoder 1

Skill 文档

Better Auth Patterns

Overview

Implement authentication and authorization using Better Auth with TRPC procedures following the project’s established patterns.

When to Use This Skill

  • Configuring Better Auth settings
  • Creating protected TRPC procedures
  • Implementing organization/project access control
  • Working with sessions and user roles
  • Setting up OAuth providers

Auth Configuration

// apps/web-app/src/auth/auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { admin, organization } from "better-auth/plugins";

export const auth = betterAuth({
  baseURL: serverEnv.BETTER_AUTH_URL,
  secret: serverEnv.BETTER_AUTH_SECRET,
  database: drizzleAdapter(db, {
    provider: "pg",
    schema: {
      user: usersTable,
      session: sessionsTable,
      account: accountsTable,
      verification: verificationsTable,
      organization: organizationsTable,
      member: membersTable,
      invitation: invitationsTable,
    },
  }),
  session: {
    cookieCache: {
      enabled: true,
      maxAge: 5 * 60, // 5 minutes cache
    },
  },
  socialProviders: {
    google: {
      clientId: serverEnv.GOOGLE_CLIENT_ID,
      clientSecret: serverEnv.GOOGLE_CLIENT_SECRET,
    },
  },
  emailAndPassword: { enabled: true },
  plugins: [
    admin(),
    organization({ sendInvitationEmail: async () => {} }),
  ],
});

Auth-Related Database Tables

Core Tables:

  • usersTable – User accounts with role field (admin | user)
  • sessionsTable – Auth sessions with activeOrganizationId
  • accountsTable – OAuth accounts (stores access/refresh tokens)

Organization Tables:

  • organizationsTable – Organizations
  • membersTable – Organization members with role (owner | admin | member)
  • invitationsTable – Pending invitations

Project Tables:

  • projectsTable – Projects (belong to organizations)
  • projectMembersTable – Project members with role (admin | editor | viewer)

TRPC Context & Session

// apps/web-app/src/infrastructure/trpc/init.ts
export const createTRPCContext = async ({
  headers,
}: {
  headers: Headers;
}) => {
  const session = await auth.api.getSession({ headers });
  return { db, session, headers };
};

Protected Procedure Patterns

Base Auth Procedures

// apps/web-app/src/infrastructure/trpc/procedures/auth.ts
const enforceUserIsAuthenticated = t.middleware(
  ({ ctx, next }) => {
    if (!ctx.session?.user) throw unauthorizedError();
    return next({
      ctx: {
        session: { ...ctx.session, user: ctx.session.user },
        userId: ctx.session.user.id as UserId,
      },
    });
  }
);

const enforceUserIsAdmin = t.middleware(
  async ({ ctx, next }) => {
    if (
      !ctx.session?.user ||
      ctx.session.user.role !== "admin"
    )
      throw unauthorizedError();
    return next({ ctx });
  }
);

export const publicProcedure = t.procedure
  .use(debugMiddleware)
  .use(sentryMiddleware);
export const protectedProcedure = publicProcedure.use(
  enforceUserIsAuthenticated
);
export const adminProcedure = publicProcedure.use(
  enforceUserIsAdmin
);

Organization Access Procedures

// apps/web-app/src/infrastructure/trpc/procedures/organization.ts
import { OrganizationId } from "@project/common";

// Member access - any org member
export const protectedOrganizationMemberProcedure =
  protectedProcedure
    .input(
      Schema.standardSchemaV1(
        Schema.Struct({ organizationId: OrganizationId })
      )
    )
    .use(async function isMemberOfOrganization(opts) {
      const memberAccess = await opts.ctx.db
        .select()
        .from(membersTable)
        .where(
          and(
            eq(
              membersTable.organizationId,
              opts.input.organizationId
            ),
            eq(membersTable.userId, opts.ctx.userId)
          )
        )
        .limit(1);

      if (memberAccess.length === 0)
        throw forbiddenError(
          "You are not a member of this organization"
        );

      return opts.next({
        ctx: {
          member: memberAccess[0],
          organizationId: opts.input.organizationId,
        },
      });
    });

// Admin access - org admin/owner only
export const protectedOrganizationAdminProcedure =
  protectedProcedure
    .input(
      Schema.standardSchemaV1(
        Schema.Struct({ organizationId: OrganizationId })
      )
    )
    .use(async function isAdminOfOrganization(opts) {
      const memberAccess = await opts.ctx.db
        .select()
        .from(membersTable)
        .where(
          and(
            eq(
              membersTable.organizationId,
              opts.input.organizationId
            ),
            eq(membersTable.userId, opts.ctx.userId),
            or(
              eq(membersTable.role, "admin"),
              eq(membersTable.role, "owner")
            )
          )
        )
        .limit(1);

      if (memberAccess.length === 0)
        throw forbiddenError("Admin access required");

      return opts.next({
        ctx: {
          member: memberAccess[0],
          organizationId: opts.input.organizationId,
        },
      });
    });

Project Access Procedures

// apps/web-app/src/infrastructure/trpc/procedures/project-access.ts

// Single optimized query - checks both org and project membership
export const protectedProjectMemberProcedure =
  protectedProcedure
    .input(
      Schema.standardSchemaV1(
        Schema.Struct({ projectId: ProjectId })
      )
    )
    .use(async function hasProjectAccess(opts) {
      const result = await ctx.db
        .select({
          projectId: projectsTable.id,
          organizationId: projectsTable.organizationId,
          orgMemberRole: membersTable.role,
          projectMemberRole: projectMembersTable.role,
        })
        .from(projectsTable)
        .leftJoin(
          membersTable,
          and(
            eq(
              membersTable.organizationId,
              projectsTable.organizationId
            ),
            eq(membersTable.userId, ctx.userId)
          )
        )
        .leftJoin(
          projectMembersTable,
          and(
            eq(
              projectMembersTable.projectId,
              projectsTable.id
            ),
            eq(projectMembersTable.userId, ctx.userId)
          )
        )
        .where(eq(projectsTable.id, projectId));

      // Org admins get automatic project admin access
      const isOrgAdmin =
        data.orgMemberRole === "admin" ||
        data.orgMemberRole === "owner";
      if (isOrgAdmin) {
        return opts.next({
          ctx: {
            project,
            projectRole: "admin",
            orgRole: data.orgMemberRole,
          },
        });
      }
      // Check explicit project membership...
    });

// Chained procedure for admin-only
export const protectedProjectAdminProcedure =
  protectedProjectMemberProcedure.use(
    async function requiresProjectAdmin(opts) {
      if (
        ctx.orgRole === "admin" ||
        ctx.orgRole === "owner" ||
        ctx.projectRole === "admin"
      )
        return opts.next({ ctx });
      throw forbiddenError(
        "Project admin permissions required"
      );
    }
  );

Available Procedures Summary

Procedure Access Level Context Provided
publicProcedure No auth { db, session?, headers }
protectedProcedure Authenticated { db, session, userId, headers }
adminProcedure Admin role { db, session, headers }
protectedOrganizationMemberProcedure Org member { ..., member, organizationId }
protectedOrganizationAdminProcedure Org admin/owner { ..., member, organizationId }
protectedProjectMemberProcedure Project access { ..., project, projectRole, orgRole }
protectedProjectAdminProcedure Project admin { ..., project, projectRole, orgRole }
protectedProjectEditorProcedure Project editor+ { ..., project, projectRole, orgRole }

Client-Side Auth

// apps/web-app/src/auth/auth-client.ts
import { createAuthClient } from "better-auth/react";
import {
  adminClient,
  organizationClient,
} from "better-auth/client/plugins";

export const authClient = createAuthClient({
  betterAuthBaseUrl,
  plugins: [adminClient(), organizationClient()],
});

export const { signIn, signOut, useSession, getSession } =
  authClient;

// Sign in with redirect
export const signInWithEmail = async (
  email: string,
  password: string,
  callbackURL = "/app/dashboard"
) => {
  return withAuthRedirect(
    (callbacks) =>
      signIn.email({ email, password }, callbacks),
    callbackURL
  );
};

export const signInWithGoogle = async (
  callbackURL = "/app/dashboard"
) => {
  return signIn.social({ provider: "google", callbackURL });
};

Admin API Usage

// Using Better Auth server API in TRPC procedures
export const router = {
  setUserAdmin: adminProcedure.mutation(
    async ({ ctx, input }) => {
      const users = await auth.api.listUsers({
        headers: ctx.headers,
        query: {
          searchField: "email",
          searchValue: input.email,
        },
      });
      await auth.api.setRole({
        headers: ctx.headers,
        body: {
          userId: user.id,
          role: input.isAdmin ? "admin" : "user",
        },
      });
    }
  ),

  banUser: adminProcedure.mutation(
    async ({ ctx, input }) => {
      await auth.api.banUser({
        headers: ctx.headers,
        body: { userId: input.userId, banReason },
      });
    }
  ),
};

Key Rules

  1. Use appropriate procedure for access level needed
  2. Org admins get automatic project access – don’t duplicate checks
  3. Single query for access checks – use JOINs, not multiple queries
  4. Pass headers to auth.api calls for session context
  5. Chain procedures for more specific access (e.g., protectedProjectAdminProcedure)