@folpe/loom 0.2.0 → 0.3.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 +12 -0
- package/data/agents/database/AGENT.md +3 -0
- package/data/agents/frontend/AGENT.md +10 -0
- package/data/agents/marketing/AGENT.md +3 -0
- package/data/agents/orchestrator/AGENT.md +1 -19
- package/data/agents/security/AGENT.md +3 -0
- package/data/agents/tests/AGENT.md +2 -0
- package/data/agents/ux-ui/AGENT.md +5 -0
- package/data/presets/api-backend.yaml +0 -3
- package/data/presets/chrome-extension.yaml +0 -3
- package/data/presets/cli-tool.yaml +0 -3
- package/data/presets/e-commerce.yaml +0 -3
- package/data/presets/expo-mobile.yaml +0 -3
- package/data/presets/fullstack-auth.yaml +0 -3
- package/data/presets/landing-page.yaml +0 -3
- package/data/presets/mvp-lean.yaml +0 -3
- package/data/presets/saas-default.yaml +3 -4
- package/data/presets/saas-full.yaml +71 -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 +254 -100
- package/package.json +2 -1
|
@@ -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
|
+
```
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: chrome-extension-patterns
|
|
3
|
-
description: "Chrome extension development patterns for Manifest V3, content scripts, popup UI,
|
|
4
|
-
allowed-tools: "Read, Write, Edit, Glob, Grep"
|
|
3
|
+
description: "Chrome extension development patterns for Manifest V3, content scripts, popup UI, service workers, messaging, and storage. Use when building browser extensions, creating content scripts, setting up background service workers, or implementing extension popup interfaces."
|
|
5
4
|
---
|
|
6
5
|
|
|
7
6
|
# Chrome Extension Patterns (Manifest V3)
|
|
8
7
|
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **Always use Manifest V3** — Manifest V2 is deprecated and no longer accepted.
|
|
11
|
+
- **Minimal permissions** — request only what the extension needs; use `activeTab` over `<all_urls>`.
|
|
12
|
+
- **Never use `setInterval` in service workers** — use `chrome.alarms` for periodic tasks.
|
|
13
|
+
- **Never use `innerHTML` with user input** — use `textContent` to prevent XSS.
|
|
14
|
+
- **Never include API keys in content scripts** — they are visible to the host page.
|
|
15
|
+
- **Never use `localStorage` in content scripts** — it belongs to the host page; use `chrome.storage`.
|
|
16
|
+
|
|
9
17
|
## Manifest
|
|
10
18
|
|
|
11
|
-
- Always use **Manifest V3** — Manifest V2 is deprecated.
|
|
12
19
|
- Define minimal permissions — request only what the extension needs:
|
|
13
20
|
```json
|
|
14
21
|
{
|
|
@@ -65,8 +72,8 @@ allowed-tools: "Read, Write, Edit, Glob, Grep"
|
|
|
65
72
|
|
|
66
73
|
## Messaging
|
|
67
74
|
|
|
68
|
-
- Use `chrome.runtime.sendMessage` for popup/options
|
|
69
|
-
- Use `chrome.tabs.sendMessage` for background
|
|
75
|
+
- Use `chrome.runtime.sendMessage` for popup/options <-> background communication.
|
|
76
|
+
- Use `chrome.tabs.sendMessage` for background -> content script communication.
|
|
70
77
|
- Define a message type system for type-safe messaging:
|
|
71
78
|
```ts
|
|
72
79
|
type Message =
|
|
@@ -94,5 +101,5 @@ allowed-tools: "Read, Write, Edit, Glob, Grep"
|
|
|
94
101
|
|
|
95
102
|
- Use `chrome://extensions` with Developer Mode for loading unpacked extensions.
|
|
96
103
|
- Enable "Errors" view for debugging service worker issues.
|
|
97
|
-
- Use Chrome DevTools to inspect popup (right-click
|
|
104
|
+
- Use Chrome DevTools to inspect popup (right-click -> Inspect) and background (service worker link).
|
|
98
105
|
- Hot-reload: rebuild with tsup watch, then click "Update" in `chrome://extensions`.
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cli-development
|
|
3
|
-
description: "CLI tool development patterns for Node.js with Commander.js, terminal UX, and npm distribution. Use when building command-line tools."
|
|
4
|
-
allowed-tools: "Read, Write, Edit, Glob, Grep"
|
|
3
|
+
description: "CLI tool development patterns for Node.js with Commander.js, terminal UX, error handling, and npm distribution. Use when building command-line tools, adding CLI commands, implementing terminal prompts, or bundling CLI binaries for distribution."
|
|
5
4
|
---
|
|
6
5
|
|
|
7
6
|
# CLI Development Patterns
|
|
8
7
|
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **Use `stdout` for data, `stderr` for logs** — never mix output channels.
|
|
11
|
+
- **Exit with proper codes** — `0` success, `1` runtime error, `2` usage error.
|
|
12
|
+
- **Never show raw stack traces** — log them with `--verbose` flag only.
|
|
13
|
+
- **Respect `NO_COLOR`** — check `process.env.NO_COLOR` before using colors.
|
|
14
|
+
- **Validate config with Zod** — fail fast with clear error on invalid config.
|
|
15
|
+
- **Confirm destructive actions** — always prompt before irreversible operations.
|
|
16
|
+
|
|
9
17
|
## Project Structure
|
|
10
18
|
|
|
11
19
|
```
|
|
@@ -71,7 +79,7 @@ src/
|
|
|
71
79
|
|
|
72
80
|
- Show a spinner for long operations (use `ora` or `nanospinner`).
|
|
73
81
|
- Use progress bars for multi-step or percentage-based operations.
|
|
74
|
-
- Confirm destructive actions with a prompt (use `@inquirer/prompts`):
|
|
82
|
+
- Confirm destructive actions with a prompt (use `@inquirer/prompts` or `@clack/prompts`):
|
|
75
83
|
```ts
|
|
76
84
|
const confirmed = await confirm({ message: 'Delete all files?' })
|
|
77
85
|
if (!confirmed) process.exit(0)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: drizzle-patterns
|
|
3
|
+
description: "Drizzle ORM patterns for schema design, migrations, transactions, and DAO functions. Use when creating database schemas, writing queries, implementing transactions, or working with PostgreSQL via Drizzle."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Drizzle ORM Patterns
|
|
7
|
+
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **Infer types from schema** — never duplicate type definitions manually.
|
|
11
|
+
- **Use `limit()` on all queries** — never fetch unbounded result sets.
|
|
12
|
+
- **Select only needed columns** — avoid `select()` with no arguments on large tables.
|
|
13
|
+
- **Never nest transactions** — keep transactions short with no external API calls inside.
|
|
14
|
+
- **Never edit generated migration files** — review SQL before applying in production.
|
|
15
|
+
- **Add indexes** on columns used in `WHERE`, `JOIN`, and `ORDER BY`.
|
|
16
|
+
|
|
17
|
+
## Schema Design
|
|
18
|
+
|
|
19
|
+
- Define schemas in `src/schema/` with one file per entity.
|
|
20
|
+
- Export all tables from `src/schema/index.ts` barrel file.
|
|
21
|
+
- Use `pgTable` for table definitions with typed columns.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// src/schema/user.ts
|
|
25
|
+
import { pgTable, uuid, text, timestamp, boolean } from "drizzle-orm/pg-core";
|
|
26
|
+
|
|
27
|
+
export const users = pgTable("users", {
|
|
28
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
29
|
+
email: text("email").notNull().unique(),
|
|
30
|
+
name: text("name").notNull(),
|
|
31
|
+
role: text("role", { enum: ["USER", "ADMIN"] }).notNull().default("USER"),
|
|
32
|
+
emailVerified: boolean("email_verified").notNull().default(false),
|
|
33
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
34
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Relations
|
|
39
|
+
|
|
40
|
+
- Define relations separately using `relations()`:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { relations } from "drizzle-orm";
|
|
44
|
+
|
|
45
|
+
export const usersRelations = relations(users, ({ many }) => ({
|
|
46
|
+
posts: many(posts),
|
|
47
|
+
organizations: many(organizationMembers),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
export const postsRelations = relations(posts, ({ one }) => ({
|
|
51
|
+
author: one(users, { fields: [posts.authorId], references: [users.id] }),
|
|
52
|
+
}));
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Database Client
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
// src/lib/db.ts
|
|
59
|
+
import { drizzle } from "drizzle-orm/node-postgres";
|
|
60
|
+
import * as schema from "@/schema";
|
|
61
|
+
|
|
62
|
+
export const db = drizzle(process.env.DATABASE_URL!, { schema });
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Query Patterns
|
|
66
|
+
|
|
67
|
+
- Use the query builder for complex reads:
|
|
68
|
+
```ts
|
|
69
|
+
const result = await db.query.users.findMany({
|
|
70
|
+
where: eq(users.role, "ADMIN"),
|
|
71
|
+
with: { posts: true },
|
|
72
|
+
orderBy: [desc(users.createdAt)],
|
|
73
|
+
limit: 20,
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- Use `select()` for simple reads with specific columns:
|
|
78
|
+
```ts
|
|
79
|
+
const result = await db.select({
|
|
80
|
+
id: users.id,
|
|
81
|
+
name: users.name,
|
|
82
|
+
}).from(users).where(eq(users.role, "ADMIN"));
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Transactions
|
|
86
|
+
|
|
87
|
+
- Wrap multi-table writes in transactions:
|
|
88
|
+
```ts
|
|
89
|
+
await db.transaction(async (tx) => {
|
|
90
|
+
const [org] = await tx.insert(organizations).values({ name }).returning();
|
|
91
|
+
await tx.insert(organizationMembers).values({
|
|
92
|
+
organizationId: org.id,
|
|
93
|
+
userId: currentUser.id,
|
|
94
|
+
role: "OWNER",
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## DAO Pattern
|
|
100
|
+
|
|
101
|
+
- Create DAO functions in `src/dal/` for reusable queries:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
// src/dal/user.dal.ts
|
|
105
|
+
export async function findUserById(id: string) {
|
|
106
|
+
return db.query.users.findFirst({
|
|
107
|
+
where: eq(users.id, id),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function findUsersByOrg(orgId: string) {
|
|
112
|
+
return db.select()
|
|
113
|
+
.from(users)
|
|
114
|
+
.innerJoin(orgMembers, eq(orgMembers.userId, users.id))
|
|
115
|
+
.where(eq(orgMembers.orgId, orgId));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function createUser(data: NewUser) {
|
|
119
|
+
const [user] = await db.insert(users).values(data).returning();
|
|
120
|
+
return user;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function updateUser(id: string, data: Partial<NewUser>) {
|
|
124
|
+
const [user] = await db.update(users)
|
|
125
|
+
.set({ ...data, updatedAt: new Date() })
|
|
126
|
+
.where(eq(users.id, id))
|
|
127
|
+
.returning();
|
|
128
|
+
return user;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function deleteUser(id: string) {
|
|
132
|
+
await db.delete(users).where(eq(users.id, id));
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Migrations
|
|
137
|
+
|
|
138
|
+
- Generate migrations with `npx drizzle-kit generate`.
|
|
139
|
+
- Apply with `npx drizzle-kit migrate`.
|
|
140
|
+
- Use `drizzle-kit push` only in development for quick iteration.
|
|
141
|
+
- Review generated SQL before applying in production.
|
|
142
|
+
|
|
143
|
+
## Type Inference
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import type { InferSelectModel, InferInsertModel } from "drizzle-orm";
|
|
147
|
+
|
|
148
|
+
export type User = InferSelectModel<typeof users>;
|
|
149
|
+
export type NewUser = InferInsertModel<typeof users>;
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Performance
|
|
153
|
+
|
|
154
|
+
- Add indexes on columns used in `WHERE`, `JOIN`, and `ORDER BY`:
|
|
155
|
+
```ts
|
|
156
|
+
import { index } from "drizzle-orm/pg-core";
|
|
157
|
+
|
|
158
|
+
export const posts = pgTable("posts", {
|
|
159
|
+
// columns...
|
|
160
|
+
}, (table) => [
|
|
161
|
+
index("posts_author_idx").on(table.authorId),
|
|
162
|
+
index("posts_created_at_idx").on(table.createdAt),
|
|
163
|
+
]);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
- Use `prepare()` for frequently executed queries.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: env-validation
|
|
3
|
+
description: "Environment variable validation with Zod schemas and type-safe access. Use when configuring environment variables, adding new secrets, or setting up project configuration."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Environment Variable Validation
|
|
7
|
+
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **Validate at startup** — crash early if env vars are missing/invalid.
|
|
11
|
+
- **Single source of truth** — `src/env.ts` is the only place to access env vars.
|
|
12
|
+
- **Never use `process.env` directly** — always import from `@/env`.
|
|
13
|
+
- **Type-safe prefixes** — validate format (e.g., `startsWith("sk_")` for Stripe keys).
|
|
14
|
+
- **Maintain `.env.example`** — document all required variables without values.
|
|
15
|
+
- **Never commit secrets** — `.env.local` and `.env.production` are gitignored.
|
|
16
|
+
|
|
17
|
+
## Schema Definition
|
|
18
|
+
|
|
19
|
+
Validate all environment variables at startup with a Zod schema:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// src/env.ts
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
|
|
25
|
+
const envSchema = z.object({
|
|
26
|
+
// Database
|
|
27
|
+
DATABASE_URL: z.string().url(),
|
|
28
|
+
|
|
29
|
+
// Auth
|
|
30
|
+
BETTER_AUTH_SECRET: z.string().min(32),
|
|
31
|
+
BETTER_AUTH_URL: z.string().url(),
|
|
32
|
+
|
|
33
|
+
// Email
|
|
34
|
+
RESEND_API_KEY: z.string().startsWith("re_"),
|
|
35
|
+
|
|
36
|
+
// Stripe
|
|
37
|
+
STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
|
|
38
|
+
STRIPE_WEBHOOK_SECRET: z.string().startsWith("whsec_"),
|
|
39
|
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().startsWith("pk_"),
|
|
40
|
+
|
|
41
|
+
// App
|
|
42
|
+
NEXT_PUBLIC_APP_URL: z.string().url(),
|
|
43
|
+
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const env = envSchema.parse(process.env);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
Always import from `@/env` — never access `process.env` directly:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
// Good
|
|
55
|
+
import { env } from "@/env";
|
|
56
|
+
const db = drizzle(env.DATABASE_URL);
|
|
57
|
+
|
|
58
|
+
// Bad — unvalidated, untyped
|
|
59
|
+
const db = drizzle(process.env.DATABASE_URL!);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Client-Side Variables
|
|
63
|
+
|
|
64
|
+
Only `NEXT_PUBLIC_` prefixed variables are available in the browser:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// src/env.ts
|
|
68
|
+
const clientSchema = z.object({
|
|
69
|
+
NEXT_PUBLIC_APP_URL: z.string().url(),
|
|
70
|
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().startsWith("pk_"),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const serverSchema = z.object({
|
|
74
|
+
DATABASE_URL: z.string().url(),
|
|
75
|
+
BETTER_AUTH_SECRET: z.string().min(32),
|
|
76
|
+
RESEND_API_KEY: z.string().startsWith("re_"),
|
|
77
|
+
// ...server-only vars
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Merge for full validation
|
|
81
|
+
const envSchema = serverSchema.merge(clientSchema);
|
|
82
|
+
|
|
83
|
+
export const env = envSchema.parse(process.env);
|
|
84
|
+
|
|
85
|
+
// Client-only export for safe use in client components
|
|
86
|
+
export const clientEnv = clientSchema.parse({
|
|
87
|
+
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
|
|
88
|
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## .env Files
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
.env # Shared defaults (committed)
|
|
96
|
+
.env.local # Local overrides (gitignored)
|
|
97
|
+
.env.development # Dev-specific
|
|
98
|
+
.env.production # Prod-specific (gitignored)
|
|
99
|
+
.env.test # Test-specific
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### .env.example
|
|
103
|
+
|
|
104
|
+
Maintain a `.env.example` with all required variables (no values):
|
|
105
|
+
|
|
106
|
+
```env
|
|
107
|
+
# Database
|
|
108
|
+
DATABASE_URL=
|
|
109
|
+
|
|
110
|
+
# Auth
|
|
111
|
+
BETTER_AUTH_SECRET=
|
|
112
|
+
BETTER_AUTH_URL=
|
|
113
|
+
|
|
114
|
+
# Email
|
|
115
|
+
RESEND_API_KEY=
|
|
116
|
+
|
|
117
|
+
# Stripe
|
|
118
|
+
STRIPE_SECRET_KEY=
|
|
119
|
+
STRIPE_WEBHOOK_SECRET=
|
|
120
|
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
|
|
121
|
+
|
|
122
|
+
# App
|
|
123
|
+
NEXT_PUBLIC_APP_URL=
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Startup Validation
|
|
127
|
+
|
|
128
|
+
The `env.ts` import triggers validation. Import it early in your app:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
// src/app/layout.tsx
|
|
132
|
+
import "@/env"; // Validates env vars at startup
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
If validation fails, the app crashes immediately with a clear error:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
ZodError: [
|
|
139
|
+
{ path: ["DATABASE_URL"], message: "Required" },
|
|
140
|
+
{ path: ["RESEND_API_KEY"], message: "Invalid" }
|
|
141
|
+
]
|
|
142
|
+
```
|