Nts add auth
Skill juncoding/nextjs-trpc-prisma-starter/skills/nts-add-auth
Claude Code plugin: scaffold and maintain lightweight internal management systems on Next.js + tRPC + Prisma in SPA mode
npx -y skills add juncoding/nextjs-trpc-prisma-starter --skill nts-add-authAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Add Better Auth to an existing project scaffolded with nextjs-trpc-prisma-starter that initially skipped auth. Use this when the user says 'add auth', 'add login', 'wire Better Auth', 'add user accounts', invokes /nts-add-auth, or starts asking permission/session questions in a project without auth wired. Wires up the Better Auth Prisma adapter, session helper, requirePermission helper, /login page (credentials), the auth route handler at /api/auth/[...all], and the User / Session / Account / Verification Prisma models. Updates the tRPC context to include the session and switches the example router from publicProcedure to protectedProcedure.
SKILL.md
9.4 KB, as published. Nobody here has run it
Add Better Auth to an existing project
For projects scaffolded without auth that now need it. This is a substantial change — every tRPC procedure becomes auth-gated, and the schema gains four new tables.
Use when
- User wants user accounts / login / sessions.
- User says "add auth" / "wire Better Auth" / "we need permissions now".
- User invokes
/nts-add-auth.
Confirmation flow
Auth is a meaningful schema change. Before writing:
- Verify project structure (
prisma/schema.prisma,src/server/,package.jsonexist). - Confirm auth strategy:
- Credentials only (recommended) — email + password.
- Credentials + magic-link — adds Resend dependency.
- Credentials + OAuth providers (Google, GitHub, etc.) — adds provider configuration.
- Confirm the user has a place to put the bootstrap admin (sysadmin) — usually the seed script.
- Show all file changes. Confirm.
What this skill does
- Adds
better-authtopackage.json. - Adds Better Auth's required Prisma models —
User,Session,Account,Verification. - Creates a migration.
- Creates
src/server/auth/index.tswith the Better Auth config. - Creates
src/server/auth/session.tswithrequireSession()andgetSession()helpers. - Creates
src/server/auth/permissions.tswithrequirePermission()(RBAC). AddsRole,Permission,UserRole,RolePermissionPrisma models. - Creates
src/app/api/auth/[...all]/route.ts— Better Auth's catch-all handler. - Creates
src/app/(auth)/login/page.tsx— credentials login form. - Updates
src/server/api/trpc.ts— context now includes session; addsprotectedProcedure. - Updates existing tRPC routers — switches
publicProceduretoprotectedProcedurewhere appropriate. - Updates existing services — adds
userIdfirst parameter +requirePermissionif not already present. - Updates
prisma/seed.tsto seed default roles + permissions + a bootstrap admin. - Updates
CLAUDE.mdto document the auth layer.
Prisma models to add
model User {
id String @id @default(cuid())
email String @unique
emailVerified Boolean @default(false)
name String?
image String?
isSysadmin Boolean @default(false) @map("is_sysadmin")
status String @default("active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
sessions Session[]
accounts Account[]
userRoles UserRole[]
@@map("users")
}
model Session {
id String @id @default(cuid())
userId String @map("user_id")
token String @unique
expiresAt DateTime @map("expires_at")
ipAddress String? @map("ip_address")
userAgent String? @map("user_agent")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("sessions")
}
model Account {
id String @id @default(cuid())
userId String @map("user_id")
accountId String @map("account_id")
providerId String @map("provider_id")
accessToken String? @map("access_token")
refreshToken String? @map("refresh_token")
accessTokenExpiresAt DateTime? @map("access_token_expires_at")
refreshTokenExpiresAt DateTime? @map("refresh_token_expires_at")
scope String?
idToken String? @map("id_token")
password String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("accounts")
}
model Verification {
id String @id @default(cuid())
identifier String
value String
expiresAt DateTime @map("expires_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("verifications")
}
model Role {
id String @id @default(cuid())
name String @unique
description String?
createdAt DateTime @default(now()) @map("created_at")
permissions RolePermission[]
users UserRole[]
@@map("roles")
}
model Permission {
id String @id @default(cuid())
scope String @unique
roles RolePermission[]
@@map("permissions")
}
model UserRole {
userId String @map("user_id")
roleId String @map("role_id")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
@@id([userId, roleId])
@@map("user_roles")
}
model RolePermission {
roleId String @map("role_id")
permissionId String @map("permission_id")
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
@@id([roleId, permissionId])
@@map("role_permissions")
}
Files to create / modify
src/server/auth/index.ts (new)
import "server-only";
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { nextCookies } from "better-auth/next-js";
import { db } from "@/server/db/client";
export const auth = betterAuth({
database: prismaAdapter(db, { provider: "postgresql" }),
emailAndPassword: { enabled: true },
plugins: [nextCookies()],
});
src/server/auth/session.ts (new)
import "server-only";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { auth } from "./index";
export async function getSession() {
return auth.api.getSession({ headers: await headers() });
}
export async function requireSession() {
const session = await getSession();
if (!session) redirect("/login");
return session;
}
src/server/auth/permissions.ts (new)
import "server-only";
import { db } from "@/server/db/client";
import { ForbiddenError } from "@/server/lib/errors";
export type PermissionScope = string; // tighten with a union once you have your scope catalog
export async function requirePermission(userId: string, scope: PermissionScope) {
const user = await db.user.findUnique({ where: { id: userId }, select: { isSysadmin: true } });
if (user?.isSysadmin) return;
const allowed = await db.rolePermission.findFirst({
where: { role: { users: { some: { userId } } }, permission: { scope } },
select: { permissionId: true },
});
if (!allowed) throw new ForbiddenError(`Missing permission: ${scope}`);
}
src/app/api/auth/[...all]/route.ts (new)
import { auth } from "@/server/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth.handler);
src/app/(auth)/login/page.tsx (new)
Standard credentials form — name field + email + password — POSTs to Better Auth's /api/auth/sign-in/email. Use the Better Auth client (authClient.signIn.email({...})) if you prefer.
Update src/server/api/trpc.ts
Add session to context, expose protectedProcedure. See assets/trpc-server.ts.template from scaffold-internal-tool — the {{#IF_AUTH}} branch.
Update existing routers and services
Walk every router file in src/server/api/routers/ and replace publicProcedure with protectedProcedure where the procedure shouldn't be anonymous. Walk every service and verify the userId-first signature + requirePermission call.
Seed roles and permissions
Add to prisma/seed.ts:
async function seedRolesAndPermissions() {
const scopes = ["customers:read", "customers:write", /* ... */];
await Promise.all(
scopes.map((scope) =>
db.permission.upsert({ where: { scope }, update: {}, create: { scope } })
)
);
const admin = await db.role.upsert({
where: { name: "admin" }, update: {}, create: { name: "admin" },
});
// Wire all permissions to admin role
for (const scope of scopes) {
const perm = await db.permission.findUniqueOrThrow({ where: { scope } });
await db.rolePermission.upsert({
where: { roleId_permissionId: { roleId: admin.id, permissionId: perm.id } },
update: {}, create: { roleId: admin.id, permissionId: perm.id },
});
}
}
Verification
pnpm install
pnpm prisma migrate dev --name add_auth
pnpm db:seed
pnpm tsc --noEmit
pnpm build
Then guide the user to:
- Sign up a first user via the login page.
- Manually update the User row in Prisma Studio to set
isSysadmin = true. - Sign in — they should now have access to everything.
The bootstrap-admin step is manual to keep this skill stack-agnostic. Document the step in CLAUDE.md.