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.
In this repository42 other files it gives its agents.
- guidance/claude.mdCLAUDE.md
- factory-animationSkill
- factory-api-route-engineerSkill
- factory-apiSkill
- factory-authSkill
- factory-ciSkill
- factory-closeSkill
- factory-code-reviewerSkill
- factory-commitsSkill
- factory-data-layerSkill
- factory-data-pipeline-engineerSkill
- factory-data-pipelinesSkill
- factory-db-migration-engineerSkill
- factory-db-migrationSkill
- factory-db-schema-architectSkill
- factory-deploymentSkill
- factory-designSkill
- factory-entrySkill
- factory-feature-architectSkill
- factory-forms-builderSkill
- factory-formsSkill
- factory-frontend-engineerSkill
- factory-frontendSkill
- factory-kit-auditSkill
- factory-llm-workflow-engineerSkill
- factory-llm-workflowsSkill
- factory-observabilitySkill
- factory-pitfallsSkill
- factory-promptingSkill
- factory-promptSkill
- factory-releaseSkill
- factory-security-engineerSkill
- factory-securitySkill
- factory-setup-linearSkill
- factory-stackSkill
- factory-standupSkill
- factory-state-modelingSkill
- factory-submitSkill
- factory-testingSkill
- factory-verification-engineerSkill
- factory-verificationSkill
- factory-voiceSkill
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
- Distinctive: line 15
If the user has expressed a preference or the project already has a provider, defer to that.
- Very specific: line 17
Wrapper interface present?: Check for src/lib/auth/ (or equivalent).
- Distinctive: line 22
If the seam exists, use it.
- Distinctive: line 31
Multi-tenancy enforcement?: Every domain query / mutation must:
- Very specific: line 34
Never trust an orgId from the request body — always from the session
- Distinctive: line 42
Always verify RS256 signature against JWKS
- Very specific: line 48
Call requireAdmin() inside the wrapper before returning the client
- Distinctive: line 51
Role definition?: Don't put roles in code as string literals.
- Distinctive: line 155
Don't write auth code inline in routes / actions: Always through the wrapper interface.
- Very specific: line 156
Don't trust
orgIdfrom request body: Always from session. - Distinctive: line 157
Don't expose the admin client at module scope: Always wrap.
- Distinctive: line 167
If the user asks to change a single role name or add one new field to the user table, do it directly.
≈ “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.
Files
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.
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
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.

