| Dimension | Shared | Only in A | Only in B | Overlap |
|---|---|---|---|---|
| Sections | 6 | 0 | 19 | 24% |
| Commands | 0 | 0 | 0 | — |
| Section tags | 5 | 0 | 0 | 100% |
What each file covers
Sections
6 shared · 0 only in A · 19 only in B- + 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
- Project Instructions
- Overview
- Tech Stack
- Project Structure
- Rules
- General Rules
Commands
neither file has anySection tags
5 shared · 0 only in A · 0 only in B- code-style
- architecture
- security
- do-not
- agent-behaviour
Line diff
sportiz91/vibe-template · .cursor/rules/general.mdc
@@ −1 @@
1---
2description: Follow these rules for all requests.
3globs:
4alwaysApply: false
5---
6# Project Instructions
7# Project Instructions
8
9Use specification and guidelines as you build the app.
10
@@ −97 @@
97export * from "./actions-types"
98```
99
100- Always access environment variables via the centralized config module (`serverConfig` and `publicEnv` from `@/lib/config`). Do not use `process.env` directly in application code.
101
102
sportiz91/vibe-template · .cursorrules
@@ +1 @@
1# Project Instructions
2
3Use specification and guidelines as you build the app.
4
@@ +91 @@
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: Follow these rules for all requests.
3−globs:
4−alwaysApply: false
5−---
61 # Project Instructions
7−# Project Instructions
82
93 Use specification and guidelines as you build the app.
104
@@ −97 +91 @@
9791 export * from "./actions-types"
9892 ```
9993
100−- Always access environment variables via the centralized config module (`serverConfig` and `publicEnv` from `@/lib/config`). Do not use `process.env` directly in application code.
94+### Frontend Rules
10195
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
417+ }
418+ } catch (error) {
419+ console.error("Error creating todo:", error)
420+ return { isSuccess: false, message: "Failed to create todo" }
421+ }
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)
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+
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
457+ }
458+ } catch (error) {
459+ console.error("Error updating todo:", error)
460+ return { isSuccess: false, message: "Failed to update todo" }
461+ }
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
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+
481+Follow these rules when working on auth.
482+
483+It 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+
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" }
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+
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")
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
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]);
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
102660
