Backend Rules
DatabasePostgresBackendSupabaseORM
Description
Follow these rules when working on the backend.
Globs
**/*
---
description: Follow these rules when working on the backend.
globs: **/*
---
# Backend Rules
Follow these rules when working on the backend.
It uses Postgres, Supabase, Drizzle ORM, and Server Actions.
## General Rules
- Never generate migrations. You do not have to do anything in the `db/migrations` folder inluding migrations and metadata. Ignore it.
## Organization
## Schemas
- When importing schemas, use `@/db/schema`
- Name files like `example-schema.ts`
- All schemas should go in `db/schema`
- Make sure to export the schema in `db/schema/index.ts`
- Make sure to add the schema to the `schema` object in `db/db.ts`
- 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:
`db/schema/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 exporting it:
`db/schema/index.ts`
```ts
export * from "./todos-schema"
```
And adding it to the schema in `db/db.ts`:
`db/db.ts`
```ts
import { todosTable } from "@/db/schema"
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 "./chats-schema"
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 exporting it:
`db/schema/index.ts`
```ts
export * from "./chats-schema"
export * from "./messages-schema"
```
And adding it to the schema in `db/db.ts`:
`db/db.ts`
```ts
import { chatsTable, messagesTable } from "@/db/schema"
const schema = {
chats: chatsTable,
messages: messagesTable
}
```
## Server Actions
- When importing actions, use `@/actions` or `@/actions/db` if db related
- DB related actions should go in the `actions/db` folder
- Other actions should go in the `actions` folder
- Name files like `example-actions.ts`
- All actions should go in the `actions` folder
- 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
- **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.
```ts
export type ActionState =
| { isSuccess: true; message: string; data: T }
| { isSuccess: false; message: string; data?: never }
```
Example of an action:
`actions/db/todos-actions.ts`
```ts
"use server"
import { db } from "@/db/db"
import { InsertTodo, SelectTodo, todosTable } from "@/db/schema/todos-schema"
import { ActionState } from "@/types"
import { eq } from "drizzle-orm"
export async function createTodoAction(
todo: InsertTodo
): Promise<ActionState> {
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> {
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
): Promise<ActionState> {
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> {
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" }
}
}
```