| Dimension | Shared | Only in A | Only in B | Overlap |
|---|---|---|---|---|
| Sections | 0 | 0 | 25 | 0% |
| Commands | 0 | 0 | 0 | — |
| Section tags | 1 | 2 | 4 | 14% |
What each file covers
Sections
0 shared · 0 only in A · 25 only in B- + Project Instructions
- + Overview
- + Tech Stack
- + Project Structure
- + Rules
- + General Rules
- + Frontend Rules
- + Backend Rules
- + Auth Rules
- + Payments Rules
- + Analytics Rules
- + Storage Rules
- + Organization
- + Buckets
- + File Structure
- + Actions
- + File Handling
- + Upload Rules
- + Download Rules
- + Delete Rules
- + Security
- + Bucket Policies
- + Access Control
- + Error Handling
- + Optimization
Commands
neither file has anySection tags
1 shared · 2 only in A · 4 only in B- − types
- − ui
- + architecture
- + security
- + do-not
- + agent-behaviour
- code-style
Line diff
sportiz91/vibe-template · .cursor/rules/coding-standards.mdc
@@ −1 @@
1---
2description:
3globs:
4alwaysApply: false
5---
6Rule Name: coding-standards
7Description:
8This rule defines the coding standards and formatting guidelines that must be followed for all code changes in this project.
9
10<coding_format>
11
12A. Syntax & Structure
13- File names must be dash-case (word-cloud.service.ts) unless an existing pattern differs.
14- Group imports: node/standard → npm packages → internal paths. No unused imports.
15- Use arrow functions everywhere except inside class bodies, where concise method syntax is allowed.
16- Prefer early returns; nested if/else blocks deeper than two levels are disallowed.
17- Early returns must use block format with braces (e.g., `if (!value) { return }`) for readability.
18- Extract function call results as scope variables before using in conditions (e.g., `const trimmedText = text.trim(); if (!trimmedText) {...}` instead of `if (!text.trim()) {...}`).
19- Use async/await—never chain .then().
20- No .forEach for side effects; use for (const x of arr) instead.
21- Array combinators (map, reduce, filter) are allowed only when you return their result.
22- Identifiers must be English.
23- No commented code allowed.
24
25B. Functional-Programming Rules
26- Each function must:
27 * Be ≤ 50 lines (preferably; extract helpers if longer).
28 * Take ≤ 4 parameters (optional ones last).
29 * Have a single responsibility.
30 * Be pure unless it is an intentional I/O wrapper (e.g. DB write); such wrappers must be ≤ 15 lines.
31 * Name functions with camelCase imperative verbs (calculateTotals, getUserById).
32
33C. Type Safety & Error Handling
34- Explicitly type all function parameters, return types, and exported constants.
35- Type all local variables inside a function.
36- **Special attention for async operations**: Variables from awaited functions (e.g., `const { userId } = await auth()`) must be explicitly typed, especially in Next.js components where auth results should use proper domain types.
37- No any; if an external library forces it, wrap and narrow.
38- Error handling in catch blocks:
39 - If the error variable is not used, use `catch {}` (no parameter).
40 - If the error is used, type it as `unknown` and handle it safely within the catch block.
41
42D. React Component Standards
43
44- Always define props with interfaces, never inline types
45- Place interfaces directly above component definitions
46- Use const arrow functions for component definitions
47- Use implicit return syntax when components only return JSX (no logic before return)
48- Export components using export default pattern (required for Next.js pages/layouts)
49- Handler functions inside components must be ≤ 20 lines and have a single, clear responsibility. Extract helper functions for complex logic.
50
51 **Wrong (~50 lines in one handler):**
52 ```tsx
53 const handleFormSubmit = async (): Promise<void> => {
54 const trimmedName: string = formData.name.trim()
55 const trimmedEmail: string = formData.email.trim()
56 const trimmedMessage: string = formData.message.trim()
57
58 if (!trimmedName) {
59 setErrors({ ...errors, name: "Name is required" })
60 toast({ title: "Error", description: "Name is required", variant: "destructive" })
61 return
62 }
63
64 if (!trimmedEmail || !trimmedEmail.includes("@")) {
65 setErrors({ ...errors, email: "Valid email is required" })
66 toast({ title: "Error", description: "Valid email is required", variant: "destructive" })
67 return
68 }
69
70 if (!trimmedMessage || trimmedMessage.length < 10) {
71 setErrors({ ...errors, message: "Message must be at least 10 characters" })
72 toast({ title: "Error", description: "Message too short", variant: "destructive" })
73 return
74 }
75
76 setIsSubmitting(true)
77 setErrors({})
78
79 try {
80 const payload: FormPayload = {
81 name: trimmedName,
82 email: trimmedEmail,
83 message: trimmedMessage,
84 timestamp: new Date().toISOString()
85 }
86
87 const response: Response = await fetch("/api/contact", {
88 method: "POST",
89 headers: { "Content-Type": "application/json" },
90 body: JSON.stringify(payload)
91 })
92
93 if (!response.ok) {
94 throw new Error("Failed to submit")
95 }
96
97 const result: SubmissionResult = await response.json()
98
99 setFormData({ name: "", email: "", message: "" })
100 setSubmissionCount(prev => prev + 1)
101
102 toast({ title: "Success", description: "Message sent successfully!" })
103
104 if (onSuccess) {
105 onSuccess(result)
106 }
107 } catch (error: unknown) {
108 const errorMessage: string = error instanceof Error ? error.message : "Unknown error"
109 console.error("Submission error:", errorMessage)
110 setErrors({ submit: "Failed to send message" })
111 toast({ title: "Error", description: "Failed to send message", variant: "destructive" })
112 } finally {
113 setIsSubmitting(false)
114 }
115 }
116 ```
117
118 **Good (broken into focused helpers ≤ 20 lines each):**
119 ```tsx
120 const validateForm = (): boolean => {
121 const trimmedName: string = formData.name.trim()
122 const trimmedEmail: string = formData.email.trim()
123 const trimmedMessage: string = formData.message.trim()
124
125 if (!trimmedName) {
126 setErrors({ ...errors, name: "Name is required" })
127 toast({ title: "Error", description: "Name is required", variant: "destructive" })
128 return false
129 }
130
131 if (!trimmedEmail || !trimmedEmail.includes("@")) {
132 setErrors({ ...errors, email: "Valid email is required" })
133 toast({ title: "Error", description: "Valid email is required", variant: "destructive" })
134 return false
135 }
136
137 if (!trimmedMessage || trimmedMessage.length < 10) {
138 setErrors({ ...errors, message: "Message must be at least 10 characters" })
139 toast({ title: "Error", description: "Message too short", variant: "destructive" })
140 return false
141 }
142
143 return true
144 }
145
146 const submitForm = async (): Promise<SubmissionResult> => {
147 const payload: FormPayload = {
148 name: formData.name.trim(),
149 email: formData.email.trim(),
150 message: formData.message.trim(),
151 timestamp: new Date().toISOString()
152 }
153
154 const response: Response = await fetch("/api/contact", {
155 method: "POST",
156 headers: { "Content-Type": "application/json" },
157 body: JSON.stringify(payload)
158 })
159
160 if (!response.ok) {
161 throw new Error("Failed to submit")
162 }
163
164 return response.json()
165 }
166
167 const handleSuccess = (result: SubmissionResult): void => {
168 setFormData({ name: "", email: "", message: "" })
169 setSubmissionCount((prev: number) => prev + 1)
170 toast({ title: "Success", description: "Message sent successfully!" })
171
172 if (onSuccess) {
173 onSuccess(result)
174 }
175 }
176
177 const handleError = (error: unknown): void => {
178 const errorMessage: string = error instanceof Error ? error.message : "Unknown error"
179 console.error("Submission error:", errorMessage)
180 setErrors({ submit: "Failed to send message" })
181 toast({ title: "Error", description: "Failed to send message", variant: "destructive" })
182 }
183
184 const handleFormSubmit = async (): Promise<void> => {
185 const isValid: boolean = validateForm()
186
187 if (!isValid) {
188 return
189 }
190
191 setIsSubmitting(true)
192 setErrors({})
193
194 try {
195 const result: SubmissionResult = await submitForm()
196 handleSuccess(result)
197 } catch (error: unknown) {
198 handleError(error)
199 } finally {
200 setIsSubmitting(false)
201 }
202 }
203 ```
204- Example with implicit return:
205
206 ```tsx
207 interface MyComponentProps {
208 title: string
209 children: React.ReactNode
210 }
211
212 const MyComponent = ({ title, children }: MyComponentProps) => (
213 <div>
214 {title}
215 {children}
216 </div>
217 )
218
219 export default MyComponent
220 ```
221
222- Example with explicit return (when logic is present):
223
224 ```tsx
225 interface MyComponentProps {
226 title: string
227 children: React.ReactNode
228 }
229
230 const MyComponent = ({ title, children }: MyComponentProps) => {
231 const processedTitle = title.toUpperCase()
232
233 return (
234 <div>
235 {processedTitle}
236 {children}
237 </div>
238 )
239 }
240
241 export default MyComponent
242 ```
243
244- Normal components that are not Next.js pages/layouts should be exported
245 using export const pattern
246- Example with implicit return:
247
248 ```tsx
249 interface NotAPageOrLayoutComponentProps {
250 title: string
251 children: React.ReactNode
252 }
253
254 export const NotAPageOrLayoutComponent = ({
255 title,
256 children
257 }: NotAPageOrLayoutComponentProps) => (
258 <div>
259 {title}
260 {children}
261 </div>
262 )
263
264E. Component Granularity & Organization
265- Break down large components into smaller, focused components for better maintainability.
266- When a component contains multiple logical sections (e.g., Card with CardHeader + CardContent), extract each section into separate components.
267- Create dedicated folders for related component groups:
268 * Use kebab-case folder names matching the main component concept
269 * Place related sub-components within the same folder using kebab-case file names
270 * Example structure: `component-name/component-name-header.tsx`, `component-name/component-name-content.tsx`
271- Each sub-component should have a single, clear responsibility.
272- Maintain the parent component as a composition wrapper that orchestrates child components.
273- Follow this pattern when refactoring existing components or creating new feature components.
274
275F. Advanced Component Architecture Patterns
276
277F.1. Pure Functions and Constants Organization
278- **Pure functions** (no side effects, deterministic output) must be extracted outside components:
279 * Place above the component definition
280 * Examples: `getGreeting()`, `getMembershipBadgeColor()`, `formatDate()`
281- **Constants and static data** must be moved outside components:
282 * Place after imports and interfaces, before pure functions
283 * Use SCREAMING_SNAKE_CASE for naming (e.g., `TEMPLATE_FEATURES`, `TECH_STACK`)
284 * **Always explicitly type constants** with appropriate type annotations
285 * Examples: `const API_URL: string = "..."`, `const MAX_RETRIES: number = 3`
286 * Use `as const` for immutable values when type inference is sufficient
287 * Group related constants together
288
289F.1.1. Custom Hooks Organization
290- **Custom hooks** must be extracted to separate files in the `/hooks/` directory:
291 * Use kebab-case file naming: `use-scroll-detection.ts`, `use-local-storage.ts`
292 * Start hook names with `use` prefix following React conventions
293 * Place hooks in `/hooks/` folder at project root level
294 * Export hooks using named exports: `export const useScrollDetection = () => {}`
295 * **Always explicitly type hook return values** and parameters
296 * Examples: `useScrollDetection(): boolean`, `useLocalStorage<T>(key: string): [T, (value: T) => void]`
297 * Group related hooks in the same file only if they're tightly coupled
298
299F.1.2. Whitespace and Formatting Rules
300- **Component variable organization**: Maintain consistent whitespace between different types of declarations:
301 * Add a blank line between React state declarations and custom hook calls
302 * Add a blank line between custom hook calls and other variable declarations
303 * Example:
304 ```tsx
305 const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false)
306 const [isVisible, setIsVisible] = useState<boolean>(true)
307
308 const isScrolled: boolean = useScrollDetection()
309 const userData: UserData = useUserData()
310
311 const processedData: ProcessedData = processUserData(userData)
312 ```
313
314F.2. Nested Component Structure for Complex Components
315When a component has multiple distinct sections, create nested folder structure:
316
317```
318dashboard-welcome/
319├── dashboard-welcome.tsx // Main orchestrator component
320├── greeting.tsx // Self-contained greeting section
321├── whats-included/ // Folder for multi-part section
322│ ├── whats-included.tsx // Section orchestrator
323│ ├── whats-included-title.tsx // Title sub-component
324│ └── whats-included-features.tsx // Features list sub-component
325├── core-technologies/ // Folder for multi-part section
326│ ├── core-technologies.tsx // Section orchestrator
327│ ├── core-technologies-title.tsx // Title sub-component
328│ └── core-technologies-list.tsx // Tech list sub-component
329└── get-started/ // Folder for multi-part section
330 ├── get-started.tsx // Section orchestrator
331 ├── get-started-title.tsx // Title sub-component
332 ├── get-started-features.tsx // Features grid sub-component
333 └── get-started-feature-2.tsx // Individual feature card
334```
335
336F.3. Component Organization Rules
3371. **Main orchestrator**: Composition only, minimal logic, imports and renders sub-components
3382. **Section orchestrators**: Handle section-specific logic, render related sub-components
3393. **Leaf components**: Single responsibility, pure presentation, accept props only
3404. **Shared constants**: Extract to file level, use proper naming conventions
3415. **Pure functions**: Extract above component definitions, properly typed
3426. **File structure**: Mirror logical component hierarchy in folder structure
343
344</coding_format>
345
346<usage_guidelines>
347
3481. Apply these standards to all new code and when refactoring existing code.
3492. When making any code changes, ensure they conform to these guidelines.
3503. If existing code doesn't follow these standards, update it to comply when modifying those files.
3514. Use these standards as a checklist when reviewing code changes.
3525. Prefer extracting helper functions over writing long, complex functions.
3536. Always prioritize code readability and maintainability.
354
355</usage_guidelines>
sportiz91/vibe-template · .cursorrules
@@ +1 @@
1# Project Instructions
2
3Use specification and guidelines as you build the app.
4
5Write the complete code for every step. Do not get lazy.
6
7Your goal is to completely finish whatever I ask for.
8
9You will see <ai_context> tags in the code. These are context tags that you should use to help you understand the codebase.
10
11## Overview
12
13This is a web app template.
14
15## Tech Stack
16
17- Frontend: Next.js, Tailwind, Shadcn, Framer Motion
18- Backend: Postgres, Supabase, Drizzle ORM, Server Actions
19- Auth: Clerk
20- Payments: Stripe
21- Analytics: PostHog
22- Deployment: Vercel
23
24## Project Structure
25
26- `actions` - Server actions
27 - `db` - Database related actions
28 - Other actions
29- `app` - Next.js app router
30 - `api` - API routes
31 - `route` - An example route
32 - `_components` - One-off components for the route
33 - `layout.tsx` - Layout for the route
34 - `page.tsx` - Page for the route
35- `components` - Shared components
36 - `ui` - UI components
37 - `utilities` - Utility components
38- `db` - Database
39 - `schema` - Database schemas
40- `lib` - Library code
41 - `hooks` - Custom hooks
42 - `services` - Business logic services
43- `prompts` - Prompt files
44- `public` - Static assets
45- `types` - Type definitions
46
47## Rules
48
49Follow these rules when building the app.
50
51### General Rules
52
53- Use `@` to import anything from the app unless otherwise specified
54- Use kebab case for all files and folders unless otherwise specified
55- Don't update shadcn components unless otherwise specified
56
57#### Env Rules
58
59- If you update environment variables, update the `.env.example` file
60- All environment variables should go in `.env.local`
61- Do not expose environment variables to the frontend
62- Use `NEXT_PUBLIC_` prefix for environment variables that need to be accessed from the frontend
63- Always access environment variables via the centralized config module (`serverConfig` and `publicEnv` from `@/lib/config`). Do not use `process.env` directly in application code.
64
65#### Type Rules
66
67Follow these rules when working with types.
68
69- When importing types, use `@/types`
70- Name files like `example-types.ts`
71- All types should go in `types`
72- Make sure to export the types in `types/index.ts`
73- Prefer interfaces over type aliases
74- If referring to db types, use `@/db/schema` such as `SelectTodo` from `todos-schema.ts`
75
76An example of a type:
77
78`types/actions-types.ts`
79
80```ts
81export type ActionState<T> =
82 | { isSuccess: true; message: string; data: T }
83 | { isSuccess: false; message: string; data?: never }
84```
85
86And exporting it:
87
88`types/index.ts`
89
90```ts
91export * from "./actions-types"
92```
93
94### Frontend Rules
95
96Follow these rules when working on the frontend.
97
98It uses Next.js, Tailwind, Shadcn, and Framer Motion.
99
100#### General Rules
101
102- Use `lucide-react` for icons
103- useSidebar must be used within a SidebarProvider
104
105#### Components
106
107- Use divs instead of other html tags unless otherwise specified
108- Separate the main parts of a component's html with an extra blank line for visual spacing
109- Always tag a component with either `use server` or `use client` at the top, including layouts and pages
110
111##### Organization
112
113- All components be named using kebab case like `example-component.tsx` unless otherwise specified
114- Put components in `/_components` in the route if one-off components
115- Put components in `/components` from the root if shared components
116
117##### Data Fetching
118
119- Fetch data in server components and pass the data down as props to client components.
120- Use server actions from `/actions` to mutate data.
121
122##### Server Components
123
124- Use `"use server"` at the top of the file.
125- Implement Suspense for asynchronous data fetching to show loading states while data is being fetched.
126- If no asynchronous logic is required for a given server component, you do not need to wrap the component in `<Suspense>`. You can simply return the final UI directly since there is no async boundary needed.
127- If asynchronous fetching is required, you can use a `<Suspense>` boundary and a fallback to indicate a loading state while data is loading.
128- Server components cannot be imported into client components. If you want to use a server component in a client component, you must pass the as props using the "children" prop
129- params in server pages should be awaited such as `const { courseId } = await params` where the type is `params: Promise<{ courseId: string }>`
130
131Example of a server layout:
132
133```tsx
134"use server"
135
136export default async function ExampleServerLayout({
137 children
138}: {
139 children: React.ReactNode
140}) {
141 return children
142}
143```
144
145Example of a server page (with async logic):
146
147```tsx
148"use server"
149
150import { Suspense } from "react"
151import { SomeAction } from "@/actions/some-actions"
152import SomeComponent from "./_components/some-component"
153import SomeSkeleton from "./_components/some-skeleton"
154
155export default async function ExampleServerPage() {
156 return (
157 <Suspense fallback={<SomeSkeleton className="some-class" />}>
158 <SomeComponentFetcher />
159 </Suspense>
160 )
161}
162
163async function SomeComponentFetcher() {
164 const { data } = await SomeAction()
165 return <SomeComponent className="some-class" initialData={data || []} />
166}
167```
168
169Example of a server page (no async logic required):
170
171```tsx
172"use server"
173
174import SomeClientComponent from "./_components/some-client-component"
175
176// In this case, no asynchronous work is being done, so no Suspense or fallback is required.
177export default async function ExampleServerPage() {
178 return <SomeClientComponent initialData={[]} />
179}
180```
181
182Example of a server component:
183
184```tsx
185"use server"
186
187interface ExampleServerComponentProps {
188 // Your props here
189}
190
191export async function ExampleServerComponent({
192 props
193}: ExampleServerComponentProps) {
194 // Your code here
195}
196```
197
198##### Client Components
199
200- Use `"use client"` at the top of the file
201- Client components can safely rely on props passed down from server components, or handle UI interactions without needing <Suspense> if there's no async logic.
202- Never use server actions in client components. If you need to create a new server action, create it in `/actions`
203
204Example of a client page:
205
206```tsx
207"use client"
208
209export default function ExampleClientPage() {
210 // Your code here
211}
212```
213
214Example of a client component:
215
216```tsx
217"use client"
218
219interface ExampleClientComponentProps {
220 initialData: any[]
221}
222
223export default function ExampleClientComponent({
224 initialData
225}: ExampleClientComponentProps) {
226 // Client-side logic here
227 return <div>{initialData.length} items</div>
228}
229```
230
231### Backend Rules
232
233Follow these rules when working on the backend.
234
235It uses Postgres, Supabase, Drizzle ORM, and Server Actions.
236
237#### General Rules
238
239- Never generate migrations. You do not have to do anything in the `db/migrations` folder inluding migrations and metadata. Ignore it.
240
241#### Organization
242
243#### Schemas
244
245- When importing schemas, use `@/db/schema`
246- Name files like `example-schema.ts`
247- All schemas should go in `db/schema`
248- Make sure to export the schema in `db/schema/index.ts`
249- Make sure to add the schema to the `schema` object in `db/db.ts`
250- If using a userId, always use `userId: text("user_id").notNull()`
251- Always include createdAt and updatedAt columns in all tables
252- Make sure to cascade delete when necessary
253- Use enums for columns that have a limited set of possible values such as:
254
255```ts
256import { pgEnum } from "drizzle-orm/pg-core"
257
258export const MEMBERSHIP: PgEnum<Membership> = pgEnum(
259 "membership",
260 MEMBERSHIP_VALUES
261)
262
263membership: MEMBERSHIP("membership").notNull().default("free")
264```
265
266Example of a schema:
267
268`db/schema/todos-schema.ts`
269
270```ts
271import { boolean, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"
272
273export const todosTable = pgTable("todos", {
274 id: uuid("id").defaultRandom().primaryKey(),
275 userId: text("user_id").notNull(),
276 content: text("content").notNull(),
277 completed: boolean("completed").default(false).notNull(),
278 createdAt: timestamp("created_at").defaultNow().notNull(),
279 updatedAt: timestamp("updated_at")
280 .defaultNow()
281 .notNull()
282 .$onUpdate(() => new Date())
283})
284
285export type InsertTodo = typeof todosTable.$inferInsert
286export type SelectTodo = typeof todosTable.$inferSelect
287```
288
289And exporting it:
290
291`db/schema/index.ts`
292
293```ts
294export * from "./todos-schema"
295```
296
297And adding it to the schema in `db/db.ts`:
298
299`db/db.ts`
300
301```ts
302import { todosTable } from "@/db/schema"
303
304const schema = {
305 todos: todosTable
306}
307```
308
309And a more complex schema:
310
311```ts
312import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"
313
314export const chatsTable = pgTable("chats", {
315 id: uuid("id").defaultRandom().primaryKey(),
316 userId: text("user_id").notNull(),
317 name: text("name").notNull(),
318 createdAt: timestamp("created_at").defaultNow().notNull(),
319 updatedAt: timestamp("updated_at")
320 .defaultNow()
321 .notNull()
322 .$onUpdate(() => new Date())
323})
324
325export type InsertChat = typeof chatsTable.$inferInsert
326export type SelectChat = typeof chatsTable.$inferSelect
327```
328
329```ts
330import { pgEnum, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"
331import { chatsTable } from "./chats-schema"
332
333export const roleEnum = pgEnum("role", ["assistant", "user"])
334
335export const messagesTable = pgTable("messages", {
336 id: uuid("id").defaultRandom().primaryKey(),
337 chatId: uuid("chat_id")
338 .references(() => chatsTable.id, { onDelete: "cascade" })
339 .notNull(),
340 content: text("content").notNull(),
341 role: roleEnum("role").notNull(),
342 createdAt: timestamp("created_at").defaultNow().notNull(),
343 updatedAt: timestamp("updated_at")
344 .defaultNow()
345 .notNull()
346 .$onUpdate(() => new Date())
347})
348
349export type InsertMessage = typeof messagesTable.$inferInsert
350export type SelectMessage = typeof messagesTable.$inferSelect
351```
352
353And exporting it:
354
355`db/schema/index.ts`
356
357```ts
358export * from "./chats-schema"
359export * from "./messages-schema"
360```
361
362And adding it to the schema in `db/db.ts`:
363
364`db/db.ts`
365
366```ts
367import { chatsTable, messagesTable } from "@/db/schema"
368
369const schema = {
370 chats: chatsTable,
371 messages: messagesTable
372}
373```
374
375#### Server Actions
376
377- When importing actions, use `@/actions` or `@/actions/db` if db related
378- DB related actions should go in the `actions/db` folder
379- Other actions should go in the `actions` folder
380- Name files like `example-actions.ts`
381- All actions should go in the `actions` folder
382- Only write the needed actions
383- Return an ActionState with the needed data type from actions
384- Include Action at the end of function names `Ex: exampleFunction -> exampleFunctionAction`
385- Actions should return a Promise<ActionState<T>>
386- Sort in CRUD order: Create, Read, Update, Delete
387- Make sure to return undefined as the data type if the action is not supposed to return any data
388- **Date Handling:** For columns defined as `PgDateString` (or any date string type), always convert JavaScript `Date` objects to ISO strings using `.toISOString()` before performing operations (e.g., comparisons or insertions). This ensures value type consistency and prevents type errors.
389
390```ts
391export type ActionState<T> =
392 | { isSuccess: true; message: string; data: T }
393 | { isSuccess: false; message: string; data?: never }
394```
395
396Example of an action:
397
398`actions/db/todos-actions.ts`
399
400```ts
401"use server"
402
403import { db } from "@/db/db"
404import { InsertTodo, SelectTodo, todosTable } from "@/db/schema/todos-schema"
405import { ActionState } from "@/types"
406import { eq } from "drizzle-orm"
407
408export async function createTodoAction(
409 todo: InsertTodo
410): Promise<ActionState<SelectTodo>> {
411 try {
412 const [newTodo] = await db.insert(todosTable).values(todo).returning()
413 return {
414 isSuccess: true,
415 message: "Todo created successfully",
416 data: newTodo
417 }
418 } catch (error) {
419 console.error("Error creating todo:", error)
420 return { isSuccess: false, message: "Failed to create todo" }
421 }
422}
423
424export async function getTodosAction(
425 userId: string
426): Promise<ActionState<SelectTodo[]>> {
427 try {
428 const todos = await db.query.todos.findMany({
429 where: eq(todosTable.userId, userId)
430 })
431 return {
432 isSuccess: true,
433 message: "Todos retrieved successfully",
434 data: todos
435 }
436 } catch (error) {
437 console.error("Error getting todos:", error)
438 return { isSuccess: false, message: "Failed to get todos" }
439 }
440}
441
442export async function updateTodoAction(
443 id: string,
444 data: Partial<InsertTodo>
445): Promise<ActionState<SelectTodo>> {
446 try {
447 const [updatedTodo] = await db
448 .update(todosTable)
449 .set(data)
450 .where(eq(todosTable.id, id))
451 .returning()
452
453 return {
454 isSuccess: true,
455 message: "Todo updated successfully",
456 data: updatedTodo
457 }
458 } catch (error) {
459 console.error("Error updating todo:", error)
460 return { isSuccess: false, message: "Failed to update todo" }
461 }
462}
463
464export async function deleteTodoAction(id: string): Promise<ActionState<void>> {
465 try {
466 await db.delete(todosTable).where(eq(todosTable.id, id))
467 return {
468 isSuccess: true,
469 message: "Todo deleted successfully",
470 data: undefined
471 }
472 } catch (error) {
473 console.error("Error deleting todo:", error)
474 return { isSuccess: false, message: "Failed to delete todo" }
475 }
476}
477```
478
479### Auth Rules
480
481Follow these rules when working on auth.
482
483It uses Clerk for authentication.
484
485#### General Rules
486
487- Import the auth helper with `import { auth } from "@clerk/nextjs/server"` in server components
488- await the auth helper in server actions
489
490### Payments Rules
491
492Follow these rules when working on payments.
493
494It uses Stripe for payments.
495
496### Analytics Rules
497
498Follow these rules when working on analytics.
499
500It uses PostHog for analytics.
501
502# Storage Rules
503
504Follow these rules when working with Supabase Storage.
505
506It uses Supabase Storage for file uploads, downloads, and management.
507
508## General Rules
509
510- Always use environment variables for bucket names to maintain consistency across environments
511- Never hardcode bucket names in the application code
512- Always handle file size limits and allowed file types at the application level
513- Use the `upsert` method instead of `upload` when you want to replace existing files
514- Always implement proper error handling for storage operations
515- Use content-type headers when uploading files to ensure proper file handling
516
517## Organization
518
519### Buckets
520
521- Name buckets in kebab-case: `user-uploads`, `profile-images`
522- Create separate buckets for different types of files (e.g., `profile-images`, `documents`, `attachments`)
523- Document bucket purposes in a central location
524- Set appropriate bucket policies (public/private) based on access requirements
525- Implement RLS (Row Level Security) policies for buckets that need user-specific access
526- Make sure to let me know instructions for setting up RLS policies on Supabase since you can't do this yourself, including the SQL scripts I need to run in the editor
527
528### File Structure
529
530- Organize files in folders based on their purpose and ownership
531- Use predictable, collision-resistant naming patterns
532- Structure: `{bucket}/{userId}/{purpose}/{filename}`
533- Example: `profile-images/123e4567-e89b/avatar/profile.jpg`
534- Include timestamps in filenames when version history is important
535- Example: `documents/123e4567-e89b/contracts/2024-02-13-contract.pdf`
536
537## Actions
538
539- When importing storage actions, use `@/actions/storage`
540- Name files like `example-storage-actions.ts`
541- Include Storage at the end of function names `Ex: uploadFile -> uploadFileStorage`
542- Follow the same ActionState pattern as DB actions
543
544Example of a storage action:
545
546```ts
547"use server"
548
549import { createClientComponentClient } from "@supabase/auth-helpers-nextjs"
550import { ActionState } from "@/types"
551
552export async function uploadFileStorage(
553 bucket: string,
554 path: string,
555 file: File
556): Promise<ActionState<{ path: string }>> {
557 try {
558 const supabase = createClientComponentClient()
559
560 const { data, error } = await supabase.storage
561 .from(bucket)
562 .upload(path, file, {
563 upsert: false,
564 contentType: file.type
565 })
566
567 if (error) throw error
568
569 return {
570 isSuccess: true,
571 message: "File uploaded successfully",
572 data: { path: data.path }
573 }
574 } catch (error) {
575 console.error("Error uploading file:", error)
576 return { isSuccess: false, message: "Failed to upload file" }
577 }
578}
579```
580
581## File Handling
582
583### Upload Rules
584
585- Always validate file size before upload
586- Implement file type validation using both extension and MIME type
587- Generate unique filenames to prevent collisions
588- Set appropriate content-type headers
589- Handle existing files appropriately (error or upsert)
590
591Example validation:
592
593```ts
594const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB
595const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"]
596
597function validateFile(file: File): boolean {
598 if (file.size > MAX_FILE_SIZE) {
599 throw new Error("File size exceeds limit")
600 }
601
602 if (!ALLOWED_TYPES.includes(file.type)) {
603 throw new Error("File type not allowed")
604 }
605
606 return true
607}
608```
609
610### Download Rules
611
612- Always handle missing files gracefully
613- Implement proper error handling for failed downloads
614- Use signed URLs for private files
615
616### Delete Rules
617
618- Implement soft deletes when appropriate
619- Clean up related database records when deleting files
620- Handle bulk deletions carefully
621- Verify ownership before deletion
622- Always delete all versions/transforms of a file
623
624## Security
625
626### Bucket Policies
627
628- Make buckets private by default
629- Only make buckets public when absolutely necessary
630- Use RLS policies to restrict access to authorized users
631- Example RLS policy:
632
633```sql
634CREATE POLICY "Users can only access their own files"
635ON storage.objects
636FOR ALL
637USING (auth.uid()::text = (storage.foldername(name))[1]);
638```
639
640### Access Control
641
642- Generate short-lived signed URLs for private files
643- Implement proper CORS policies
644- Use separate buckets for public and private files
645- Never expose internal file paths
646- Validate user permissions before any operation
647
648## Error Handling
649
650- Implement specific error types for common storage issues
651- Always provide meaningful error messages
652- Implement retry logic for transient failures
653- Log storage errors separately for monitoring
654
655## Optimization
656
657- Implement progressive upload for large files
658- Clean up temporary files and failed uploads
659- Use batch operations when handling multiple files
660
@@ −1 +1 @@
1−---
2−description:
3−globs:
4−alwaysApply: false
5−---
6−Rule Name: coding-standards
7−Description:
8−This rule defines the coding standards and formatting guidelines that must be followed for all code changes in this project.
1+# Project Instructions
92
10−<coding_format>
3+Use specification and guidelines as you build the app.
114
12−A. Syntax & Structure
13−- File names must be dash-case (word-cloud.service.ts) unless an existing pattern differs.
14−- Group imports: node/standard → npm packages → internal paths. No unused imports.
15−- Use arrow functions everywhere except inside class bodies, where concise method syntax is allowed.
16−- Prefer early returns; nested if/else blocks deeper than two levels are disallowed.
17−- Early returns must use block format with braces (e.g., `if (!value) { return }`) for readability.
18−- Extract function call results as scope variables before using in conditions (e.g., `const trimmedText = text.trim(); if (!trimmedText) {...}` instead of `if (!text.trim()) {...}`).
19−- Use async/await—never chain .then().
20−- No .forEach for side effects; use for (const x of arr) instead.
21−- Array combinators (map, reduce, filter) are allowed only when you return their result.
22−- Identifiers must be English.
23−- No commented code allowed.
5+Write the complete code for every step. Do not get lazy.
246
25−B. Functional-Programming Rules
26−- Each function must:
27− * Be ≤ 50 lines (preferably; extract helpers if longer).
28− * Take ≤ 4 parameters (optional ones last).
29− * Have a single responsibility.
30− * Be pure unless it is an intentional I/O wrapper (e.g. DB write); such wrappers must be ≤ 15 lines.
31− * Name functions with camelCase imperative verbs (calculateTotals, getUserById).
7+Your goal is to completely finish whatever I ask for.
328
33−C. Type Safety & Error Handling
34−- Explicitly type all function parameters, return types, and exported constants.
35−- Type all local variables inside a function.
36−- **Special attention for async operations**: Variables from awaited functions (e.g., `const { userId } = await auth()`) must be explicitly typed, especially in Next.js components where auth results should use proper domain types.
37−- No any; if an external library forces it, wrap and narrow.
38−- Error handling in catch blocks:
39− - If the error variable is not used, use `catch {}` (no parameter).
40− - If the error is used, type it as `unknown` and handle it safely within the catch block.
9+You will see <ai_context> tags in the code. These are context tags that you should use to help you understand the codebase.
4110
42−D. React Component Standards
11+## Overview
4312
44−- Always define props with interfaces, never inline types
45−- Place interfaces directly above component definitions
46−- Use const arrow functions for component definitions
47−- Use implicit return syntax when components only return JSX (no logic before return)
48−- Export components using export default pattern (required for Next.js pages/layouts)
49−- Handler functions inside components must be ≤ 20 lines and have a single, clear responsibility. Extract helper functions for complex logic.
13+This is a web app template.
5014
51− **Wrong (~50 lines in one handler):**
52− ```tsx
53− const handleFormSubmit = async (): Promise<void> => {
54− const trimmedName: string = formData.name.trim()
55− const trimmedEmail: string = formData.email.trim()
56− const trimmedMessage: string = formData.message.trim()
57−
58− if (!trimmedName) {
59− setErrors({ ...errors, name: "Name is required" })
60− toast({ title: "Error", description: "Name is required", variant: "destructive" })
61− return
62− }
63−
64− if (!trimmedEmail || !trimmedEmail.includes("@")) {
65− setErrors({ ...errors, email: "Valid email is required" })
66− toast({ title: "Error", description: "Valid email is required", variant: "destructive" })
67− return
68− }
69−
70− if (!trimmedMessage || trimmedMessage.length < 10) {
71− setErrors({ ...errors, message: "Message must be at least 10 characters" })
72− toast({ title: "Error", description: "Message too short", variant: "destructive" })
73− return
74− }
75−
76− setIsSubmitting(true)
77− setErrors({})
78−
79− try {
80− const payload: FormPayload = {
81− name: trimmedName,
82− email: trimmedEmail,
83− message: trimmedMessage,
84− timestamp: new Date().toISOString()
85− }
86−
87− const response: Response = await fetch("/api/contact", {
88− method: "POST",
89− headers: { "Content-Type": "application/json" },
90− body: JSON.stringify(payload)
91− })
92−
93− if (!response.ok) {
94− throw new Error("Failed to submit")
95− }
96−
97− const result: SubmissionResult = await response.json()
98−
99− setFormData({ name: "", email: "", message: "" })
100− setSubmissionCount(prev => prev + 1)
101−
102− toast({ title: "Success", description: "Message sent successfully!" })
103−
104− if (onSuccess) {
105− onSuccess(result)
106− }
107− } catch (error: unknown) {
108− const errorMessage: string = error instanceof Error ? error.message : "Unknown error"
109− console.error("Submission error:", errorMessage)
110− setErrors({ submit: "Failed to send message" })
111− toast({ title: "Error", description: "Failed to send message", variant: "destructive" })
112− } finally {
113− setIsSubmitting(false)
114− }
115− }
116− ```
15+## Tech Stack
11716
118− **Good (broken into focused helpers ≤ 20 lines each):**
119− ```tsx
120− const validateForm = (): boolean => {
121− const trimmedName: string = formData.name.trim()
122− const trimmedEmail: string = formData.email.trim()
123− const trimmedMessage: string = formData.message.trim()
124−
125− if (!trimmedName) {
126− setErrors({ ...errors, name: "Name is required" })
127− toast({ title: "Error", description: "Name is required", variant: "destructive" })
128− return false
17+- Frontend: Next.js, Tailwind, Shadcn, Framer Motion
18+- Backend: Postgres, Supabase, Drizzle ORM, Server Actions
19+- Auth: Clerk
20+- Payments: Stripe
21+- Analytics: PostHog
22+- Deployment: Vercel
23+
24+## Project Structure
25+
26+- `actions` - Server actions
27+ - `db` - Database related actions
28+ - Other actions
29+- `app` - Next.js app router
30+ - `api` - API routes
31+ - `route` - An example route
32+ - `_components` - One-off components for the route
33+ - `layout.tsx` - Layout for the route
34+ - `page.tsx` - Page for the route
35+- `components` - Shared components
36+ - `ui` - UI components
37+ - `utilities` - Utility components
38+- `db` - Database
39+ - `schema` - Database schemas
40+- `lib` - Library code
41+ - `hooks` - Custom hooks
42+ - `services` - Business logic services
43+- `prompts` - Prompt files
44+- `public` - Static assets
45+- `types` - Type definitions
46+
47+## Rules
48+
49+Follow these rules when building the app.
50+
51+### General Rules
52+
53+- Use `@` to import anything from the app unless otherwise specified
54+- Use kebab case for all files and folders unless otherwise specified
55+- Don't update shadcn components unless otherwise specified
56+
57+#### Env Rules
58+
59+- If you update environment variables, update the `.env.example` file
60+- All environment variables should go in `.env.local`
61+- Do not expose environment variables to the frontend
62+- Use `NEXT_PUBLIC_` prefix for environment variables that need to be accessed from the frontend
63+- Always access environment variables via the centralized config module (`serverConfig` and `publicEnv` from `@/lib/config`). Do not use `process.env` directly in application code.
64+
65+#### Type Rules
66+
67+Follow these rules when working with types.
68+
69+- When importing types, use `@/types`
70+- Name files like `example-types.ts`
71+- All types should go in `types`
72+- Make sure to export the types in `types/index.ts`
73+- Prefer interfaces over type aliases
74+- If referring to db types, use `@/db/schema` such as `SelectTodo` from `todos-schema.ts`
75+
76+An example of a type:
77+
78+`types/actions-types.ts`
79+
80+```ts
81+export type ActionState<T> =
82+ | { isSuccess: true; message: string; data: T }
83+ | { isSuccess: false; message: string; data?: never }
84+```
85+
86+And exporting it:
87+
88+`types/index.ts`
89+
90+```ts
91+export * from "./actions-types"
92+```
93+
94+### Frontend Rules
95+
96+Follow these rules when working on the frontend.
97+
98+It uses Next.js, Tailwind, Shadcn, and Framer Motion.
99+
100+#### General Rules
101+
102+- Use `lucide-react` for icons
103+- useSidebar must be used within a SidebarProvider
104+
105+#### Components
106+
107+- Use divs instead of other html tags unless otherwise specified
108+- Separate the main parts of a component's html with an extra blank line for visual spacing
109+- Always tag a component with either `use server` or `use client` at the top, including layouts and pages
110+
111+##### Organization
112+
113+- All components be named using kebab case like `example-component.tsx` unless otherwise specified
114+- Put components in `/_components` in the route if one-off components
115+- Put components in `/components` from the root if shared components
116+
117+##### Data Fetching
118+
119+- Fetch data in server components and pass the data down as props to client components.
120+- Use server actions from `/actions` to mutate data.
121+
122+##### Server Components
123+
124+- Use `"use server"` at the top of the file.
125+- Implement Suspense for asynchronous data fetching to show loading states while data is being fetched.
126+- If no asynchronous logic is required for a given server component, you do not need to wrap the component in `<Suspense>`. You can simply return the final UI directly since there is no async boundary needed.
127+- If asynchronous fetching is required, you can use a `<Suspense>` boundary and a fallback to indicate a loading state while data is loading.
128+- Server components cannot be imported into client components. If you want to use a server component in a client component, you must pass the as props using the "children" prop
129+- params in server pages should be awaited such as `const { courseId } = await params` where the type is `params: Promise<{ courseId: string }>`
130+
131+Example of a server layout:
132+
133+```tsx
134+"use server"
135+
136+export default async function ExampleServerLayout({
137+ children
138+}: {
139+ children: React.ReactNode
140+}) {
141+ return children
142+}
143+```
144+
145+Example of a server page (with async logic):
146+
147+```tsx
148+"use server"
149+
150+import { Suspense } from "react"
151+import { SomeAction } from "@/actions/some-actions"
152+import SomeComponent from "./_components/some-component"
153+import SomeSkeleton from "./_components/some-skeleton"
154+
155+export default async function ExampleServerPage() {
156+ return (
157+ <Suspense fallback={<SomeSkeleton className="some-class" />}>
158+ <SomeComponentFetcher />
159+ </Suspense>
160+ )
161+}
162+
163+async function SomeComponentFetcher() {
164+ const { data } = await SomeAction()
165+ return <SomeComponent className="some-class" initialData={data || []} />
166+}
167+```
168+
169+Example of a server page (no async logic required):
170+
171+```tsx
172+"use server"
173+
174+import SomeClientComponent from "./_components/some-client-component"
175+
176+// In this case, no asynchronous work is being done, so no Suspense or fallback is required.
177+export default async function ExampleServerPage() {
178+ return <SomeClientComponent initialData={[]} />
179+}
180+```
181+
182+Example of a server component:
183+
184+```tsx
185+"use server"
186+
187+interface ExampleServerComponentProps {
188+ // Your props here
189+}
190+
191+export async function ExampleServerComponent({
192+ props
193+}: ExampleServerComponentProps) {
194+ // Your code here
195+}
196+```
197+
198+##### Client Components
199+
200+- Use `"use client"` at the top of the file
201+- Client components can safely rely on props passed down from server components, or handle UI interactions without needing <Suspense> if there's no async logic.
202+- Never use server actions in client components. If you need to create a new server action, create it in `/actions`
203+
204+Example of a client page:
205+
206+```tsx
207+"use client"
208+
209+export default function ExampleClientPage() {
210+ // Your code here
211+}
212+```
213+
214+Example of a client component:
215+
216+```tsx
217+"use client"
218+
219+interface ExampleClientComponentProps {
220+ initialData: any[]
221+}
222+
223+export default function ExampleClientComponent({
224+ initialData
225+}: ExampleClientComponentProps) {
226+ // Client-side logic here
227+ return <div>{initialData.length} items</div>
228+}
229+```
230+
231+### Backend Rules
232+
233+Follow these rules when working on the backend.
234+
235+It uses Postgres, Supabase, Drizzle ORM, and Server Actions.
236+
237+#### General Rules
238+
239+- Never generate migrations. You do not have to do anything in the `db/migrations` folder inluding migrations and metadata. Ignore it.
240+
241+#### Organization
242+
243+#### Schemas
244+
245+- When importing schemas, use `@/db/schema`
246+- Name files like `example-schema.ts`
247+- All schemas should go in `db/schema`
248+- Make sure to export the schema in `db/schema/index.ts`
249+- Make sure to add the schema to the `schema` object in `db/db.ts`
250+- If using a userId, always use `userId: text("user_id").notNull()`
251+- Always include createdAt and updatedAt columns in all tables
252+- Make sure to cascade delete when necessary
253+- Use enums for columns that have a limited set of possible values such as:
254+
255+```ts
256+import { pgEnum } from "drizzle-orm/pg-core"
257+
258+export const MEMBERSHIP: PgEnum<Membership> = pgEnum(
259+ "membership",
260+ MEMBERSHIP_VALUES
261+)
262+
263+membership: MEMBERSHIP("membership").notNull().default("free")
264+```
265+
266+Example of a schema:
267+
268+`db/schema/todos-schema.ts`
269+
270+```ts
271+import { boolean, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"
272+
273+export const todosTable = pgTable("todos", {
274+ id: uuid("id").defaultRandom().primaryKey(),
275+ userId: text("user_id").notNull(),
276+ content: text("content").notNull(),
277+ completed: boolean("completed").default(false).notNull(),
278+ createdAt: timestamp("created_at").defaultNow().notNull(),
279+ updatedAt: timestamp("updated_at")
280+ .defaultNow()
281+ .notNull()
282+ .$onUpdate(() => new Date())
283+})
284+
285+export type InsertTodo = typeof todosTable.$inferInsert
286+export type SelectTodo = typeof todosTable.$inferSelect
287+```
288+
289+And exporting it:
290+
291+`db/schema/index.ts`
292+
293+```ts
294+export * from "./todos-schema"
295+```
296+
297+And adding it to the schema in `db/db.ts`:
298+
299+`db/db.ts`
300+
301+```ts
302+import { todosTable } from "@/db/schema"
303+
304+const schema = {
305+ todos: todosTable
306+}
307+```
308+
309+And a more complex schema:
310+
311+```ts
312+import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"
313+
314+export const chatsTable = pgTable("chats", {
315+ id: uuid("id").defaultRandom().primaryKey(),
316+ userId: text("user_id").notNull(),
317+ name: text("name").notNull(),
318+ createdAt: timestamp("created_at").defaultNow().notNull(),
319+ updatedAt: timestamp("updated_at")
320+ .defaultNow()
321+ .notNull()
322+ .$onUpdate(() => new Date())
323+})
324+
325+export type InsertChat = typeof chatsTable.$inferInsert
326+export type SelectChat = typeof chatsTable.$inferSelect
327+```
328+
329+```ts
330+import { pgEnum, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"
331+import { chatsTable } from "./chats-schema"
332+
333+export const roleEnum = pgEnum("role", ["assistant", "user"])
334+
335+export const messagesTable = pgTable("messages", {
336+ id: uuid("id").defaultRandom().primaryKey(),
337+ chatId: uuid("chat_id")
338+ .references(() => chatsTable.id, { onDelete: "cascade" })
339+ .notNull(),
340+ content: text("content").notNull(),
341+ role: roleEnum("role").notNull(),
342+ createdAt: timestamp("created_at").defaultNow().notNull(),
343+ updatedAt: timestamp("updated_at")
344+ .defaultNow()
345+ .notNull()
346+ .$onUpdate(() => new Date())
347+})
348+
349+export type InsertMessage = typeof messagesTable.$inferInsert
350+export type SelectMessage = typeof messagesTable.$inferSelect
351+```
352+
353+And exporting it:
354+
355+`db/schema/index.ts`
356+
357+```ts
358+export * from "./chats-schema"
359+export * from "./messages-schema"
360+```
361+
362+And adding it to the schema in `db/db.ts`:
363+
364+`db/db.ts`
365+
366+```ts
367+import { chatsTable, messagesTable } from "@/db/schema"
368+
369+const schema = {
370+ chats: chatsTable,
371+ messages: messagesTable
372+}
373+```
374+
375+#### Server Actions
376+
377+- When importing actions, use `@/actions` or `@/actions/db` if db related
378+- DB related actions should go in the `actions/db` folder
379+- Other actions should go in the `actions` folder
380+- Name files like `example-actions.ts`
381+- All actions should go in the `actions` folder
382+- Only write the needed actions
383+- Return an ActionState with the needed data type from actions
384+- Include Action at the end of function names `Ex: exampleFunction -> exampleFunctionAction`
385+- Actions should return a Promise<ActionState<T>>
386+- Sort in CRUD order: Create, Read, Update, Delete
387+- Make sure to return undefined as the data type if the action is not supposed to return any data
388+- **Date Handling:** For columns defined as `PgDateString` (or any date string type), always convert JavaScript `Date` objects to ISO strings using `.toISOString()` before performing operations (e.g., comparisons or insertions). This ensures value type consistency and prevents type errors.
389+
390+```ts
391+export type ActionState<T> =
392+ | { isSuccess: true; message: string; data: T }
393+ | { isSuccess: false; message: string; data?: never }
394+```
395+
396+Example of an action:
397+
398+`actions/db/todos-actions.ts`
399+
400+```ts
401+"use server"
402+
403+import { db } from "@/db/db"
404+import { InsertTodo, SelectTodo, todosTable } from "@/db/schema/todos-schema"
405+import { ActionState } from "@/types"
406+import { eq } from "drizzle-orm"
407+
408+export async function createTodoAction(
409+ todo: InsertTodo
410+): Promise<ActionState<SelectTodo>> {
411+ try {
412+ const [newTodo] = await db.insert(todosTable).values(todo).returning()
413+ return {
414+ isSuccess: true,
415+ message: "Todo created successfully",
416+ data: newTodo
129417 }
130−
131− if (!trimmedEmail || !trimmedEmail.includes("@")) {
132− setErrors({ ...errors, email: "Valid email is required" })
133− toast({ title: "Error", description: "Valid email is required", variant: "destructive" })
134− return false
135− }
136−
137− if (!trimmedMessage || trimmedMessage.length < 10) {
138− setErrors({ ...errors, message: "Message must be at least 10 characters" })
139− toast({ title: "Error", description: "Message too short", variant: "destructive" })
140− return false
141− }
142−
143− return true
418+ } catch (error) {
419+ console.error("Error creating todo:", error)
420+ return { isSuccess: false, message: "Failed to create todo" }
144421 }
145−
146− const submitForm = async (): Promise<SubmissionResult> => {
147− const payload: FormPayload = {
148− name: formData.name.trim(),
149− email: formData.email.trim(),
150− message: formData.message.trim(),
151− timestamp: new Date().toISOString()
152− }
153−
154− const response: Response = await fetch("/api/contact", {
155− method: "POST",
156− headers: { "Content-Type": "application/json" },
157− body: JSON.stringify(payload)
422+}
423+
424+export async function getTodosAction(
425+ userId: string
426+): Promise<ActionState<SelectTodo[]>> {
427+ try {
428+ const todos = await db.query.todos.findMany({
429+ where: eq(todosTable.userId, userId)
158430 })
159−
160− if (!response.ok) {
161− throw new Error("Failed to submit")
431+ return {
432+ isSuccess: true,
433+ message: "Todos retrieved successfully",
434+ data: todos
162435 }
163−
164− return response.json()
436+ } catch (error) {
437+ console.error("Error getting todos:", error)
438+ return { isSuccess: false, message: "Failed to get todos" }
165439 }
166−
167− const handleSuccess = (result: SubmissionResult): void => {
168− setFormData({ name: "", email: "", message: "" })
169− setSubmissionCount((prev: number) => prev + 1)
170− toast({ title: "Success", description: "Message sent successfully!" })
171−
172− if (onSuccess) {
173− onSuccess(result)
440+}
441+
442+export async function updateTodoAction(
443+ id: string,
444+ data: Partial<InsertTodo>
445+): Promise<ActionState<SelectTodo>> {
446+ try {
447+ const [updatedTodo] = await db
448+ .update(todosTable)
449+ .set(data)
450+ .where(eq(todosTable.id, id))
451+ .returning()
452+
453+ return {
454+ isSuccess: true,
455+ message: "Todo updated successfully",
456+ data: updatedTodo
174457 }
458+ } catch (error) {
459+ console.error("Error updating todo:", error)
460+ return { isSuccess: false, message: "Failed to update todo" }
175461 }
176−
177− const handleError = (error: unknown): void => {
178− const errorMessage: string = error instanceof Error ? error.message : "Unknown error"
179− console.error("Submission error:", errorMessage)
180− setErrors({ submit: "Failed to send message" })
181− toast({ title: "Error", description: "Failed to send message", variant: "destructive" })
182− }
183−
184− const handleFormSubmit = async (): Promise<void> => {
185− const isValid: boolean = validateForm()
186−
187− if (!isValid) {
188− return
462+}
463+
464+export async function deleteTodoAction(id: string): Promise<ActionState<void>> {
465+ try {
466+ await db.delete(todosTable).where(eq(todosTable.id, id))
467+ return {
468+ isSuccess: true,
469+ message: "Todo deleted successfully",
470+ data: undefined
189471 }
190−
191− setIsSubmitting(true)
192− setErrors({})
193−
194− try {
195− const result: SubmissionResult = await submitForm()
196− handleSuccess(result)
197− } catch (error: unknown) {
198− handleError(error)
199− } finally {
200− setIsSubmitting(false)
201− }
472+ } catch (error) {
473+ console.error("Error deleting todo:", error)
474+ return { isSuccess: false, message: "Failed to delete todo" }
202475 }
203− ```
204−- Example with implicit return:
476+}
477+```
205478
206− ```tsx
207− interface MyComponentProps {
208− title: string
209− children: React.ReactNode
210− }
479+### Auth Rules
211480
212− const MyComponent = ({ title, children }: MyComponentProps) => (
213− <div>
214− {title}
215− {children}
216− </div>
217− )
481+Follow these rules when working on auth.
218482
219− export default MyComponent
220− ```
483+It uses Clerk for authentication.
221484
222−- Example with explicit return (when logic is present):
485+#### General Rules
223486
224− ```tsx
225− interface MyComponentProps {
226− title: string
227− children: React.ReactNode
228− }
487+- Import the auth helper with `import { auth } from "@clerk/nextjs/server"` in server components
488+- await the auth helper in server actions
229489
230− const MyComponent = ({ title, children }: MyComponentProps) => {
231− const processedTitle = title.toUpperCase()
232−
233− return (
234− <div>
235− {processedTitle}
236− {children}
237− </div>
238− )
490+### Payments Rules
491+
492+Follow these rules when working on payments.
493+
494+It uses Stripe for payments.
495+
496+### Analytics Rules
497+
498+Follow these rules when working on analytics.
499+
500+It uses PostHog for analytics.
501+
502+# Storage Rules
503+
504+Follow these rules when working with Supabase Storage.
505+
506+It uses Supabase Storage for file uploads, downloads, and management.
507+
508+## General Rules
509+
510+- Always use environment variables for bucket names to maintain consistency across environments
511+- Never hardcode bucket names in the application code
512+- Always handle file size limits and allowed file types at the application level
513+- Use the `upsert` method instead of `upload` when you want to replace existing files
514+- Always implement proper error handling for storage operations
515+- Use content-type headers when uploading files to ensure proper file handling
516+
517+## Organization
518+
519+### Buckets
520+
521+- Name buckets in kebab-case: `user-uploads`, `profile-images`
522+- Create separate buckets for different types of files (e.g., `profile-images`, `documents`, `attachments`)
523+- Document bucket purposes in a central location
524+- Set appropriate bucket policies (public/private) based on access requirements
525+- Implement RLS (Row Level Security) policies for buckets that need user-specific access
526+- Make sure to let me know instructions for setting up RLS policies on Supabase since you can't do this yourself, including the SQL scripts I need to run in the editor
527+
528+### File Structure
529+
530+- Organize files in folders based on their purpose and ownership
531+- Use predictable, collision-resistant naming patterns
532+- Structure: `{bucket}/{userId}/{purpose}/{filename}`
533+- Example: `profile-images/123e4567-e89b/avatar/profile.jpg`
534+- Include timestamps in filenames when version history is important
535+- Example: `documents/123e4567-e89b/contracts/2024-02-13-contract.pdf`
536+
537+## Actions
538+
539+- When importing storage actions, use `@/actions/storage`
540+- Name files like `example-storage-actions.ts`
541+- Include Storage at the end of function names `Ex: uploadFile -> uploadFileStorage`
542+- Follow the same ActionState pattern as DB actions
543+
544+Example of a storage action:
545+
546+```ts
547+"use server"
548+
549+import { createClientComponentClient } from "@supabase/auth-helpers-nextjs"
550+import { ActionState } from "@/types"
551+
552+export async function uploadFileStorage(
553+ bucket: string,
554+ path: string,
555+ file: File
556+): Promise<ActionState<{ path: string }>> {
557+ try {
558+ const supabase = createClientComponentClient()
559+
560+ const { data, error } = await supabase.storage
561+ .from(bucket)
562+ .upload(path, file, {
563+ upsert: false,
564+ contentType: file.type
565+ })
566+
567+ if (error) throw error
568+
569+ return {
570+ isSuccess: true,
571+ message: "File uploaded successfully",
572+ data: { path: data.path }
573+ }
574+ } catch (error) {
575+ console.error("Error uploading file:", error)
576+ return { isSuccess: false, message: "Failed to upload file" }
239577 }
578+}
579+```
240580
241− export default MyComponent
242− ```
581+## File Handling
243582
244−- Normal components that are not Next.js pages/layouts should be exported
245− using export const pattern
246−- Example with implicit return:
583+### Upload Rules
247584
248− ```tsx
249− interface NotAPageOrLayoutComponentProps {
250− title: string
251− children: React.ReactNode
585+- Always validate file size before upload
586+- Implement file type validation using both extension and MIME type
587+- Generate unique filenames to prevent collisions
588+- Set appropriate content-type headers
589+- Handle existing files appropriately (error or upsert)
590+
591+Example validation:
592+
593+```ts
594+const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB
595+const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"]
596+
597+function validateFile(file: File): boolean {
598+ if (file.size > MAX_FILE_SIZE) {
599+ throw new Error("File size exceeds limit")
252600 }
253601
254− export const NotAPageOrLayoutComponent = ({
255− title,
256− children
257− }: NotAPageOrLayoutComponentProps) => (
258− <div>
259− {title}
260− {children}
261− </div>
262− )
602+ if (!ALLOWED_TYPES.includes(file.type)) {
603+ throw new Error("File type not allowed")
604+ }
263605
264−E. Component Granularity & Organization
265−- Break down large components into smaller, focused components for better maintainability.
266−- When a component contains multiple logical sections (e.g., Card with CardHeader + CardContent), extract each section into separate components.
267−- Create dedicated folders for related component groups:
268− * Use kebab-case folder names matching the main component concept
269− * Place related sub-components within the same folder using kebab-case file names
270− * Example structure: `component-name/component-name-header.tsx`, `component-name/component-name-content.tsx`
271−- Each sub-component should have a single, clear responsibility.
272−- Maintain the parent component as a composition wrapper that orchestrates child components.
273−- Follow this pattern when refactoring existing components or creating new feature components.
606+ return true
607+}
608+```
274609
275−F. Advanced Component Architecture Patterns
610+### Download Rules
276611
277−F.1. Pure Functions and Constants Organization
278−- **Pure functions** (no side effects, deterministic output) must be extracted outside components:
279− * Place above the component definition
280− * Examples: `getGreeting()`, `getMembershipBadgeColor()`, `formatDate()`
281−- **Constants and static data** must be moved outside components:
282− * Place after imports and interfaces, before pure functions
283− * Use SCREAMING_SNAKE_CASE for naming (e.g., `TEMPLATE_FEATURES`, `TECH_STACK`)
284− * **Always explicitly type constants** with appropriate type annotations
285− * Examples: `const API_URL: string = "..."`, `const MAX_RETRIES: number = 3`
286− * Use `as const` for immutable values when type inference is sufficient
287− * Group related constants together
612+- Always handle missing files gracefully
613+- Implement proper error handling for failed downloads
614+- Use signed URLs for private files
288615
289−F.1.1. Custom Hooks Organization
290−- **Custom hooks** must be extracted to separate files in the `/hooks/` directory:
291− * Use kebab-case file naming: `use-scroll-detection.ts`, `use-local-storage.ts`
292− * Start hook names with `use` prefix following React conventions
293− * Place hooks in `/hooks/` folder at project root level
294− * Export hooks using named exports: `export const useScrollDetection = () => {}`
295− * **Always explicitly type hook return values** and parameters
296− * Examples: `useScrollDetection(): boolean`, `useLocalStorage<T>(key: string): [T, (value: T) => void]`
297− * Group related hooks in the same file only if they're tightly coupled
616+### Delete Rules
298617
299−F.1.2. Whitespace and Formatting Rules
300−- **Component variable organization**: Maintain consistent whitespace between different types of declarations:
301− * Add a blank line between React state declarations and custom hook calls
302− * Add a blank line between custom hook calls and other variable declarations
303− * Example:
304− ```tsx
305− const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false)
306− const [isVisible, setIsVisible] = useState<boolean>(true)
307−
308− const isScrolled: boolean = useScrollDetection()
309− const userData: UserData = useUserData()
310−
311− const processedData: ProcessedData = processUserData(userData)
312− ```
618+- Implement soft deletes when appropriate
619+- Clean up related database records when deleting files
620+- Handle bulk deletions carefully
621+- Verify ownership before deletion
622+- Always delete all versions/transforms of a file
313623
314−F.2. Nested Component Structure for Complex Components
315−When a component has multiple distinct sections, create nested folder structure:
624+## Security
316625
626+### Bucket Policies
627+
628+- Make buckets private by default
629+- Only make buckets public when absolutely necessary
630+- Use RLS policies to restrict access to authorized users
631+- Example RLS policy:
632+
633+```sql
634+CREATE POLICY "Users can only access their own files"
635+ON storage.objects
636+FOR ALL
637+USING (auth.uid()::text = (storage.foldername(name))[1]);
317638 ```
318−dashboard-welcome/
319−├── dashboard-welcome.tsx // Main orchestrator component
320−├── greeting.tsx // Self-contained greeting section
321−├── whats-included/ // Folder for multi-part section
322−│ ├── whats-included.tsx // Section orchestrator
323−│ ├── whats-included-title.tsx // Title sub-component
324−│ └── whats-included-features.tsx // Features list sub-component
325−├── core-technologies/ // Folder for multi-part section
326−│ ├── core-technologies.tsx // Section orchestrator
327−│ ├── core-technologies-title.tsx // Title sub-component
328−│ └── core-technologies-list.tsx // Tech list sub-component
329−└── get-started/ // Folder for multi-part section
330− ├── get-started.tsx // Section orchestrator
331− ├── get-started-title.tsx // Title sub-component
332− ├── get-started-features.tsx // Features grid sub-component
333− └── get-started-feature-2.tsx // Individual feature card
334−```
335639
336−F.3. Component Organization Rules
337−1. **Main orchestrator**: Composition only, minimal logic, imports and renders sub-components
338−2. **Section orchestrators**: Handle section-specific logic, render related sub-components
339−3. **Leaf components**: Single responsibility, pure presentation, accept props only
340−4. **Shared constants**: Extract to file level, use proper naming conventions
341−5. **Pure functions**: Extract above component definitions, properly typed
342−6. **File structure**: Mirror logical component hierarchy in folder structure
640+### Access Control
343641
344−</coding_format>
642+- Generate short-lived signed URLs for private files
643+- Implement proper CORS policies
644+- Use separate buckets for public and private files
645+- Never expose internal file paths
646+- Validate user permissions before any operation
345647
346−<usage_guidelines>
648+## Error Handling
347649
348−1. Apply these standards to all new code and when refactoring existing code.
349−2. When making any code changes, ensure they conform to these guidelines.
350−3. If existing code doesn't follow these standards, update it to comply when modifying those files.
351−4. Use these standards as a checklist when reviewing code changes.
352−5. Prefer extracting helper functions over writing long, complex functions.
353−6. Always prioritize code readability and maintainability.
650+- Implement specific error types for common storage issues
651+- Always provide meaningful error messages
652+- Implement retry logic for transient failures
653+- Log storage errors separately for monitoring
354654
355−</usage_guidelines>
655+## Optimization
656+
657+- Implement progressive upload for large files
658+- Clean up temporary files and failed uploads
659+- Use batch operations when handling multiple files
660+
