@folpe/loom 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -16
- package/data/agents/backend/AGENT.md +35 -3
- package/data/agents/database/AGENT.md +16 -4
- package/data/agents/frontend/AGENT.md +39 -5
- package/data/agents/marketing/AGENT.md +19 -2
- package/data/agents/orchestrator/AGENT.md +3 -21
- package/data/agents/performance/AGENT.md +9 -2
- package/data/agents/review-qa/AGENT.md +7 -3
- package/data/agents/security/AGENT.md +10 -4
- package/data/agents/tests/AGENT.md +11 -2
- package/data/agents/ux-ui/AGENT.md +16 -3
- package/data/presets/api-backend.yaml +2 -11
- package/data/presets/chrome-extension.yaml +2 -11
- package/data/presets/cli-tool.yaml +2 -10
- package/data/presets/e-commerce.yaml +2 -14
- package/data/presets/expo-mobile.yaml +2 -12
- package/data/presets/fullstack-auth.yaml +2 -14
- package/data/presets/landing-page.yaml +2 -11
- package/data/presets/mvp-lean.yaml +2 -12
- package/data/presets/saas-default.yaml +5 -14
- package/data/presets/saas-full.yaml +58 -0
- package/data/skills/api-design/SKILL.md +43 -2
- package/data/skills/auth-rbac/SKILL.md +179 -0
- package/data/skills/better-auth-patterns/SKILL.md +212 -0
- package/data/skills/chrome-extension-patterns/SKILL.md +13 -6
- package/data/skills/cli-development/SKILL.md +11 -3
- package/data/skills/drizzle-patterns/SKILL.md +166 -0
- package/data/skills/env-validation/SKILL.md +142 -0
- package/data/skills/form-validation/SKILL.md +169 -0
- package/data/skills/hero-copywriting/SKILL.md +12 -4
- package/data/skills/i18n-patterns/SKILL.md +176 -0
- package/data/skills/layered-architecture/SKILL.md +131 -0
- package/data/skills/nextjs-conventions/SKILL.md +46 -7
- package/data/skills/react-native-patterns/SKILL.md +10 -8
- package/data/skills/react-query-patterns/SKILL.md +193 -0
- package/data/skills/resend-email/SKILL.md +181 -0
- package/data/skills/seo-optimization/SKILL.md +10 -2
- package/data/skills/server-actions-patterns/SKILL.md +156 -0
- package/data/skills/shadcn-ui/SKILL.md +46 -12
- package/data/skills/stripe-integration/SKILL.md +11 -3
- package/data/skills/supabase-patterns/SKILL.md +23 -6
- package/data/skills/table-pagination/SKILL.md +224 -0
- package/data/skills/tailwind-patterns/SKILL.md +12 -2
- package/data/skills/testing-patterns/SKILL.md +203 -0
- package/data/skills/ui-ux-guidelines/SKILL.md +10 -5
- package/dist/index.js +451 -122
- package/package.json +2 -1
|
@@ -27,26 +27,14 @@ constitution:
|
|
|
27
27
|
- Payment security — never handle raw card data, always use Stripe Elements
|
|
28
28
|
- SEO drives traffic — product pages must be indexable with structured data
|
|
29
29
|
- Performance converts — every 100ms of load time impacts conversion rate
|
|
30
|
-
stack:
|
|
31
|
-
- Next.js 16+ (App Router)
|
|
32
|
-
- React 19
|
|
33
|
-
- TypeScript 5 (strict)
|
|
34
|
-
- Tailwind CSS 4
|
|
35
|
-
- ShadCN UI
|
|
36
|
-
- Supabase (auth + database)
|
|
37
|
-
- Stripe (payments)
|
|
38
|
-
- Vercel (deployment)
|
|
39
30
|
conventions:
|
|
40
31
|
- Use Stripe Checkout or Payment Elements — never collect card details directly
|
|
41
32
|
- Implement webhook handlers for Stripe events (payment success, refund, etc.)
|
|
42
33
|
- Use ISR or SSG for product catalog pages for performance
|
|
43
34
|
- Add JSON-LD structured data for products (price, availability, reviews)
|
|
44
35
|
- Implement optimistic cart updates with server validation
|
|
45
|
-
|
|
36
|
+
context:
|
|
46
37
|
projectDescription: >
|
|
47
|
-
This is an e-commerce application
|
|
38
|
+
This is an e-commerce application. It includes product catalog, shopping
|
|
48
39
|
cart, Stripe checkout, and transactional emails. The orchestrator coordinates the full agent
|
|
49
40
|
team including marketing for copywriting and SEO.
|
|
50
|
-
orchestratorRef: >
|
|
51
|
-
The orchestrator agent (.claude/agents/orchestrator.md) is the main entry point. Always start
|
|
52
|
-
by delegating to the orchestrator, which will route tasks to the appropriate specialized agent.
|
|
@@ -20,25 +20,15 @@ constitution:
|
|
|
20
20
|
- Offline-first — the app must be usable without network connectivity
|
|
21
21
|
- Battery conscious — minimize background tasks and network requests
|
|
22
22
|
- Smooth animations — target 60fps, never block the JS thread
|
|
23
|
-
stack:
|
|
24
|
-
- Expo SDK 52+
|
|
25
|
-
- React Native
|
|
26
|
-
- TypeScript 5 (strict)
|
|
27
|
-
- NativeWind (Tailwind for React Native)
|
|
28
|
-
- Expo Router (file-based navigation)
|
|
29
|
-
- Supabase (auth + database)
|
|
30
23
|
conventions:
|
|
31
24
|
- Use Expo Router for all navigation — file-based routing
|
|
32
25
|
- Implement offline storage with async-storage or MMKV for critical data
|
|
33
26
|
- Use expo-notifications for push notifications setup
|
|
34
27
|
- Handle deep linking and universal links from the start
|
|
35
28
|
- Test on both iOS and Android — never assume platform parity
|
|
36
|
-
|
|
29
|
+
context:
|
|
37
30
|
projectDescription: >
|
|
38
|
-
This is a mobile application
|
|
31
|
+
This is a mobile application using Expo and React Native. It uses
|
|
39
32
|
NativeWind for styling and Expo Router for navigation. The orchestrator delegates mobile UI
|
|
40
33
|
to the frontend agent, API integration to the backend agent, and native feature testing
|
|
41
34
|
to the tests agent.
|
|
42
|
-
orchestratorRef: >
|
|
43
|
-
The orchestrator agent (.claude/agents/orchestrator.md) is the main entry point. Always start
|
|
44
|
-
by delegating to the orchestrator, which will route tasks to the appropriate specialized agent.
|
|
@@ -23,26 +23,14 @@ constitution:
|
|
|
23
23
|
- Least privilege — users and services get only the permissions they need
|
|
24
24
|
- Defense in depth — middleware, RLS, and API validation work together
|
|
25
25
|
- Session management — handle expiry, refresh, and revocation properly
|
|
26
|
-
stack:
|
|
27
|
-
- Next.js 16+ (App Router)
|
|
28
|
-
- React 19
|
|
29
|
-
- TypeScript 5 (strict)
|
|
30
|
-
- Tailwind CSS 4
|
|
31
|
-
- ShadCN UI
|
|
32
|
-
- Supabase Auth (email, OAuth, magic link)
|
|
33
|
-
- Supabase (database with RLS)
|
|
34
|
-
- Vercel (deployment)
|
|
35
26
|
conventions:
|
|
36
27
|
- Use Supabase Auth for all authentication flows
|
|
37
28
|
- Implement middleware for route protection and role checks
|
|
38
29
|
- Enable Row Level Security on all tables — no exceptions
|
|
39
30
|
- Use server-side session validation, never trust client-only auth state
|
|
40
31
|
- Separate admin and user dashboards with distinct layouts
|
|
41
|
-
|
|
32
|
+
context:
|
|
42
33
|
projectDescription: >
|
|
43
|
-
This is a fullstack SaaS with complete authentication
|
|
34
|
+
This is a fullstack SaaS with complete authentication. It includes email,
|
|
44
35
|
OAuth, and magic link flows via Supabase Auth, plus RBAC and admin dashboard. The orchestrator
|
|
45
36
|
coordinates security, frontend, backend, and database agents for auth-related tasks.
|
|
46
|
-
orchestratorRef: >
|
|
47
|
-
The orchestrator agent (.claude/agents/orchestrator.md) is the main entry point. Always start
|
|
48
|
-
by delegating to the orchestrator, which will route tasks to the appropriate specialized agent.
|
|
@@ -19,23 +19,14 @@ constitution:
|
|
|
19
19
|
- Performance is UX — aim for perfect Lighthouse scores
|
|
20
20
|
- Mobile-first responsive — design for small screens, enhance for large
|
|
21
21
|
- Accessible by default — semantic HTML, ARIA, keyboard navigation
|
|
22
|
-
stack:
|
|
23
|
-
- Next.js 16+ (static export)
|
|
24
|
-
- React 19
|
|
25
|
-
- TypeScript 5 (strict)
|
|
26
|
-
- Tailwind CSS 4
|
|
27
|
-
- Vercel (deployment)
|
|
28
22
|
conventions:
|
|
29
23
|
- Use Server Components and static generation for maximum performance
|
|
30
24
|
- Optimize all images with next/image and proper srcset
|
|
31
25
|
- Implement structured data (JSON-LD) for SEO
|
|
32
26
|
- Use CSS animations and transitions over JS-based animation libraries
|
|
33
27
|
- Ensure all interactive elements have visible focus states
|
|
34
|
-
|
|
28
|
+
context:
|
|
35
29
|
projectDescription: >
|
|
36
|
-
This is a landing page / marketing site
|
|
30
|
+
This is a landing page / marketing site. It focuses on SEO, conversion
|
|
37
31
|
optimization, and fast loading. The orchestrator delegates visual tasks to the frontend and
|
|
38
32
|
ux-ui agents, copywriting to marketing, and performance audits to the performance agent.
|
|
39
|
-
orchestratorRef: >
|
|
40
|
-
The orchestrator agent (.claude/agents/orchestrator.md) is the main entry point. Always start
|
|
41
|
-
by delegating to the orchestrator, which will route tasks to the appropriate specialized agent.
|
|
@@ -15,24 +15,14 @@ constitution:
|
|
|
15
15
|
- Iterate fast — build the simplest thing that works, then improve
|
|
16
16
|
- Technical debt is acceptable — speed wins at the prototype stage
|
|
17
17
|
- Validate assumptions — build to learn, not to last
|
|
18
|
-
stack:
|
|
19
|
-
- Next.js 16+ (App Router)
|
|
20
|
-
- React 19
|
|
21
|
-
- TypeScript 5 (strict)
|
|
22
|
-
- Tailwind CSS 4
|
|
23
|
-
- Supabase (auth + database)
|
|
24
|
-
- Vercel (deployment)
|
|
25
18
|
conventions:
|
|
26
19
|
- Prefer inline styles and co-located logic over abstractions
|
|
27
20
|
- Use Supabase client directly — skip custom API layers when possible
|
|
28
21
|
- Minimal component hierarchy — flatten where you can
|
|
29
22
|
- Skip extensive error handling — focus on happy path first
|
|
30
23
|
- One file per feature when practical
|
|
31
|
-
|
|
24
|
+
context:
|
|
32
25
|
projectDescription: >
|
|
33
|
-
This is a lean MVP
|
|
26
|
+
This is a lean MVP. Speed and iteration are prioritized over architecture.
|
|
34
27
|
The orchestrator coordinates a small team of frontend, backend, and database agents to ship
|
|
35
28
|
features as fast as possible.
|
|
36
|
-
orchestratorRef: >
|
|
37
|
-
The orchestrator agent (.claude/agents/orchestrator.md) is the main entry point. Always start
|
|
38
|
-
by delegating to the orchestrator, which will route tasks to the appropriate specialized agent.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
name: SaaS
|
|
1
|
+
name: SaaS Full
|
|
2
2
|
description: Standard SaaS project preset with full agent team, Next.js conventions, and Tailwind patterns. Includes
|
|
3
3
|
orchestrator-driven multi-agent workflow.
|
|
4
4
|
agents:
|
|
@@ -20,31 +20,22 @@ skills:
|
|
|
20
20
|
- shadcn-ui
|
|
21
21
|
- api-design
|
|
22
22
|
- ui-ux-guidelines
|
|
23
|
+
- stripe-integration
|
|
24
|
+
- seo-optimization
|
|
23
25
|
constitution:
|
|
24
26
|
principles:
|
|
25
27
|
- Ship fast, iterate often — prefer working software over perfect plans
|
|
26
28
|
- User-first design — every feature must solve a real user problem
|
|
27
29
|
- Type safety everywhere — leverage TypeScript strict mode
|
|
28
30
|
- Convention over configuration — follow established patterns
|
|
29
|
-
stack:
|
|
30
|
-
- Next.js 16+ (App Router)
|
|
31
|
-
- React 19
|
|
32
|
-
- TypeScript 5 (strict)
|
|
33
|
-
- Tailwind CSS 4
|
|
34
|
-
- ShadCN UI
|
|
35
|
-
- Supabase (auth + database)
|
|
36
|
-
- Vercel (deployment)
|
|
37
31
|
conventions:
|
|
38
32
|
- Use Server Components by default, Client Components only when needed
|
|
39
33
|
- Server Actions for all mutations
|
|
40
34
|
- Zod validation at API boundaries
|
|
41
35
|
- Mobile-first responsive design
|
|
42
36
|
- Semantic HTML with ARIA attributes for accessibility
|
|
43
|
-
|
|
37
|
+
context:
|
|
44
38
|
projectDescription: >
|
|
45
|
-
This is a SaaS application
|
|
39
|
+
This is a SaaS application. It uses a multi-agent architecture where the orchestrator delegates
|
|
46
40
|
tasks to specialized agents (frontend, backend, database, etc.). Each agent follows the conventions defined in the
|
|
47
41
|
linked skills.
|
|
48
|
-
orchestratorRef: >
|
|
49
|
-
The orchestrator agent (.claude/agents/orchestrator.md) is the main entry point. Always start by delegating to the
|
|
50
|
-
orchestrator, which will route tasks to the appropriate specialized agent.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
name: SaaS Full
|
|
2
|
+
description: SaaS complet avec architecture en couches, auth RBAC, i18n, forms, testing, emails et Stripe. Batteries-included.
|
|
3
|
+
agents:
|
|
4
|
+
- orchestrator
|
|
5
|
+
- frontend
|
|
6
|
+
- backend
|
|
7
|
+
- database
|
|
8
|
+
- ux-ui
|
|
9
|
+
- tests
|
|
10
|
+
- review-qa
|
|
11
|
+
- security
|
|
12
|
+
- performance
|
|
13
|
+
- marketing
|
|
14
|
+
skills:
|
|
15
|
+
- nextjs-conventions
|
|
16
|
+
- tailwind-patterns
|
|
17
|
+
- shadcn-ui
|
|
18
|
+
- api-design
|
|
19
|
+
- supabase-patterns
|
|
20
|
+
- ui-ux-guidelines
|
|
21
|
+
- layered-architecture
|
|
22
|
+
- drizzle-patterns
|
|
23
|
+
- server-actions-patterns
|
|
24
|
+
- form-validation
|
|
25
|
+
- auth-rbac
|
|
26
|
+
- i18n-patterns
|
|
27
|
+
- testing-patterns
|
|
28
|
+
- resend-email
|
|
29
|
+
- react-query-patterns
|
|
30
|
+
- table-pagination
|
|
31
|
+
- env-validation
|
|
32
|
+
- better-auth-patterns
|
|
33
|
+
- stripe-integration
|
|
34
|
+
- hero-copywriting
|
|
35
|
+
- seo-optimization
|
|
36
|
+
constitution:
|
|
37
|
+
principles:
|
|
38
|
+
- Functional over OOP — pure functions, composition, immutability
|
|
39
|
+
- Strict layered architecture — Presentation → Facade → Service → DAL → Persistence
|
|
40
|
+
- Type safety everywhere — TypeScript strict, Zod validation at boundaries
|
|
41
|
+
- Ship fast, iterate often — working software over perfect plans
|
|
42
|
+
- Security by design — auth + RBAC + validation at every layer
|
|
43
|
+
conventions:
|
|
44
|
+
- RSC by default, "use client" only when strictly needed
|
|
45
|
+
- Server Actions for all mutations, DAL for all reads
|
|
46
|
+
- Zod dual validation (client + server)
|
|
47
|
+
- Facades between presentation and services
|
|
48
|
+
- CASL authorization at service layer
|
|
49
|
+
- next-intl for all user-facing strings
|
|
50
|
+
- Vitest with role-based test strategy (PUBLIC/USER/ADMIN)
|
|
51
|
+
- kebab-case for all new files
|
|
52
|
+
context:
|
|
53
|
+
projectDescription: >
|
|
54
|
+
This is a full-featured SaaS application. It follows a strict layered architecture
|
|
55
|
+
(Presentation → Facade → Service → DAL → Persistence) with multi-agent orchestration. Authentication is
|
|
56
|
+
handled by Better Auth with RBAC via CASL. The app supports i18n with next-intl, transactional emails via
|
|
57
|
+
Resend, payments via Stripe, and uses Drizzle ORM with PostgreSQL. Every feature is tested with Vitest
|
|
58
|
+
using a role-based strategy (PUBLIC/USER/ADMIN).
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: api-design
|
|
3
|
-
description: "REST API design
|
|
4
|
-
allowed-tools: "Read, Write, Edit, Glob, Grep"
|
|
3
|
+
description: "REST API design with validation, error handling, auth wrappers, and facades. Use when building API routes, server actions, implementing auth middleware, or designing backend services."
|
|
5
4
|
---
|
|
6
5
|
|
|
7
6
|
# API Design Principles
|
|
8
7
|
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **Validate all incoming data** with Zod schemas at the API boundary.
|
|
11
|
+
- **Never expose stack traces** or internal details in production error responses.
|
|
12
|
+
- **Always use facades** between presentation and services.
|
|
13
|
+
- **Auth wrappers on every protected endpoint** — `withAuth()`, `withAdmin()`.
|
|
14
|
+
- **Consistent response shapes** — `{ data }` for success, `{ error, code }` for errors.
|
|
15
|
+
- **Parameterized queries only** — never string concatenation for SQL.
|
|
16
|
+
|
|
9
17
|
## Route Structure
|
|
10
18
|
|
|
11
19
|
- Use resource-based URLs: `/api/users`, `/api/posts/:id/comments`.
|
|
@@ -82,6 +90,39 @@ allowed-tools: "Read, Write, Edit, Glob, Grep"
|
|
|
82
90
|
- Check permissions at the resource level: "Can this user access THIS specific post?"
|
|
83
91
|
- Return `401` for missing/invalid auth, `403` for insufficient permissions.
|
|
84
92
|
|
|
93
|
+
### Auth Wrappers
|
|
94
|
+
|
|
95
|
+
Use auth wrapper functions for consistent protection:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
// Require any authenticated user
|
|
99
|
+
export async function withAuth() {
|
|
100
|
+
const user = await getCurrentUser();
|
|
101
|
+
if (!user) throw new ApiError(401, "Unauthorized");
|
|
102
|
+
return user;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Require authenticated user with valid token
|
|
106
|
+
export async function withAuthToken(request: Request) {
|
|
107
|
+
const token = request.headers.get("authorization")?.replace("Bearer ", "");
|
|
108
|
+
if (!token) throw new ApiError(401, "Missing token");
|
|
109
|
+
return verifyToken(token);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Dynamic auth — optional session, different behavior for authed/unauthed
|
|
113
|
+
export async function withDynamicAuth() {
|
|
114
|
+
const user = await getCurrentUser();
|
|
115
|
+
return { user, isAuthenticated: !!user };
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Facades
|
|
120
|
+
|
|
121
|
+
- **Always use facades** between presentation and services.
|
|
122
|
+
- One facade per domain entity in `src/facades/`.
|
|
123
|
+
- Facades handle auth context extraction and coordinate service calls.
|
|
124
|
+
- Routes/pages call facades — never services directly.
|
|
125
|
+
|
|
85
126
|
## Rate Limiting
|
|
86
127
|
|
|
87
128
|
- Implement rate limiting on public endpoints and auth endpoints.
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: auth-rbac
|
|
3
|
+
description: "CASL-based authorization with role hierarchies, organization roles, and safe route protection. Use when implementing access control, role-based permissions, or protecting routes and API endpoints."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Auth & RBAC Patterns
|
|
7
|
+
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **Authorization in services, not routes** — use CASL in the service layer.
|
|
11
|
+
- **Route groups for access control** — `(public)`, `(auth)`, `(app)`, `admin`.
|
|
12
|
+
- **Middleware for redirects** — protect routes at the edge.
|
|
13
|
+
- **Auth wrappers for pages** — `withAuth()`, `withAdmin()` in server components.
|
|
14
|
+
- **Never trust client-side role checks** — always verify server-side.
|
|
15
|
+
- **Principle of least privilege** — grant minimum required permissions.
|
|
16
|
+
|
|
17
|
+
## Role System
|
|
18
|
+
|
|
19
|
+
### Application Roles
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// src/lib/roles.ts
|
|
23
|
+
export const APP_ROLES = ["USER", "ADMIN", "SUPER_ADMIN"] as const;
|
|
24
|
+
export type AppRole = (typeof APP_ROLES)[number];
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Organization Roles
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
export const ORG_ROLES = ["MEMBER", "MANAGER", "OWNER"] as const;
|
|
31
|
+
export type OrgRole = (typeof ORG_ROLES)[number];
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- Every user has an **app role** (global) and an **org role** (per organization).
|
|
35
|
+
- Role checks always consider both: app role for platform features, org role for org resources.
|
|
36
|
+
|
|
37
|
+
## CASL Authorization
|
|
38
|
+
|
|
39
|
+
### Ability Definition
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// src/lib/casl.ts
|
|
43
|
+
import { AbilityBuilder, createMongoAbility, type MongoAbility } from "@casl/ability";
|
|
44
|
+
|
|
45
|
+
type Actions = "create" | "read" | "update" | "delete" | "manage";
|
|
46
|
+
type Subjects = "User" | "Post" | "Organization" | "all";
|
|
47
|
+
export type AppAbility = MongoAbility<[Actions, Subjects]>;
|
|
48
|
+
|
|
49
|
+
export function defineAbilityFor(user: AuthUser): AppAbility {
|
|
50
|
+
const { can, cannot, build } = new AbilityBuilder<AppAbility>(createMongoAbility);
|
|
51
|
+
|
|
52
|
+
// Base permissions for all authenticated users
|
|
53
|
+
can("read", "Post", { published: true });
|
|
54
|
+
can("update", "User", { id: user.id }); // own profile
|
|
55
|
+
|
|
56
|
+
if (user.role === "ADMIN") {
|
|
57
|
+
can("manage", "User");
|
|
58
|
+
can("manage", "Post");
|
|
59
|
+
can("read", "Organization");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (user.role === "SUPER_ADMIN") {
|
|
63
|
+
can("manage", "all");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return build();
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Organization Ability
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
export function defineOrgAbilityFor(user: AuthUser, orgRole: OrgRole): AppAbility {
|
|
74
|
+
const { can, build } = new AbilityBuilder<AppAbility>(createMongoAbility);
|
|
75
|
+
|
|
76
|
+
can("read", "Organization");
|
|
77
|
+
|
|
78
|
+
if (orgRole === "MANAGER" || orgRole === "OWNER") {
|
|
79
|
+
can("update", "Organization");
|
|
80
|
+
can("manage", "User"); // manage org members
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (orgRole === "OWNER") {
|
|
84
|
+
can("delete", "Organization");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return build();
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Service Layer Authorization
|
|
92
|
+
|
|
93
|
+
Check permissions in the **service layer**, never in presentation or DAL:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// src/services/post.service.ts
|
|
97
|
+
import { ForbiddenError } from "@casl/ability";
|
|
98
|
+
import { defineAbilityFor } from "@/lib/casl";
|
|
99
|
+
|
|
100
|
+
export async function deletePost(user: AuthUser, postId: string) {
|
|
101
|
+
const ability = defineAbilityFor(user);
|
|
102
|
+
const post = await findPostById(postId);
|
|
103
|
+
|
|
104
|
+
ForbiddenError.from(ability).throwUnlessCan("delete", {
|
|
105
|
+
...post,
|
|
106
|
+
__typename: "Post",
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return removePost(postId);
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Safe Route Protection
|
|
114
|
+
|
|
115
|
+
### Middleware-Level
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
// middleware.ts
|
|
119
|
+
const publicRoutes = ["/", "/login", "/register", "/api/webhook"];
|
|
120
|
+
const adminRoutes = ["/admin"];
|
|
121
|
+
|
|
122
|
+
export async function middleware(request: NextRequest) {
|
|
123
|
+
const session = await getSession();
|
|
124
|
+
|
|
125
|
+
if (!session && !publicRoutes.some(r => request.nextUrl.pathname.startsWith(r))) {
|
|
126
|
+
return NextResponse.redirect(new URL("/login", request.url));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (adminRoutes.some(r => request.nextUrl.pathname.startsWith(r))) {
|
|
130
|
+
if (session?.user.role !== "ADMIN" && session?.user.role !== "SUPER_ADMIN") {
|
|
131
|
+
return NextResponse.redirect(new URL("/", request.url));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Route Group Structure
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
src/app/
|
|
141
|
+
(public)/ # No auth required — landing, login, register
|
|
142
|
+
login/
|
|
143
|
+
register/
|
|
144
|
+
(auth)/ # Auth required, any role — onboarding
|
|
145
|
+
onboarding/
|
|
146
|
+
(app)/ # Auth required, active user — main app
|
|
147
|
+
dashboard/
|
|
148
|
+
settings/
|
|
149
|
+
admin/ # Admin only — user management, app settings
|
|
150
|
+
users/
|
|
151
|
+
settings/
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Auth Wrappers
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
// src/lib/auth-wrappers.ts
|
|
158
|
+
import { getCurrentUser } from "@/lib/auth";
|
|
159
|
+
import { redirect } from "next/navigation";
|
|
160
|
+
|
|
161
|
+
export async function withAuth() {
|
|
162
|
+
const user = await getCurrentUser();
|
|
163
|
+
if (!user) redirect("/login");
|
|
164
|
+
return user;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function withAdmin() {
|
|
168
|
+
const user = await withAuth();
|
|
169
|
+
if (user.role !== "ADMIN" && user.role !== "SUPER_ADMIN") redirect("/");
|
|
170
|
+
return user;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function withOrgRole(orgId: string, requiredRole: OrgRole) {
|
|
174
|
+
const user = await withAuth();
|
|
175
|
+
const membership = await getOrgMembership(user.id, orgId);
|
|
176
|
+
if (!membership || !hasOrgRole(membership.role, requiredRole)) redirect("/");
|
|
177
|
+
return { user, membership };
|
|
178
|
+
}
|
|
179
|
+
```
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: better-auth-patterns
|
|
3
|
+
description: "Better Auth setup with session management, social login, organization plugin, and middleware. Use when implementing authentication, adding social login providers, or managing user sessions with Better Auth."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Better Auth Patterns
|
|
7
|
+
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **Server-side session check** — use `getCurrentUser()` in RSC and Server Actions.
|
|
11
|
+
- **Client-side `useSession()`** — only in Client Components for UI state.
|
|
12
|
+
- **Middleware for redirects** — protect routes at the edge, not in pages.
|
|
13
|
+
- **Never expose auth secrets** — keep `BETTER_AUTH_SECRET` server-only.
|
|
14
|
+
- **Use plugins** — `organization()`, `admin()` for built-in features.
|
|
15
|
+
- **Social login always available** — Google + GitHub as defaults.
|
|
16
|
+
|
|
17
|
+
## Server Setup
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// src/lib/auth.ts
|
|
21
|
+
import { betterAuth } from "better-auth";
|
|
22
|
+
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
23
|
+
import { organization, admin } from "better-auth/plugins";
|
|
24
|
+
import { db } from "@/lib/db";
|
|
25
|
+
|
|
26
|
+
export const auth = betterAuth({
|
|
27
|
+
database: drizzleAdapter(db, { provider: "pg" }),
|
|
28
|
+
emailAndPassword: { enabled: true },
|
|
29
|
+
socialProviders: {
|
|
30
|
+
google: {
|
|
31
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
32
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
33
|
+
},
|
|
34
|
+
github: {
|
|
35
|
+
clientId: process.env.GITHUB_CLIENT_ID!,
|
|
36
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
plugins: [
|
|
40
|
+
organization(),
|
|
41
|
+
admin(),
|
|
42
|
+
],
|
|
43
|
+
session: {
|
|
44
|
+
expiresIn: 60 * 60 * 24 * 7, // 7 days
|
|
45
|
+
updateAge: 60 * 60 * 24, // refresh daily
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Client Setup
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// src/lib/auth-client.ts
|
|
54
|
+
import { createAuthClient } from "better-auth/react";
|
|
55
|
+
import { organizationClient, adminClient } from "better-auth/client/plugins";
|
|
56
|
+
|
|
57
|
+
export const authClient = createAuthClient({
|
|
58
|
+
plugins: [organizationClient(), adminClient()],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export const { useSession, signIn, signUp, signOut } = authClient;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## API Route Handler
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// src/app/api/auth/[...all]/route.ts
|
|
68
|
+
import { auth } from "@/lib/auth";
|
|
69
|
+
import { toNextJsHandler } from "better-auth/next-js";
|
|
70
|
+
|
|
71
|
+
export const { GET, POST } = toNextJsHandler(auth);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Session Management
|
|
75
|
+
|
|
76
|
+
### Server-Side Session
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// src/lib/auth-server.ts
|
|
80
|
+
import { auth } from "@/lib/auth";
|
|
81
|
+
import { headers } from "next/headers";
|
|
82
|
+
|
|
83
|
+
export async function getCurrentUser() {
|
|
84
|
+
const session = await auth.api.getSession({
|
|
85
|
+
headers: await headers(),
|
|
86
|
+
});
|
|
87
|
+
return session?.user ?? null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function requireAuth() {
|
|
91
|
+
const user = await getCurrentUser();
|
|
92
|
+
if (!user) redirect("/login");
|
|
93
|
+
return user;
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Client-Side Session
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
"use client";
|
|
101
|
+
import { useSession } from "@/lib/auth-client";
|
|
102
|
+
|
|
103
|
+
export function UserMenu() {
|
|
104
|
+
const { data: session, isPending } = useSession();
|
|
105
|
+
|
|
106
|
+
if (isPending) return <Skeleton />;
|
|
107
|
+
if (!session) return <LoginButton />;
|
|
108
|
+
|
|
109
|
+
return <span>{session.user.name}</span>;
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Authentication Flows
|
|
114
|
+
|
|
115
|
+
### Sign Up
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
"use client";
|
|
119
|
+
import { authClient } from "@/lib/auth-client";
|
|
120
|
+
|
|
121
|
+
async function handleSignUp(data: SignUpInput) {
|
|
122
|
+
const { error } = await authClient.signUp.email({
|
|
123
|
+
email: data.email,
|
|
124
|
+
password: data.password,
|
|
125
|
+
name: data.name,
|
|
126
|
+
});
|
|
127
|
+
if (error) toast.error(error.message);
|
|
128
|
+
else router.push("/dashboard");
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Social Login
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
async function handleGoogleLogin() {
|
|
136
|
+
await authClient.signIn.social({ provider: "google" });
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Sign Out
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
async function handleSignOut() {
|
|
144
|
+
await authClient.signOut();
|
|
145
|
+
router.push("/");
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Middleware
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
// middleware.ts
|
|
153
|
+
import { betterFetch } from "@better-fetch/fetch";
|
|
154
|
+
import { NextResponse } from "next/server";
|
|
155
|
+
import type { NextRequest } from "next/server";
|
|
156
|
+
import type { Session } from "better-auth/types";
|
|
157
|
+
|
|
158
|
+
const publicRoutes = ["/", "/login", "/register", "/api/auth"];
|
|
159
|
+
|
|
160
|
+
export async function middleware(request: NextRequest) {
|
|
161
|
+
const { pathname } = request.nextUrl;
|
|
162
|
+
|
|
163
|
+
// Skip public routes
|
|
164
|
+
if (publicRoutes.some((r) => pathname.startsWith(r))) {
|
|
165
|
+
return NextResponse.next();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Check session
|
|
169
|
+
const { data: session } = await betterFetch<Session>(
|
|
170
|
+
"/api/auth/get-session",
|
|
171
|
+
{
|
|
172
|
+
baseURL: request.nextUrl.origin,
|
|
173
|
+
headers: { cookie: request.headers.get("cookie") ?? "" },
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
if (!session) {
|
|
178
|
+
return NextResponse.redirect(new URL("/login", request.url));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Admin route protection
|
|
182
|
+
if (pathname.startsWith("/admin") && session.user.role !== "admin") {
|
|
183
|
+
return NextResponse.redirect(new URL("/", request.url));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return NextResponse.next();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const config = {
|
|
190
|
+
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
|
|
191
|
+
};
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Organization Plugin
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
// Create organization
|
|
198
|
+
const { data: org } = await authClient.organization.create({
|
|
199
|
+
name: "Acme Inc",
|
|
200
|
+
slug: "acme",
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Invite member
|
|
204
|
+
await authClient.organization.inviteMember({
|
|
205
|
+
email: "user@example.com",
|
|
206
|
+
role: "member",
|
|
207
|
+
organizationId: org.id,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Switch active organization
|
|
211
|
+
await authClient.organization.setActive({ organizationId: org.id });
|
|
212
|
+
```
|