@gallopsystems/agent-skills 1.0.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.
Files changed (52) hide show
  1. package/README.md +137 -0
  2. package/package.json +26 -0
  3. package/plugins/doctl/.claude-plugin/plugin.json +8 -0
  4. package/plugins/doctl/skills/doctl/SKILL.md +93 -0
  5. package/plugins/kysely-postgres/.claude-plugin/plugin.json +8 -0
  6. package/plugins/kysely-postgres/skills/kysely-postgres/SKILL.md +1101 -0
  7. package/plugins/kysely-postgres/skills/kysely-postgres/references/aggregations.ts +167 -0
  8. package/plugins/kysely-postgres/skills/kysely-postgres/references/ctes.ts +165 -0
  9. package/plugins/kysely-postgres/skills/kysely-postgres/references/expressions.ts +272 -0
  10. package/plugins/kysely-postgres/skills/kysely-postgres/references/joins.ts +206 -0
  11. package/plugins/kysely-postgres/skills/kysely-postgres/references/json-arrays.ts +398 -0
  12. package/plugins/kysely-postgres/skills/kysely-postgres/references/mutations.ts +199 -0
  13. package/plugins/kysely-postgres/skills/kysely-postgres/references/orderby-pagination.ts +117 -0
  14. package/plugins/kysely-postgres/skills/kysely-postgres/references/relations.ts +176 -0
  15. package/plugins/kysely-postgres/skills/kysely-postgres/references/select-where.ts +146 -0
  16. package/plugins/linear/.claude-plugin/plugin.json +8 -0
  17. package/plugins/linear/skills/linear/SKILL.md +1040 -0
  18. package/plugins/linear/skills/linear/bin/linear.mjs +1228 -0
  19. package/plugins/linear/skills/linear/tech-stack.md +273 -0
  20. package/plugins/nitro-testing/.claude-plugin/plugin.json +8 -0
  21. package/plugins/nitro-testing/skills/nitro-testing/SKILL.md +497 -0
  22. package/plugins/nitro-testing/skills/nitro-testing/async-testing.md +270 -0
  23. package/plugins/nitro-testing/skills/nitro-testing/ci-setup.md +226 -0
  24. package/plugins/nitro-testing/skills/nitro-testing/examples/global-setup.ts +90 -0
  25. package/plugins/nitro-testing/skills/nitro-testing/examples/handler.test.ts +167 -0
  26. package/plugins/nitro-testing/skills/nitro-testing/examples/setup.ts +29 -0
  27. package/plugins/nitro-testing/skills/nitro-testing/examples/test-utils-index.ts +297 -0
  28. package/plugins/nitro-testing/skills/nitro-testing/examples/vitest.config.ts +42 -0
  29. package/plugins/nitro-testing/skills/nitro-testing/factories.md +278 -0
  30. package/plugins/nitro-testing/skills/nitro-testing/frontend-testing.md +512 -0
  31. package/plugins/nitro-testing/skills/nitro-testing/test-utils.md +262 -0
  32. package/plugins/nitro-testing/skills/nitro-testing/transaction-rollback.md +183 -0
  33. package/plugins/nitro-testing/skills/nitro-testing/vitest-config.md +236 -0
  34. package/plugins/nuxt-nitro-api/.claude-plugin/plugin.json +8 -0
  35. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/SKILL.md +260 -0
  36. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/auth-patterns.md +228 -0
  37. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/composables-utils.md +174 -0
  38. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/deep-linking.md +190 -0
  39. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/auth-middleware.ts +32 -0
  40. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/auth-utils.ts +51 -0
  41. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/deep-link-page.vue +61 -0
  42. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/service-util.ts +63 -0
  43. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/sse-endpoint.ts +59 -0
  44. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/validation-endpoint.ts +38 -0
  45. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/fetch-patterns.md +178 -0
  46. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/nitro-tasks.md +243 -0
  47. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/page-structure.md +162 -0
  48. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/server-services.md +238 -0
  49. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/sse.md +221 -0
  50. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/ssr-client.md +166 -0
  51. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/validation.md +131 -0
  52. package/scripts/link-skills.mjs +252 -0
@@ -0,0 +1,260 @@
1
+ ---
2
+ name: nuxt-nitro-api
3
+ description: Build type-safe Nuxt 3 applications with Nitro API patterns. Covers validation, fetch patterns, auth, SSR, composables, background tasks, and real-time features.
4
+ ---
5
+
6
+ # Nuxt 3 / Nitro API Patterns
7
+
8
+ This skill provides patterns for building type-safe Nuxt 3 applications with Nitro backends.
9
+
10
+ ## When to Use This Skill
11
+
12
+ Use this skill when:
13
+ - Working in a Nuxt 3 project with TypeScript
14
+ - Building API endpoints with Nitro
15
+ - Implementing authentication with nuxt-auth-utils
16
+ - Handling SSR + client-side state
17
+ - Creating background tasks or real-time features
18
+
19
+ ## Reference Files
20
+
21
+ For detailed patterns, see these topic-focused reference files:
22
+
23
+ - [validation.md](./validation.md) - Zod validation with h3, Standard Schema, error handling
24
+ - [fetch-patterns.md](./fetch-patterns.md) - useFetch vs $fetch vs useAsyncData
25
+ - [auth-patterns.md](./auth-patterns.md) - nuxt-auth-utils, OAuth, WebAuthn, middleware
26
+ - [page-structure.md](./page-structure.md) - Keep pages thin, components do the work
27
+ - [composables-utils.md](./composables-utils.md) - When to use composables vs utils
28
+ - [ssr-client.md](./ssr-client.md) - SSR + localStorage, hydration, VueUse
29
+ - [deep-linking.md](./deep-linking.md) - URL params sync with filters and useFetch
30
+ - [nitro-tasks.md](./nitro-tasks.md) - Background jobs, scheduled tasks, job queues
31
+ - [sse.md](./sse.md) - Server-Sent Events for real-time streaming
32
+ - [server-services.md](./server-services.md) - Third-party service integration patterns
33
+
34
+ ## Example Files
35
+
36
+ Working examples from a Nuxt project:
37
+
38
+ - [validation-endpoint.ts](./examples/validation-endpoint.ts) - API endpoint with Zod validation
39
+ - [auth-middleware.ts](./examples/auth-middleware.ts) - Server auth middleware
40
+ - [auth-utils.ts](./examples/auth-utils.ts) - Reusable auth helpers
41
+ - [deep-link-page.vue](./examples/deep-link-page.vue) - URL params sync with filters
42
+ - [sse-endpoint.ts](./examples/sse-endpoint.ts) - SSE streaming endpoint
43
+ - [service-util.ts](./examples/service-util.ts) - Server-side service pattern
44
+
45
+ ## Core Principles
46
+
47
+ 1. **Let Nitro infer types** - Never add manual type params to `$fetch<Type>()` or `useFetch<Type>()`
48
+ 2. **Use h3 validation** - `getValidatedQuery()`, `readValidatedBody()` with Zod schemas
49
+ 3. **Composables for context, utils for pure functions** - Composables access Nuxt context, utils are pure
50
+ 4. **SSR-safe code** - Guard browser APIs with `import.meta.client` or `onMounted`
51
+ 5. **Keep pages thin** - Pages = layout + route params + components. Components own data fetching and logic.
52
+
53
+ ## Auto-Imports Quick Reference
54
+
55
+ ### Server-side (`/server` directory)
56
+
57
+ All h3 utilities auto-imported:
58
+ - `defineEventHandler`, `createError`, `getQuery`, `getValidatedQuery`
59
+ - `readBody`, `readValidatedBody`, `getRouterParams`, `getValidatedRouterParams`
60
+ - `getCookie`, `setCookie`, `deleteCookie`, `getHeader`, `setHeader`
61
+
62
+ From nuxt-auth-utils:
63
+ - `getUserSession`, `setUserSession`, `clearUserSession`, `requireUserSession`
64
+ - `hashPassword`, `verifyPassword`
65
+ - `defineOAuth*EventHandler` (Google, GitHub, etc.)
66
+
67
+ **Need to import:** `z` from "zod", `fromZodError` from "zod-validation-error"
68
+
69
+ ### Client-side
70
+
71
+ All auto-imported:
72
+ - Vue: `ref`, `computed`, `watch`, `onMounted`, etc.
73
+ - VueUse: `refDebounced`, `useLocalStorage`, `useUrlSearchParams`, etc.
74
+ - Nuxt: `useFetch`, `useAsyncData`, `useRoute`, `useRouter`, `useState`, `navigateTo`
75
+
76
+ ### Shared (`/shared` directory - Nuxt 3.14+)
77
+
78
+ Code auto-imported on both client AND server. Use for:
79
+ - Types and interfaces
80
+ - Pure utility functions
81
+ - Constants
82
+
83
+ ## Quick Patterns
84
+
85
+ ### Validation (h3 v2+ with Standard Schema)
86
+
87
+ ```typescript
88
+ // Pass Zod schema directly (h3 v2+)
89
+ const query = await getValidatedQuery(event, z.object({
90
+ search: z.string().optional(),
91
+ page: z.coerce.number().default(1),
92
+ }));
93
+
94
+ const body = await readValidatedBody(event, z.object({
95
+ email: z.string().email(),
96
+ name: z.string().min(1),
97
+ }));
98
+ ```
99
+
100
+ ### $fetch Type Inference
101
+
102
+ ```typescript
103
+ // Template literals preserve type inference (fixed late 2024)
104
+ const userId = "123"; // Literal type "123"
105
+ const result = await $fetch(`/api/users/${userId}`);
106
+ // result is typed from the handler's return type
107
+
108
+ // NEVER do this - defeats type inference
109
+ const result = await $fetch<User>("/api/users/123"); // WRONG
110
+ ```
111
+
112
+ ### useFetch for Page Data
113
+
114
+ ```typescript
115
+ // Basic - types inferred from Nitro
116
+ const { data, status, refresh } = await useFetch("/api/users");
117
+
118
+ // Reactive query params - auto-refetch on change
119
+ const search = ref("");
120
+ const debouncedSearch = refDebounced(search, 300); // Auto-imported
121
+ const { data } = await useFetch("/api/users", {
122
+ query: computed(() => ({
123
+ ...(debouncedSearch.value ? { search: debouncedSearch.value } : {}),
124
+ })),
125
+ });
126
+
127
+ // Dynamic URL with getter
128
+ const userId = ref("123");
129
+ const { data } = await useFetch(() => `/api/users/${userId.value}`);
130
+
131
+ // New options (Nuxt 3.14+)
132
+ const { data } = await useFetch("/api/data", {
133
+ retry: 3, // Retry on failure
134
+ retryDelay: 1000, // Wait between retries
135
+ dedupe: "cancel", // Cancel previous request
136
+ delay: 300, // Debounce the request
137
+ });
138
+ ```
139
+
140
+ ### $fetch for Event Handlers
141
+
142
+ ```typescript
143
+ // ONLY use $fetch in event handlers (onClick, onSubmit)
144
+ const handleSubmit = async () => {
145
+ const result = await $fetch("/api/users", {
146
+ method: "POST",
147
+ body: { name: "Test" },
148
+ });
149
+ };
150
+ ```
151
+
152
+ ### Auth Check in API
153
+
154
+ ```typescript
155
+ // In server/utils/auth.ts
156
+ export async function getAuthenticatedUser(event: H3Event) {
157
+ const session = await getUserSession(event);
158
+ if (!session?.user) {
159
+ throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
160
+ }
161
+ return session.user;
162
+ }
163
+
164
+ // In API handler
165
+ export default defineEventHandler(async (event) => {
166
+ const user = await getAuthenticatedUser(event);
167
+ // user is typed and guaranteed to exist
168
+ });
169
+ ```
170
+
171
+ ### SSR-Safe localStorage
172
+
173
+ ```typescript
174
+ // Option 1: import.meta.client guard
175
+ watch(preference, (value) => {
176
+ if (import.meta.client) {
177
+ localStorage.setItem("pref", value);
178
+ }
179
+ });
180
+
181
+ // Option 2: onMounted
182
+ onMounted(() => {
183
+ const saved = localStorage.getItem("pref");
184
+ if (saved) preference.value = saved;
185
+ });
186
+
187
+ // Option 3: VueUse (SSR-safe)
188
+ const theme = useLocalStorage("theme", "light");
189
+ ```
190
+
191
+ ### Composable vs Util Decision
192
+
193
+ ```
194
+ Needs Nuxt/Vue context (useRuntimeConfig, useRoute, refs)?
195
+ ├─ YES → COMPOSABLE in /composables/use*.ts
196
+ └─ NO → UTIL in /utils/*.ts (client) or /server/utils/*.ts (server)
197
+ ```
198
+
199
+ ## Key Gotchas
200
+
201
+ 1. **Don't use `$fetch` at top level** - Causes double-fetch (SSR + client). Use `useFetch`.
202
+ 2. **Debounce search inputs** - Use `refDebounced` to avoid excessive API calls.
203
+ 3. **Reset pagination on filter change** - Or users see empty page 5 with new filters.
204
+ 4. **Guard browser APIs** - Use `import.meta.client`, `onMounted`, or `<ClientOnly>`.
205
+ 5. **Nitro tasks are single-instance** - Can't run same task twice concurrently. Use DB job queue.
206
+ 6. **useRouteQuery needs Nuxt composables** - Pass `route` and `router` explicitly.
207
+ 7. **Input types aren't auto-generated** - Export Zod schemas for client use.
208
+ 8. **Cookie size limit is 4096 bytes** - Store only essential session data.
209
+ 9. **Ambiguous routes need type assertion** - See below.
210
+ 10. **Never use generic type params with useFetch/$fetch** - See below.
211
+
212
+ ### Ambiguous Route Type Inference
213
+
214
+ Nuxt generates types in `.nuxt/types/nitro-routes.d.ts` with an `InternalApi` object keyed by route paths. When routes overlap, Nuxt can't infer types from template literals:
215
+
216
+ ```typescript
217
+ // Routes: GET /api/projects and GET /api/projects/:id
218
+ // If route.params.id is "", the path matches BOTH routes
219
+ const { data } = await useFetch(`/api/projects/${route.params.id}`);
220
+ // data type: unknown (ambiguous)
221
+
222
+ // Fix: Assert the specific route pattern
223
+ const { data } = await useFetch(`/api/projects/${route.params.id}` as '/api/projects/:id');
224
+ // data type: correctly inferred from /api/projects/:id handler
225
+ ```
226
+
227
+ ### Extracting Types from useFetch (Never Use Generic Params)
228
+
229
+ Never pass type parameters to `useFetch` or `$fetch`:
230
+
231
+ ```typescript
232
+ // WRONG - Lies to type checker, breaks when endpoint changes
233
+ const { data } = await useFetch<Project[]>('/api/projects');
234
+
235
+ // RIGHT - Let Nuxt infer from the actual endpoint
236
+ const { data: projects } = await useFetch('/api/projects');
237
+ ```
238
+
239
+ To use the inferred type elsewhere in your component:
240
+
241
+ ```typescript
242
+ const { data: projects } = await useFetch('/api/projects');
243
+
244
+ // Get the full ref type (Ref<Project[] | null>)
245
+ type ProjectsRef = typeof projects;
246
+
247
+ // Get a single item type from an array response
248
+ type Project = NonNullable<typeof projects.value>[number];
249
+
250
+ // Use in functions/computeds
251
+ function formatProject(project: Project) {
252
+ return `${project.name} - ${project.status}`;
253
+ }
254
+
255
+ const activeProjects = computed(() =>
256
+ projects.value?.filter(p => p.status === 'active') ?? []
257
+ );
258
+ ```
259
+
260
+ This ensures your frontend types stay in sync with your API - if the endpoint return type changes, TypeScript will catch mismatches.
@@ -0,0 +1,228 @@
1
+ # Auth Patterns (nuxt-auth-utils)
2
+
3
+ > **Examples:** [auth-utils.ts](./examples/auth-utils.ts), [auth-middleware.ts](./examples/auth-middleware.ts)
4
+
5
+ nuxt-auth-utils supports 40+ OAuth providers and includes WebAuthn (passkey) support.
6
+
7
+ ## Server-side Functions (auto-imported)
8
+
9
+ | Function | Purpose |
10
+ |----------|---------|
11
+ | `getUserSession(event)` | Get session (null if not logged in) |
12
+ | `setUserSession(event, data)` | Create/update session (merges) |
13
+ | `replaceUserSession(event, data)` | Replace entire session (no merge) |
14
+ | `clearUserSession(event)` | Clear session (logout) |
15
+ | `requireUserSession(event)` | Get session or throw 401 |
16
+
17
+ ### Password Utilities
18
+
19
+ | Function | Purpose |
20
+ |----------|---------|
21
+ | `hashPassword(password)` | Hash with scrypt |
22
+ | `verifyPassword(hash, password)` | Verify password |
23
+ | `passwordNeedsRehash(hash)` | Check if rehash needed |
24
+
25
+ ## Client-side Composable
26
+
27
+ ```typescript
28
+ const {
29
+ ready, // Computed<boolean> - session loaded?
30
+ loggedIn, // Computed<boolean> - is logged in?
31
+ user, // Computed<User | null> - user data
32
+ session, // Ref<Session> - full session
33
+ fetch, // () => Promise<void> - refresh
34
+ clear, // () => Promise<void> - logout
35
+ openInPopup, // (url: string) => void - OAuth popup
36
+ } = useUserSession();
37
+ ```
38
+
39
+ ## OAuth Handler Pattern
40
+
41
+ ```typescript
42
+ // server/api/auth/google.get.ts
43
+ export default defineOAuthGoogleEventHandler({
44
+ config: {
45
+ clientId: config.oauth.google.clientId,
46
+ clientSecret: config.oauth.google.clientSecret,
47
+ },
48
+ async onSuccess(event, { user, tokens }) {
49
+ const dbUser = await findOrCreateUser(user.email, user);
50
+
51
+ await setUserSession(event, {
52
+ user: {
53
+ id: dbUser.id,
54
+ email: dbUser.email,
55
+ name: dbUser.name,
56
+ role: dbUser.role,
57
+ },
58
+ });
59
+
60
+ return sendRedirect(event, dbUser.role === "admin" ? "/dashboard" : "/home");
61
+ },
62
+ onError(event, error) {
63
+ console.error("OAuth error:", error);
64
+ return sendRedirect(event, "/login?error=oauth");
65
+ },
66
+ });
67
+ ```
68
+
69
+ Client trigger:
70
+ ```typescript
71
+ const { openInPopup } = useUserSession();
72
+ const loginWithGoogle = () => openInPopup("/api/auth/google");
73
+ ```
74
+
75
+ ## WebAuthn (Passkeys)
76
+
77
+ ```typescript
78
+ // Server: Register credential
79
+ export default defineWebAuthnRegisterEventHandler({
80
+ async onSuccess(event, { credential, user }) {
81
+ await db.insertInto("webauthn_credentials").values({
82
+ user_id: user.id,
83
+ credential_id: credential.id,
84
+ public_key: credential.publicKey,
85
+ }).execute();
86
+ },
87
+ });
88
+
89
+ // Server: Authenticate
90
+ export default defineWebAuthnAuthenticateEventHandler({
91
+ async getCredential(event, credentialId) {
92
+ return await db.selectFrom("webauthn_credentials")
93
+ .where("credential_id", "=", credentialId)
94
+ .executeTakeFirst();
95
+ },
96
+ async onSuccess(event, { credential, user }) {
97
+ await setUserSession(event, { user });
98
+ },
99
+ });
100
+ ```
101
+
102
+ ```typescript
103
+ // Client
104
+ const { register, authenticate } = useWebAuthn();
105
+ await register({ userName: user.email });
106
+ await authenticate();
107
+ ```
108
+
109
+ ## Server Middleware
110
+
111
+ ```typescript
112
+ // server/middleware/auth.ts
113
+ export default defineEventHandler(async (event) => {
114
+ // Skip auth routes
115
+ if (event.path.startsWith("/api/auth")) return;
116
+
117
+ if (event.path.startsWith("/api")) {
118
+ const session = await getUserSession(event);
119
+ if (!session?.user) {
120
+ throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
121
+ }
122
+
123
+ // Role-based restrictions
124
+ if (event.path.startsWith("/api/admin") && session.user.role !== "admin") {
125
+ throw createError({ statusCode: 403, statusMessage: "Forbidden" });
126
+ }
127
+ }
128
+ });
129
+ ```
130
+
131
+ ## Client Middleware
132
+
133
+ ```typescript
134
+ // middleware/auth.global.ts
135
+ export default defineNuxtRouteMiddleware((to) => {
136
+ const { loggedIn, user } = useUserSession();
137
+ const publicRoutes = ["/login", "/signup"];
138
+
139
+ if (!loggedIn.value && !publicRoutes.includes(to.path)) {
140
+ return navigateTo("/login");
141
+ }
142
+
143
+ if (loggedIn.value && to.path === "/login") {
144
+ return navigateTo("/");
145
+ }
146
+ });
147
+ ```
148
+
149
+ Named middleware:
150
+ ```typescript
151
+ // middleware/admin.ts
152
+ export default defineNuxtRouteMiddleware(() => {
153
+ const { loggedIn, user } = useUserSession();
154
+ if (!loggedIn.value || user.value?.role !== "admin") {
155
+ return navigateTo("/");
156
+ }
157
+ });
158
+
159
+ // pages/admin/dashboard.vue
160
+ definePageMeta({ middleware: "admin" });
161
+ ```
162
+
163
+ ## Reusable Auth Helpers
164
+
165
+ ```typescript
166
+ // server/utils/auth.ts
167
+ export async function getAuthenticatedUser(event: H3Event) {
168
+ const session = await getUserSession(event);
169
+ if (!session?.user) {
170
+ throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
171
+ }
172
+ return session.user;
173
+ }
174
+
175
+ export async function requireRole(event: H3Event, roles: string[]) {
176
+ const user = await getAuthenticatedUser(event);
177
+ if (!roles.includes(user.role)) {
178
+ throw createError({ statusCode: 403, statusMessage: "Forbidden" });
179
+ }
180
+ return user;
181
+ }
182
+
183
+ export async function requireAdmin(event: H3Event) {
184
+ return requireRole(event, ["admin", "superadmin"]);
185
+ }
186
+ ```
187
+
188
+ ## Type Extension
189
+
190
+ ```typescript
191
+ // types/auth.d.ts
192
+ declare module "#auth-utils" {
193
+ interface User {
194
+ id: number;
195
+ email: string;
196
+ name: string;
197
+ role: "admin" | "user";
198
+ }
199
+
200
+ interface UserSession {
201
+ loggedInAt: string;
202
+ }
203
+
204
+ interface SecureSessionData {
205
+ internalToken?: string; // Server-only
206
+ }
207
+ }
208
+ ```
209
+
210
+ ## Configuration
211
+
212
+ ```bash
213
+ # Required (32+ chars, auto-generated in dev)
214
+ NUXT_SESSION_PASSWORD=your-super-secret-password-at-least-32-chars
215
+
216
+ # OAuth (per-provider)
217
+ NUXT_OAUTH_GOOGLE_CLIENT_ID=...
218
+ NUXT_OAUTH_GOOGLE_CLIENT_SECRET=...
219
+ ```
220
+
221
+ ## Key Gotchas
222
+
223
+ 1. **Skip auth routes in middleware** - `/api/auth/*` and `/api/_auth/*`
224
+ 2. **Use `openInPopup` for OAuth** - Better UX than redirect
225
+ 3. **Cookie size limit is 4096 bytes** - Store only essential data
226
+ 4. **setUserSession merges** - Use `replaceUserSession` to replace
227
+ 5. **requireUserSession throws** - Use getUserSession for null
228
+ 6. **Cannot use with `nuxt generate`** - Requires running server
@@ -0,0 +1,174 @@
1
+ # Composables vs Utils
2
+
3
+ ## Quick Decision Tree
4
+
5
+ ```
6
+ Needs Nuxt/Vue context (useRuntimeConfig, useRoute, refs, toast)?
7
+ ├─ YES → COMPOSABLE in /composables/use*.ts
8
+
9
+ └─ NO
10
+ └─ Server-side logic (DB, file system, auth)?
11
+ ├─ YES → SERVER UTILS in /server/utils/
12
+
13
+ └─ NO (Pure data transformation)
14
+ └─ CLIENT UTILS in /utils/
15
+ ```
16
+
17
+ ## Composables (`/composables/use*.ts`)
18
+
19
+ **When to use:**
20
+ - Accesses Nuxt/Vue context: `useRuntimeConfig()`, `useRoute()`, `navigateTo()`
21
+ - Uses Vue reactivity: `ref()`, `computed()`, `watch()` (optional!)
22
+ - Accesses global services: `useToast()`, `useUserSession()`
23
+ - Named with `use` prefix (required for auto-import)
24
+
25
+ > **Note:** A composable does NOT need reactivity. If it accesses any Nuxt composable, it's a composable.
26
+
27
+ ```typescript
28
+ // composables/useFormState.ts
29
+ export const useFormState = (initialData: FormData) => {
30
+ const data = ref(initialData);
31
+ const isDirty = computed(() =>
32
+ JSON.stringify(data.value) !== JSON.stringify(initialData)
33
+ );
34
+ const errors = ref<Record<string, string>>({});
35
+ const toast = useToast();
36
+
37
+ watch(data, (newValue) => {
38
+ const result = schema.safeParse(newValue);
39
+ errors.value = result.success ? {} : formatErrors(result.error);
40
+ }, { deep: true });
41
+
42
+ const save = async () => {
43
+ try {
44
+ await $fetch("/api/save", { method: "POST", body: data.value });
45
+ toast.add({ severity: "success", summary: "Saved!" });
46
+ } catch (e) {
47
+ toast.add({ severity: "error", summary: "Failed" });
48
+ }
49
+ };
50
+
51
+ return { data, isDirty, errors, save };
52
+ };
53
+ ```
54
+
55
+ ```typescript
56
+ // composables/usePermissions.ts
57
+ export const usePermissions = () => {
58
+ const { user } = useUserSession();
59
+
60
+ const hasRole = (role: string) => user.value?.role === role;
61
+ const isAdmin = () => hasRole("admin") || hasRole("superadmin");
62
+
63
+ const can = (action: string, resource: string) => {
64
+ if (!user.value) return false;
65
+ if (isAdmin()) return true;
66
+ // User-specific permissions
67
+ return false;
68
+ };
69
+
70
+ return { hasRole, isAdmin, can };
71
+ };
72
+ ```
73
+
74
+ ## Client Utils (`/utils/*.ts`)
75
+
76
+ **When to use:**
77
+ - Pure functions, no side effects
78
+ - No Vue/Nuxt dependencies
79
+ - Data transformations, formatting, parsing
80
+ - NO `use` prefix
81
+
82
+ ```typescript
83
+ // utils/formatting.ts
84
+ export const formatDate = (date: string) => {
85
+ return new Date(date).toLocaleDateString("en-US", {
86
+ year: "numeric",
87
+ month: "short",
88
+ day: "numeric",
89
+ });
90
+ };
91
+
92
+ export const formatCurrency = (amount: number) => {
93
+ return new Intl.NumberFormat("en-US", {
94
+ style: "currency",
95
+ currency: "USD",
96
+ }).format(amount);
97
+ };
98
+
99
+ export const generateColor = (id: number) => {
100
+ const colors = ["#3B82F6", "#EF4444", "#10B981"];
101
+ return colors[id % colors.length];
102
+ };
103
+ ```
104
+
105
+ ## Server Utils (`/server/utils/*.ts`)
106
+
107
+ **When to use:**
108
+ - Server-side only logic
109
+ - Database access
110
+ - Authentication helpers
111
+ - External APIs, file system
112
+ - Auto-imported in `/server` directory
113
+
114
+ ```typescript
115
+ // server/utils/db.ts
116
+ import { Kysely, PostgresDialect } from "kysely";
117
+ import pg from "pg";
118
+
119
+ const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
120
+ export const db = new Kysely({ dialect: new PostgresDialect({ pool }) });
121
+
122
+ export function useDatabase() {
123
+ return db;
124
+ }
125
+ ```
126
+
127
+ ```typescript
128
+ // server/utils/auth.ts
129
+ export async function getAuthenticatedUser(event: H3Event) {
130
+ const session = await getUserSession(event);
131
+ if (!session?.user) {
132
+ throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
133
+ }
134
+ return session.user;
135
+ }
136
+ ```
137
+
138
+ ## Shared Utils (`/shared/utils/` - Nuxt 3.14+)
139
+
140
+ **When to use:**
141
+ - Code used on BOTH client and server
142
+ - Types, constants, pure functions
143
+ - NO browser APIs, NO server-only code
144
+
145
+ ```typescript
146
+ // shared/utils/format.ts
147
+ export function formatCurrency(amount: number) {
148
+ return new Intl.NumberFormat("en-US", {
149
+ style: "currency",
150
+ currency: "USD",
151
+ }).format(amount);
152
+ }
153
+
154
+ // Can be used in both:
155
+ // - /server/api/invoice.get.ts
156
+ // - /pages/invoice.vue
157
+ ```
158
+
159
+ ## Summary Table
160
+
161
+ | Location | Naming | Vue APIs | Auto-imported | Use Case |
162
+ |----------|--------|----------|---------------|----------|
163
+ | `/composables/` | `use*` | Yes | Yes (client) | Reactive state, global services |
164
+ | `/utils/` | Any | No | Yes (client) | Pure functions, formatting |
165
+ | `/server/utils/` | Any | No | Yes (server) | DB, auth, server logic |
166
+ | `/shared/utils/` | Any | No | Yes (both) | Isomorphic utilities |
167
+
168
+ ## Key Gotchas
169
+
170
+ 1. **Composables must start with `use`** - Required for auto-import
171
+ 2. **Don't use Vue APIs in utils** - Keeps them testable and portable
172
+ 3. **Server utils can't use Vue** - Different runtime
173
+ 4. **Auto-import scoping** - `/utils` is client-only, `/server/utils` is server-only
174
+ 5. **Composables call order matters** - Call at top of `<script setup>`, not in callbacks