agentsclimarketplace

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.

Install
npx -y skills add nonlinear-xyz/factory-kit --skill factory-auth-wiring-specialist

Assembled 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)

  1. 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.

  2. 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.

  3. Procedure tiers? For tRPC projects:

    • publicProcedure — anyone
    • protectedProcedure = publicProcedure.use(requireAuth)
    • orgProcedure = protectedProcedure.use(requireOrg)

    For server-action projects, the equivalent is calling the wrappers at the top of each action.

  4. Multi-tenancy enforcement? Every domain query / mutation must:

    • Pull orgId from session via withOrgContext
    • Filter by orgId in the query / mutation
    • Never trust an orgId from the request body — always from the session
  5. OAuth flows?

    • Validate ?next= params with safeNext() — 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)
  6. 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)
  7. 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
  8. 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 orgId from 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 safeNext validation 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.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.