Factory auth wiring specialist
Skill nonlinear-xyz/factory-kit/skills/factory-auth-wiring-specialist
Personal Claude Code factory — synthesized cross-build skills, specialist subagents, and slash commands. Phase 0.
npx -y skills add nonlinear-xyz/factory-kit --skill factory-auth-wiring-specialistAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 9 stars9 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
Use when wiring auth into a new project, switching auth providers, or adding role/org features. Carries the factory's auth conventions — the provider decision matrix (Better Auth + orgs primary, Supabase + RLS for RLS-heavy cases, Clerk for consumer/SSO), the unified `requireAuth` / `requireRole` / `withOrgContext` wrapper interface, procedure tier stacking, OAuth callback safety (`safeNext`), JWT signature verification with fallback user-linking, admin-client bypass guardrails, role-conditional post-login redirects. Produces auth code that fits the house seam — provider is a swap point, not a leak.
SKILL.md
6.6 KB, as published. Nobody here has run it
Apply the auth-wiring-specialist workflow. Wire auth using the factory's conventions, not generic provider-specific boilerplate. Load the canonical factory-auth and factory-security skills through the host's skill capability when needed.
How to think (in order)
-
Which provider? Apply the decision matrix:
- Better Auth + organization plugin — default. B2B with team/org concept.
- Supabase Auth + RLS — RLS is doing real work (multi-role partner/distributor, deeply branched authz).
- Clerk — consumer / SSO-heavy. Managed UI matters.
If the user has expressed a preference or the project already has a provider, defer to that. Otherwise pick from the matrix and flag.
-
Wrapper interface present? Check for
src/lib/auth/(or equivalent). The seam is:requireAuth() -> { user, session }requireRole(role) -> { user, session }withOrgContext(fn)— wraps an async function with org context
If the seam exists, use it. If not, create it before writing any new auth-touching code.
-
Procedure tiers? For tRPC projects:
publicProcedure— anyoneprotectedProcedure = publicProcedure.use(requireAuth)orgProcedure = protectedProcedure.use(requireOrg)
For server-action projects, the equivalent is calling the wrappers at the top of each action.
-
Multi-tenancy enforcement? Every domain query / mutation must:
- Pull
orgIdfrom session viawithOrgContext - Filter by
orgIdin the query / mutation - Never trust an
orgIdfrom the request body — always from the session
- Pull
-
OAuth flows?
- Validate
?next=params withsafeNext()— reject protocol-relative, non-relative URLs - Post-login redirect by role (admin →
/admin, rep →/submit, default/) - Skip
?next=for OAuth callback flows (too easy to weaponize on first sign-in)
- Validate
-
JWT verification? (Clerk / Supabase)
- Always verify RS256 signature against JWKS
- Cache JWKS in memory; refresh on signature failure
- Fallback user-linking on first request (create user record inline if webhook hasn't arrived)
-
Admin / service-role client?
- Wrap in
withAdmin(fn)— never expose at module scope - Call
requireAdmin()inside the wrapper before returning the client - See
factory-security.md
- Wrap in
-
Role definition? Don't put roles in code as string literals. Define an enum / const and reference it:
export const ROLES = ['owner', 'admin', 'member', 'guest'] as const; export type Role = (typeof ROLES)[number];
Reference: canonical wrapper file
// src/lib/auth/index.ts
import { auth } from './provider'; // Better Auth / Supabase / Clerk import
export class AuthError extends Error {
constructor(public reason: 'unauthenticated' | 'forbidden' | 'no_org_context') {
super(reason);
}
}
export async function requireAuth() {
const session = await auth.getSession();
if (!session) throw new AuthError('unauthenticated');
return { user: session.user, session };
}
export async function requireRole(role: Role) {
const ctx = await requireAuth();
if (!ctx.user.roles.includes(role)) throw new AuthError('forbidden');
return ctx;
}
export async function withOrgContext<T>(fn: (ctx: { orgId: string; user: User }) => Promise<T>): Promise<T> {
const { user, session } = await requireAuth();
const orgId = session.activeOrganizationId;
if (!orgId) throw new AuthError('no_org_context');
return fn({ orgId, user });
}
function safeNext(next: string | null): string {
if (!next) return '/';
if (next.startsWith('//')) return '/';
if (!next.startsWith('/')) return '/';
return next;
}
Reference: Better Auth plugin composition
// src/lib/auth/provider.ts
import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { organization, admin, magicLink, twoFactor } from 'better-auth/plugins';
import { db } from '@/db';
import { customAC, roles } from './access-control';
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: 'pg' }),
plugins: [
organization({ accessControl: customAC, roles }),
admin(),
magicLink({ sendMagicLink: async (data) => /* ... */ }),
twoFactor(),
],
});
Output format
## Restated request
<one sentence>
## Provider decision
- Picked: <Better Auth / Supabase / Clerk>
- Reason: <which criterion>
- Existing in project: <yes/no — if yes, defer; if no, proposed>
## Wrapper interface
- Status: <exists / will create>
- Files: src/lib/auth/index.ts, src/lib/auth/provider.ts, src/lib/auth/access-control.ts
## Files to create or modify
<bulleted with paths>
## Code
<actual code, organized by file>
## Multi-tenancy check
- Org middleware: <wired>
- Domain queries filter by orgId: <yes/no — list any that don't>
## Security check
- safeNext on redirects: <yes>
- JWT signature verification: <yes — if Clerk/Supabase>
- Admin client wrapped: <yes>
- Hardcoded allowlists: <none / flagged>
## Open questions
<things the user should confirm>
What you do NOT do
- Don't write auth code inline in routes / actions. Always through the wrapper interface.
- Don't trust
orgIdfrom request body. Always from session. - Don't expose the admin client at module scope. Always wrap.
- Don't stack three fallback auth paths for the same surface. Pick one per surface.
- Don't hardcode email allowlists in config. DB-backed members table.
- Don't decode JWTs without verifying signatures.
- Don't skip
safeNextvalidation on redirect params. - Don't 404 valid users when the webhook hasn't arrived. Fallback user-linking.
- Don't put roles as inline string literals. Const / enum / type.
When the request is too small for this framework
If the user asks to change a single role name or add one new field to the user table, do it directly. The framework is for wiring a new provider, swapping providers, or adding org/team features.