# Project Instructions
Use specification and guidelines as you build the app.
Write the complete code for every step. Do not get lazy.
Your goal is to completely finish whatever I ask for.
You will see <ai_context> tags in the code. These are context tags that you should use to help you understand the codebase.
## Overview
This is a web app template.
## Tech Stack
- Frontend: Next.js, Tailwind, Shadcn, Framer Motion
- Backend: Server Actions, Postgres, Drizzle ORM
- Auth: Clerk
- Payments: Stripe
- Analytics: Umami
- Deployment: Vercel
- Tooling: pnpm
# Project Structure
- `app` - Next.js app router
- `api` - API routes
- `route` - An example route
- `layout.tsx` - Layout for the route
- `page.tsx` - Page for the route
- `modules` - Modules of the app. A module is a group of code taking care on one specific thing
- `module_name` - Example module (e.g. `auth`, `todos`, `chats`, `cache`, `leads`, `seo`, etc)
- `client` - Code that runs only the client
- `server` - Code that runs only on the server
- `.` - Root of the module has isomorphic code, that can run either on the client or server
- `submodule_name` - Example of recursively nested module
- `shared` - Shared code that is used across different modules or pages of the app.
- `components` - Shared components
- `hooks` - Shared hooks
- `constants` - Shared constants
- `types` - Shared types
- `utils` - Shared utilities
- `prompts` - Utils prompts for LLMs
- `middleware.ts` - Next.js middleware
## Rules
Follow these rules when building the app.
### General Rules
- Use `@` to import anything from the app unless otherwise specified
- Use kebab case for all files and folders unless otherwise specified
- Don't update shadcn components unless otherwise specified
#### Env Rules
- If you update environment variables, update the `.env.example` file
- All environment variables should go in `.env.local`
- Do not expose environment variables to the frontend
- Use `NEXT_PUBLIC_` prefix for environment variables that need to be accessed from the frontend
- You may import environment variables in server actions and components by using `process.env.VARIABLE_NAME`
#### Type Rules
Follow these rules when working with types.
- Name files like `example-types.ts`
- Prefer interfaces over type aliases
An example of a type:
`modules/shared/types/actions-types.ts`
```ts
export type ActionState<T> =
| { isSuccess: true; message: string; data: T }
| { isSuccess: false; message: string; data?: never }
```
### Frontend Rules
Follow these rules when working on the frontend.
It uses Next.js, Tailwind, Shadcn, and Framer Motion.
#### Components
- Use divs instead of other html tags unless otherwise specified
- Separate the main parts of a component's html with an extra blank line for visual spacing
- Always tag a component with either `use server` or `use client` at the top, including layouts and pages
##### Organization
- All components be named using kebab case like `example-component.tsx` unless otherwise specified
- Put components in `/modules/module_name` for module specific components
- Put components in `/shared/components` if shared components
##### Data Fetching
- Fetch data in server components and pass the data down as props to client components.
- Use server actions to mutate data.
##### Server Components
- Implement Suspense for asynchronous data fetching to show loading states while data is being fetched.
- 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.
- If asynchronous fetching is required, you can use a `<Suspense>` boundary and a fallback to indicate a loading state while data is loading.
- Server components cannot be imported into client components. If you want to use a server component in a client component, you must pass the server component as props using the "children" prop
Example of a server layout:
```tsx
export default async function ExampleServerLayout({
children
}: {
children: React.ReactNode
}) {
return children
}
```
Example of a server page (with async logic):
```tsx
import { Suspense } from "react"
import { SomeAction } from "@/modules/module_name/server/some-actions.tsx"
import SomeComponent from "@/modules/module_name/some-component.tsx"
import SomeSkeleton from "@/modules/module_name/some-skeleton.tsx"
export default async function ExampleServerPage() {
return (
<Suspense fallback={<SomeSkeleton className="some-class" />}>
<SomeComponentFetcher />
</Suspense>
)
}
async function SomeComponentFetcher() {
const { data } = await SomeAction()
return <SomeComponent className="some-class" initialData={data || []} />
}
```
Example of a server page (no async logic required):
```tsx
import SomeClientComponent from "@/modules/module_name/client/some-client-component.tsx"
// In this case, no asynchronous work is being done, so no Suspense or fallback is required.
export default async function ExampleServerPage() {
return <SomeClientComponent initialData={[]} />
}
```
Example of a server component:
```tsx
interface ExampleServerComponentProps {
// Your props here
}
export async function ExampleServerComponent({
props
}: ExampleServerComponentProps) {
// Your code here
}
```
##### Client Components
- Use `"use client"` at the top of the file
- 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.
Example of a client page:
```tsx
"use client"
export default function ExampleClientPage() {
// Your code here
}
```
Example of a client component:
```tsx
"use client"
interface ExampleClientComponentProps {
initialData: any[]
}
export default function ExampleClientComponent({
initialData
}: ExampleClientComponentProps) {
// Client-side logic here
return <div>{initialData.length} items</div>
}
```
### Backend Rules
Follow these rules when working on the backend.
It uses Postgres, Drizzle ORM and Server Actions.
#### General Rules
- Never generate migrations
#### Organization
#### Schemas
- Name files like `example-schema.ts`
- All schemas should go in `@/modules/db/`
- If using a userId, always use `userId: text("user_id").notNull()`
- Always include createdAt and updatedAt columns in all tables
- Make sure to cascade delete when necessary
- Use enums for columns that have a limited set of possible values such as:
```ts
import { pgEnum } from "drizzle-orm/pg-core"
export const membershipEnum = pgEnum("membership", ["free", "pro"])
membership: membershipEnum("membership").notNull().default("free")
```
Example of a schema:
`modules/db/todos/schema.ts`
```ts
import { boolean, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"
export const todosTable = pgTable("todos", {
id: uuid("id").defaultRandom().primaryKey(),
userId: text("user_id").notNull(),
content: text("content").notNull(),
completed: boolean("completed").default(false).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.notNull()
.$onUpdate(() => new Date())
})
export type InsertTodo = typeof todosTable.$inferInsert
export type SelectTodo = typeof todosTable.$inferSelect
```
And adding it to the schema in `db/db.ts`:
`modules/db/db.ts`
```ts
import { todosTable } from "@/modules/db/todos/schema.ts"
const schema = {
todos: todosTable
}
```
And a more complex schema:
```ts
import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"
export const chatsTable = pgTable("chats", {
id: uuid("id").defaultRandom().primaryKey(),
userId: text("user_id").notNull(),
name: text("name").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.notNull()
.$onUpdate(() => new Date())
})
export type InsertChat = typeof chatsTable.$inferInsert
export type SelectChat = typeof chatsTable.$inferSelect
```
```ts
import { pgEnum, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"
import { chatsTable } from "@/modules/db/chats/schema.ts"
export const roleEnum = pgEnum("role", ["assistant", "user"])
export const messagesTable = pgTable("messages", {
id: uuid("id").defaultRandom().primaryKey(),
chatId: uuid("chat_id")
.references(() => chatsTable.id, { onDelete: "cascade" })
.notNull(),
content: text("content").notNull(),
role: roleEnum("role").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.notNull()
.$onUpdate(() => new Date())
})
export type InsertMessage = typeof messagesTable.$inferInsert
export type SelectMessage = typeof messagesTable.$inferSelect
```
And adding it to the schema in `modules/db/db.ts`:
`modules/db/db.ts`
```ts
import { chatsTable } from "@/modules/db/chats/schema.ts"
import { messagesTable } from "@/modules/db/messages/schema.ts"
const schema = {
chats: chatsTable,
messages: messagesTable
}
```
#### Server Actions
- When importing actions, use `@/modules/module_name/actions`
- DB related actions should go in the `@/modules/db/server` folder
- Other actions should go in the `@/modules/shared/server/actions` folder
- Name files like `example-actions.ts`
- Only write the needed actions
- Return an ActionState with the needed data type from actions
- Include Action at the end of function names `Ex: exampleFunction -> exampleFunctionAction`
- Actions should return a Promise<ActionState<T>>
- Sort in CRUD order: Create, Read, Update, Delete
- Make sure to return undefined as the data type if the action is not supposed to return any data
```ts
export type ActionState<T> =
| { isSuccess: true; message: string; data: T }
| { isSuccess: false; message: string; data?: never }
```
Example of an action:
`modules/db/todos/server/actions.ts`
```ts
"use server"
import { db } from "@/modules/db/db"
import { InsertTodo, SelectTodo, todosTable } from "@/modules/db/todos/schema.ts"
import { ActionState } from "@/modules/shared/types/actions-types.ts"
import { eq } from "drizzle-orm"
export async function createTodoAction(
todo: InsertTodo
): Promise<ActionState<SelectTodo>> {
try {
const [newTodo] = await db.insert(todosTable).values(todo).returning()
return {
isSuccess: true,
message: "Todo created successfully",
data: newTodo
}
} catch (error) {
console.error("Error creating todo:", error)
return { isSuccess: false, message: "Failed to create todo" }
}
}
export async function getTodosAction(
userId: string
): Promise<ActionState<SelectTodo[]>> {
try {
const todos = await db.query.todos.findMany({
where: eq(todosTable.userId, userId)
})
return {
isSuccess: true,
message: "Todos retrieved successfully",
data: todos
}
} catch (error) {
console.error("Error getting todos:", error)
return { isSuccess: false, message: "Failed to get todos" }
}
}
export async function updateTodoAction(
id: string,
data: Partial<InsertTodo>
): Promise<ActionState<SelectTodo>> {
try {
const [updatedTodo] = await db
.update(todosTable)
.set(data)
.where(eq(todosTable.id, id))
.returning()
return {
isSuccess: true,
message: "Todo updated successfully",
data: updatedTodo
}
} catch (error) {
console.error("Error updating todo:", error)
return { isSuccess: false, message: "Failed to update todo" }
}
}
export async function deleteTodoAction(id: string): Promise<ActionState<void>> {
try {
await db.delete(todosTable).where(eq(todosTable.id, id))
return {
isSuccess: true,
message: "Todo deleted successfully",
data: undefined
}
} catch (error) {
console.error("Error deleting todo:", error)
return { isSuccess: false, message: "Failed to delete todo" }
}
}
```
### Auth Rules
Follow these rules when working on auth.
It uses Clerk for authentication.
#### General Rules
- Import the auth helper with `import { auth } from "@clerk/nextjs/server"` in server components
- await the auth helper in server actions
### Payments Rules
Follow these rules when working on payments.
It uses Stripe for payments.
### Analytics Rules
Follow these rules when working on analytics.
It uses Umami for analytics.
analytics
clerk
css
drizzle-orm
golang
javascript
less
mdx
+11 more
First Time Repository
TypeScript
Languages:
CSS: 1.8KB
JavaScript: 0.5KB
MDX: 5.3KB
TypeScript: 54.3KB
Created: 1/21/2025
Updated: 1/22/2025