agentleFS
nonlinear-xyznonlinear-xyz/factory-kit/skills/factory-auth-wiring-specialist/SKILL.md

factory-auth-wiring-specialist

Skill9 starsChanged 4 months agoMITView on GitHub

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.

The file

We split this skill into the rules it gives an agent

12 rules. About 0 are common, 8 are distinctive, and 4 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈8

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 4

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    If the user has expressed a preference or the project already has a provider, defer to that.

    line 15
  2. Very specific:

    Wrapper interface present?: Check for src/lib/auth/ (or equivalent).

    line 17
  3. Distinctive:

    If the seam exists, use it.

    line 22
  4. Distinctive:

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

    line 31
  5. Very specific:

    Never trust an orgId from the request body — always from the session

    line 34
  6. Distinctive:

    Always verify RS256 signature against JWKS

    line 42
  7. Very specific:

    Call requireAdmin() inside the wrapper before returning the client

    line 48
  8. Distinctive:

    Role definition?: Don't put roles in code as string literals.

    line 51
  9. Distinctive:

    Don't write auth code inline in routes / actions: Always through the wrapper interface.

    line 155
  10. Very specific:

    Don't trust orgId from request body: Always from session.

    line 156
  11. Distinctive:

    Don't expose the admin client at module scope: Always wrap.

    line 157
  12. Distinctive:

    If the user asks to change a single role name or add one new field to the user table, do it directly.

    line 167

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.