Imported from nikitalbnv/aether (
apps/web/AGENTS.md). Install upstream withnpx skills add nikitalbnv/aether --skill web. Copyright stays with the author.
Agent Guidelines for Aether Codebase
Git Workflow
IMPORTANT: Make encapsulated git commits after completing each logical task or todo item.
- Commit early and often - one commit per completed task/feature
- Use conventional commit format:
type: descriptionfeat:- new featurefix:- bug fixstyle:- styling/CSS changesdocs:- documentationrefactor:- code refactoringchore:- maintenance tasks
- Keep commits atomic and focused on a single change
- Write clear commit messages that explain the "why"
Example workflow:
# After completing metadata update
git add src/app/layout.tsx
git commit -m "feat: update app metadata with SEO and branding"
# After completing styling changes
git add src/styles/globals.css
git commit -m "style: update color scheme to brand colors"
Build/Lint/Test Commands
# Development
bun run dev # Start dev server (auto-starts Docker DB)
bun run dev:next # Start dev server without DB check
bun run build # Production build
bun run preview # Build and start production server
# Code Quality
bun run lint # Check for linting errors
bun run lint:fix # Auto-fix linting errors
bun run typecheck # Run TypeScript type checking
bun run format:check # Check Prettier formatting
bun run format:write # Auto-format all files
bun run check # Run lint + typecheck together
# Testing
bun run test # Run tests in watch mode
bun run test:run # Run tests once
bun run test:coverage # Run tests with coverage report
bun run ci # Run all checks (lint, typecheck, tests)
# Database (see "Database Migrations" section below)
bun run db:generate # Generate migration files from schema changes
bun run db:migrate # Run pending migrations (PRODUCTION)
bun run db:push # Push schema directly (LOCAL DEV ONLY)
bun run db:studio # Open Drizzle Studio GUI
bun run db:start # Start Docker PostgreSQL container
bun run db:stop # Stop Docker PostgreSQL container
Testing
We use Vitest with React Testing Library for testing. Tests are co-located with source files using .test.ts or .test.tsx extension.
Writing Tests
// src/lib/utils.test.ts
import { describe, expect, it } from "vitest";
import { cn } from "~/lib/utils";
describe("cn utility", () => {
it("should merge class names", () => {
expect(cn("foo", "bar")).toBe("foo bar");
});
});
Test Location
- Place test files next to the code they test:
component.tsx->component.test.tsx - Test setup and mocks are in
src/test/setup.ts
Running Tests
bun run test # Watch mode (interactive)
bun run test:run # Single run (CI)
bun run test:coverage # With coverage report
CI/CD & Git Hooks
Pre-commit Hook
- Runs
format:checkto ensure code is formatted
Pre-push Hook
- Runs
bun run check(lint + typecheck) - Runs
bun run test:run(all tests) - Push is blocked if any check fails
GitHub Actions
CI Workflow (.github/workflows/ci.yml):
- Runs on push to main and PRs
- Lint, typecheck, format check, tests
- Build verification
Security Workflow (.github/workflows/security.yml):
- CodeQL analysis for vulnerabilities
- Secret scanning with TruffleHog
- Runs weekly and on push/PR
Database Migrations
IMPORTANT: Always use migrations for production. Never use db:push on production databases.
Workflow for Schema Changes
- Modify schema in
src/server/db/schema.ts - Generate migration:
bun run db:generate - Review migration in
drizzle/folder (check the SQL is correct) - Test locally:
bun run db:migrate(with local DATABASE_URL) - Commit migration files along with schema changes
- Deploy: Run
bun run db:migrateon production
Migration Commands
# Generate new migration from schema diff
bun run db:generate
# Apply pending migrations to database
bun run db:migrate
# View migration status
bunx drizzle-kit status
# Drop all tables and re-run migrations (DANGEROUS - dev only)
bunx drizzle-kit drop
Local Development
For rapid iteration during local development, you can use db:push which directly syncs schema without creating migration files. However, always use migrations before committing:
# Quick local sync (no migration file)
bun run db:push
# Before committing, generate proper migration
bun run db:generate
Migration Files
Migrations are stored in drizzle/ directory:
0000_initial_migration.sql- First migration0001_add_feature.sql- Subsequent migrationsmeta/- Drizzle metadata (do not edit manually)
Tech Stack
| Layer | Technology |
|---|---|
| Framework | Next.js 15 (App Router, Turbopack) |
| Language | TypeScript 5.8 (strict mode) |
| Database | PostgreSQL (Neon prod, Docker dev) |
| ORM | Drizzle ORM |
| Auth | Better Auth + Google OAuth |
| API | tRPC v11 + React Query |
| Styling | Tailwind CSS v4 + shadcn/ui |
| AI | Hugging Face Inference API |
| Payments | Stripe |
| Runtime | Bun |
Project Structure
src/
├── app/ # Next.js App Router
│ ├── (auth)/ # Auth routes (login page at /)
│ ├── (dashboard)/ # Protected routes (boards, goals, calendar, settings)
│ └── api/ # API routes (tRPC, auth, webhooks)
├── components/
│ ├── ui/ # shadcn/ui primitives (button, card, dialog, etc.)
│ ├── auth/ # Auth-related components
│ ├── boards/ # Kanban board components
│ ├── dashboard/ # Dashboard components
│ ├── layout/ # Sidebar, navigation
│ └── tasks/ # Task-related components
├── server/
│ ├── api/ # tRPC routers and procedures
│ │ └── routers/ # Feature-specific routers (task, board, goal, etc.)
│ ├── db/ # Drizzle schema, client, migrations
│ └── better-auth/ # Auth configuration
├── lib/ # Utilities (cn, date helpers, etc.)
├── hooks/ # Custom React hooks
└── trpc/ # tRPC client setup (react.tsx, server.ts)
Code Style Guidelines
Imports
// 1. External libraries (React, Next.js, etc.)
import { useState } from "react";
import Link from "next/link";
// 2. Internal modules using ~/ alias
import { api } from "~/trpc/server";
import { Button } from "~/components/ui/button";
import { task } from "~/server/db/schema";
// 3. Types (use inline type imports)
import { type AppRouter } from "~/server/api/root";
- Always use
~/path alias for src imports - Use inline type imports:
import { type Foo }notimport type { Foo } - No relative imports outside current directory
TypeScript
- Strict mode enabled with
noUncheckedIndexedAccess - Use Zod for runtime validation (especially in tRPC inputs)
- Infer types from schema:
type Task = typeof task.$inferSelect - Prefix unused variables with underscore:
_unusedVar - Explicit return types for public functions
React Components
// Server Component (default) - no directive needed
export async function Dashboard() {
const data = await api.task.getAll();
return <div>{/* ... */}</div>;
}
// Client Component - add directive
"use client";
interface TaskCardProps {
task: Task;
onUpdate: (id: string) => void;
}
export function TaskCard({ task, onUpdate }: TaskCardProps) {
const [isOpen, setIsOpen] = useState(false);
// ...
}
- Server Components by default,
"use client"only when needed - Props interfaces, not inline types
- Destructure props in function signature
- PascalCase for components, kebab-case for files
Database & tRPC
// Schema: snake_case for DB columns
export const task = pgTable("aether_task", {
id: uuid("id").primaryKey().defaultRandom(),
userId: text("user_id").notNull(),
createdAt: timestamp("created_at").$defaultFn(() => new Date()),
});
// tRPC Router
export const taskRouter = createTRPCRouter({
getAll: protectedProcedure
.input(z.object({ boardId: z.string().uuid().optional() }))
.query(async ({ ctx, input }) => {
return ctx.db.query.task.findMany({
where: eq(task.userId, ctx.session.user.id),
});
}),
});
- Table names:
aether_prefix for app tables - Always filter by
userIdfor user-owned data - Use Drizzle's relational queries with
with:for joins - Validate all inputs with Zod schemas
Styling
import { cn } from "~/lib/utils";
// Conditional classes with cn()
<div className={cn(
"rounded-lg border p-4",
isActive && "bg-primary text-primary-foreground",
variant === "destructive" && "border-destructive"
)} />
- Use
cn()utility for conditional classes - Tailwind CSS v4 with design system tokens
- shadcn/ui components from
~/components/ui/ - Reference: https://ui.shadcn.com/
Error Handling
// tRPC mutations with optimistic updates
const utils = api.useUtils();
const mutation = api.task.update.useMutation({
onMutate: async (newData) => {
await utils.task.getAll.cancel();
const previous = utils.task.getAll.getData();
utils.task.getAll.setData(undefined, (old) => /* optimistic update */);
return { previous };
},
onError: (err, newData, context) => {
utils.task.getAll.setData(undefined, context?.previous);
},
onSettled: () => {
utils.task.getAll.invalidate();
},
});
- Use tRPC error codes for API errors
- Implement optimistic updates for mutations
- Revert on error with previous data
- Invalidate queries on settlement
Drizzle Safety Rules
The ESLint config enforces:
drizzle/enforce-delete-with-where- All deletes must have WHERE clausedrizzle/enforce-update-with-where- All updates must have WHERE clause
// Correct
await db.delete(task).where(eq(task.id, id));
// Will error - no WHERE clause
await db.delete(task);
Environment Variables
Required in .env:
DATABASE_URL="postgresql://..." # PostgreSQL connection string
BETTER_AUTH_SECRET="..." # Auth secret (min 32 chars)
GOOGLE_CLIENT_ID="..." # Google OAuth
GOOGLE_CLIENT_SECRET="..." # Google OAuth
Optional:
BETTER_AUTH_URL="http://localhost:3000"
HUGGINGFACE_API_KEY="..." # AI features
STRIPE_SECRET_KEY="..." # Payments
Common Patterns
Adding a New Feature
- Schema: Add table in
src/server/db/schema.ts - Router: Create router in
src/server/api/routers/ - Register: Add to
src/server/api/root.ts - Components: Build UI in
src/components/ - Page: Add route in
src/app/(dashboard)/
Auth Check
// Server Component
import { getSession } from "~/server/better-auth/server";
const session = await getSession();
if (!session) redirect("/");
// tRPC - use protectedProcedure (auto-checks auth)
export const myRouter = createTRPCRouter({
myProcedure: protectedProcedure.query(({ ctx }) => {
// ctx.session.user is available
}),
});