Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
Tradeoff: These guidelines bias toward caution over speed. For trivial tasks, use judgment.
Don't assume. Don't hide confusion. Surface tradeoffs.
Before implementing:
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them - don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.
Minimum code that solves the problem. Nothing speculative.
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
Touch only what you must. Clean up only your own mess.
When editing existing code:
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it - don't delete it.
When your changes create orphans:
- Remove imports/variables/functions that YOUR changes made unused.
- Don't remove pre-existing dead code unless asked.
The test: Every changed line should trace directly to the user's request.
Define success criteria. Loop until verified.
Transform tasks into verifiable goals:
- "Add validation" → "Write tests for invalid inputs, then make them pass"
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
- "Refactor X" → "Ensure tests pass before and after"
For multi-step tasks, state a brief plan:
1. [Step] → verify: [check]
2. [Step] → verify: [check]
3. [Step] → verify: [check]
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
These guidelines are working if: fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
Hackra is a hackathon platform built with Next.js 16, React 19, TypeScript, Prisma 7 (PostgreSQL), Better Auth, Tailwind CSS 4, and shadcn/ui. Package manager: Bun.
⚠️ IMPORTANTE: Antes de hacer cualquier cambio de diseño, lee@DESIGN.mdpara entender el sistema de diseño de Hackra (fuentes pixel, bordes sharp, efectos glow, etc.).
- GEMINI.md: Contains the same guidelines as this file — ver GEMINI.md
- CLAUDE.md: Version for Claude Code with
.claude/skillspaths — ver CLAUDE.md - DESIGN.md: Complete design system documentation — ver DESIGN.md
¿Cuándo usar skills? Las siguientes skills están registradas y disponibles. Carga la skill relevante ANTES de escribir código en esos dominios.
| Skill | When to Load | Location |
|---|---|---|
architect-nextjs |
Setting up new features, deciding component placement | .agents/skills/architect-nextjs/SKILL.md |
ultracite |
Writing any TS/TSX code, linting, formatting | .agents/skills/ultracite/SKILL.md |
next-cache-components |
Using Next.js 16 caching (PPR, use cache, cacheLife, cacheTag) |
.agents/skills/next-cache-components/SKILL.md |
next-best-practices |
Writing Next.js pages, layouts, API routes, metadata | .agents/skills/next-best-practices/SKILL.md |
shadcn |
Adding, fixing, or composing UI components | .agents/skills/shadcn/SKILL.md |
web-design-guidelines |
Reviewing UI code for accessibility/design compliance | .agents/skills/web-design-guidelines/SKILL.md |
find-skills |
User asks "how do I do X", "is there a skill for X" | .agents/skills/find-skills/SKILL.md |
| Skill | When to Load | Location |
|---|---|---|
better-auth-best-practices |
Configuring Better Auth server/client, OAuth, sessions | .agents/skills/better-auth-best-practices/SKILL.md |
better-auth-security-best-practices |
Securing auth setup, rate limiting, CSRF | .agents/skills/better-auth-security-best-practices/SKILL.md |
prisma-cli |
Running Prisma CLI commands, migrations, schema | .agents/skills/prisma-cli/SKILL.md |
prisma-client-api |
Writing database queries, CRUD operations | .agents/skills/prisma-client-api/SKILL.md |
prisma-database-setup |
Setting up or changing database connections | .agents/skills/prisma-database-setup/SKILL.md |
prisma-postgres |
Prisma Postgres specific operations | .agents/skills/prisma-postgres/SKILL.md |
prisma-driver-adapter-implementation |
Prisma v7 driver adapter work | .agents/skills/prisma-driver-adapter-implementation/SKILL.md |
vercel-composition-patterns |
Refactoring components with prop proliferation | .agents/skills/vercel-composition-patterns/SKILL.md |
emil-design-eng |
UI polish, component design, animation | .agents/skills/emil-design-eng/SKILL.md |
frontend-design |
Creating distinctive, production-grade frontend | .agents/skills/frontend-design/SKILL.md |
brand-guidelines |
Applying Anthropic's brand colors and typography | .agents/skills/brand-guidelines/SKILL.md |
testing-next-stack |
Setting up tests, Vitest, Playwright, RTL | .agents/skills/testing-next-stack/SKILL.md |
better-upload |
File uploads with better-upload | .agents/skills/better-upload/SKILL.md |
ai-seo |
AI-powered SEO optimization and analysis | .agents/skills/ai-seo/SKILL.md |
seo-content-writer |
SEO content writing, meta tags, keywords | .agents/skills/seo-content-writer/SKILL.md |
| Skill | When to Load | Location |
|---|---|---|
sdd-init |
Initialize SDD context in project | C:\Users\juanb\.config\opencode/skills/sdd-init/SKILL.md |
sdd-explore |
Explore and investigate ideas | C:\Users\juanb\.config\opencode/skills/sdd-explore/SKILL.md |
sdd-propose |
Create change proposals | C:\Users\juanb\.config\opencode/skills/sdd-propose/SKILL.md |
sdd-spec |
Write specifications | C:\Users\juanb\.config\opencode/skills/sdd-spec/SKILL.md |
sdd-design |
Create technical design | C:\Users\juanb\.config\opencode/skills/sdd-design/SKILL.md |
sdd-tasks |
Break down into tasks | C:\Users\juanb\.config\opencode/skills/sdd-tasks/SKILL.md |
sdd-apply |
Implement tasks | C:\Users\juanb\.config\opencode/skills/sdd-apply/SKILL.md |
sdd-verify |
Validate implementation | C:\Users\juanb\.config\opencode/skills/sdd-verify/SKILL.md |
sdd-archive |
Archive completed change | C:\Users\juanb\.config\opencode/skills/sdd-archive/SKILL.md |
sdd-onboard |
Guided SDD walkthrough | C:\Users\juanb\.config\opencode/skills/sdd-onboard/SKILL.md |
| Skill | When to Load | Location |
|---|---|---|
branch-pr |
Creating pull requests, PR workflow | C:\Users\juanb\.config\opencode/skills/branch-pr/SKILL.md |
issue-creation |
Creating GitHub issues | C:\Users\juanb\.config\opencode/skills/issue-creation/SKILL.md |
judgment-day |
Adversarial code review | C:\Users\juanb\.config\opencode/skills/judgment-day/SKILL.md |
skill-creator |
Creating new AI agent skills | C:\Users\juanb\.config\opencode/skills/skill-creator/SKILL.md |
skill-registry |
Update skill registry | C:\Users\juanb\.config\opencode/skills/skill-registry/SKILL.md |
go-testing |
Go testing patterns | C:\Users\juanb\.config\opencode/skills/go-testing/SKILL.md |
- Detect context first — match the task to a skill before writing code
- Load the skill — use the
skilltool to load full instructions - Apply ALL patterns from the loaded skill — they are your coding standards
- Multiple skills can apply — load all relevant ones (e.g.,
shadcn+next-best-practicesfor a new page) - For unknown domains — use
find-skillsto search the ecosystem
| Command | Description |
|---|---|
bun run dev |
Start dev server (Turbopack) |
bun run build |
Production build |
bun run start |
Start production server |
bun run typecheck |
TypeScript check (tsc --noEmit) |
bun run check |
Ultracite lint + format check |
bun run fix |
Ultracite auto-fix (run before committing) |
bun run db:push |
Push Prisma schema to DB |
bun run db:generate |
Generate Prisma client |
bun run db:migrate |
Run Prisma migrations |
bun run db:studio |
Open Prisma Studio |
bun run db:seed |
Seed database |
bun run db:reset |
Reset DB + regenerate + seed |
Tests: No test framework installed. When adding tests, prefer Vitest. Use it()/test() with async/await (no done callbacks). Don't commit .only or .skip.
Husky + lint-staged: Pre-commit hook runs bun x ultracite fix on *.{js,jsx,ts,tsx,json,jsonc,css,scss,md,mdx}.
Ultracite (Oxlint + Oxfmt) is the single source of truth. Run bun run fix before committing.
- 80 char print width, 2-space indent, spaces (no tabs)
- Double quotes, semicolons required, trailing commas (ES5)
- LF line endings, bracket spacing enabled, arrow parens always
- Imports auto-sorted ascending, case-insensitive, grouped with blank lines
bracketSameLine: false,quoteProps: "as-needed"
func-style, curly, no-nested-ternary, no-use-before-define, complexity, no-shadow, nextjs/no-img-element, max-statements, no-negated-condition, unicorn/catch-error-name, unicorn/no-array-reduce, unicorn/no-nested-ternary
- Strict mode,
noEmit,moduleResolution: bundler,target: ES2017 - Prefer explicit types for function signatures and return values
- Use
unknownoverany; leverage type narrowing over assertions - Use
as constfor immutable values and literal types - Extract magic numbers into named constants
- Function components only (no classes),
"use client"for client components - Server components by default (no directive needed)
- Hooks at top level only, never conditionally — specify all dependencies
- React 19: use
refas prop (noforwardRef), define props viainterface - Use
keyprop on iterables (prefer unique IDs over indices) - Don't define components inside other components
- Use semantic HTML and ARIA attributes for accessibility
- Files: kebab-case (
hackathon-card.tsx,use-auth.ts) - Components: PascalCase (
HackathonCard,ThemeProvider) - Functions/variables: camelCase (
getHackathons,isLoading) - Types/interfaces: PascalCase (
HackathonCardProps,TeamMember) - Database models: PascalCase in Prisma, snake_case tables via
@@map() - Constants: UPPER_SNAKE_CASE for true constants
- External packages (
next,react,@prisma/client) - Internal
@/aliased imports - Relative imports (
./,../)
- Throw
Errorobjects with descriptive messages, never strings - Use
try-catchmeaningfully — don't catch just to rethrow - Prefer early returns over nested conditionals
- Remove
console.log/debugger/alertfrom production code - Validate/sanitize user input with Zod schemas
- Add
rel="noopener"ontarget="_blank"links - Avoid
dangerouslySetInnerHTML; never useeval() - Never assign directly to
document.cookie
- Avoid spread syntax in accumulators within loops
- Use top-level regex literals, not inside loops
- Prefer specific imports over namespace imports (
import { foo }notimport * as) - Avoid barrel files (index files that re-export everything)
- Use Next.js
<Image>over<img>tags
app/ — Next.js App Router (routes, layouts, API)
api/auth/[...all]/ — Better Auth catch-all route
generated/prisma/ — Auto-generated Prisma client (gitignored)
shared/ — Shared code
components/ — Feature components (navbar, cards, etc.)
components/ui/ — shadcn/ui primitives
lib/ — Utilities (auth, prisma, utils, email)
prisma/ — Database schema + migrations
emails/ — React Email templates
@/*→ root@/shared/*→./shared/*@/components/*→./shared/components/*@/ui/*→./shared/components/ui/*@/lib/*→./shared/lib/*@/hooks/*→./shared/hooks/*@/utils/*→./shared/utils/*
For new features, create a dedicated Data Access Layer — internal server-only modules that control data access:
- Mark DAL files with
import 'server-only'at the top - Perform authorization checks inside the DAL, not in components
- Return minimal Data Transfer Objects (DTOs), not raw database records
- Only the DAL should access
process.envand database packages - Use
cache()from React for shared helpers (e.g.,getCurrentUser)
// data/user-dto.ts
import "server-only";
import { cache } from "react";
import { cookies } from "next/headers";
export const getCurrentUser = cache(async () => {
const token = cookies().get("session");
// ... validate and return minimal user info
});
export async function getProfileDTO(slug: string) {
const currentUser = await getCurrentUser();
const userData = await db.user.findUnique({ where: { slug } });
return { username: userData?.username, bio: userData?.bio };
}- Always re-verify auth inside actions — page-level checks do NOT extend to Server Actions
- Check authorization (resource ownership), not just authentication — prevent IDOR vulnerabilities
- Validate all client input —
searchParams,formData, headers are untrusted - Return minimal data — only what the UI needs, never full database records
- No mutations during rendering — use Server Actions for all side effects
- Use DAL for mutations — keep
"use server"files thin, delegate auth + DB logic toserver-onlymodules
| Server Components | Client Components |
|---|---|
| Run only on server | Run on server (prerender) AND browser |
| Can access secrets, DB, env vars | Must NOT access privileged data or server-only modules |
| Safe by default | Follow browser security assumptions |
- Use
import 'server-only'to prevent server code from leaking to client bundles - Props passed to client components must be serializable and safe for public exposure
- Avoid passing sensitive data through component trees — read from DAL directly
- DAL: Are DB packages and
process.envonly imported inserver-onlymodules? "use client"files: Are props expecting private data? Are type signatures overly broad?"use server"files: Are args validated? Is user re-authorized? Does action check resource ownership?/[param]/folders: Are params validated as user input?route.ts: Audit carefully — has elevated power
- Server Components by default — only add
"use client"when needed - Never make client components async — fetch data in parent server component, pass as props
- Props to client components must be serializable — no
Date,Map,Set, class instances, or functions - Convert
Dateto.toISOString()before passing to client components
paramsandsearchParamsare Promises — alwaysawaitthemcookies()andheaders()are async- Type page props as:
type Props = { params: Promise<{ slug: string }>; searchParams: Promise<{ q?: string }> }
- Reads: Fetch directly in Server Components (no API layer needed)
- Mutations: Use Server Actions (
"use server") withrevalidatePath/revalidateTag - External APIs/Webhooks: Use Route Handlers (
route.ts) - Use
cache()from React to deduplicate fetches betweengenerateMetadataand page
- Use
error.tsx(must be"use client") for route-level errors - Use
global-error.tsx(must include<html>and<body>) for root layout errors - Use
notFound()for 404s,redirect()for navigation - Don't wrap navigation APIs in try-catch — they throw special errors Next.js handles internally
- Use
metadataexport for static metadata,generateMetadatafor dynamic - Both only work in Server Components — move client logic to child components
- Use
cache()to avoid duplicate fetches between metadata and page content
useSearchParams()always requires wrapping in<Suspense>usePathname()requires Suspense in dynamic routes
route.tsandpage.tsxcannot coexist in the same folder- Use Server Actions for UI mutations, Route Handlers for external APIs/webhooks
motion components (motion.div, motion.span, etc.) are CLIENT-ONLY. They use browser APIs and hooks that crash in Server Components.
- Never import
motionin a Server Component — nomotion/*imports in files without"use client" - Extract animated sections into dedicated client components — create a reusable component (e.g.,
animated-header.tsx) with"use client", import motion there, then use that component in your server page - Prefer reusable animated wrappers — if you animate a pattern (fade-in, slide-up), make it reusable (e.g.,
<FadeIn>,<SlideUp>) instead of sprinklingmotion.diveverywhere - Import path: use
motion/react(notframer-motiondirectly) — the project depends onmotionpackage
// ❌ WRONG: motion in a server component (page.tsx)
import { motion } from "motion/react";
export default function SettingsPage() {
return <motion.div initial={{ opacity: 0 }}>...</motion.div>;
}
// ✅ CORRECT: extract to a client component
// components/animated-section.tsx
("use client");
import { motion } from "motion/react";
export function AnimatedSection({ children }: { children: React.ReactNode }) {
return (
<motion.div initial={{ opacity: 0, y: -8 }} animate={{ opacity: 1, y: 0 }}>
{children}
</motion.div>
);
}
// page.tsx (server component)
import { AnimatedSection } from "./_components/animated-section";
export default function SettingsPage() {
return <AnimatedSection>...</AnimatedSection>;
}Database: Prisma singleton with globalForPrisma. Uses @prisma/adapter-pg with @neondatabase/serverless for PostgreSQL. All models use @@map() for snake_case table names.
Auth: Better Auth with OAuth only (GitHub + Google). Session: 7 days. Additional user fields: username (required), bio, githubUsername, location, position, techStack, karmaPoints.
Styling: Tailwind CSS v4 with @import "tailwindcss". CSS variables with oklch. Brand colors: --brand-green, --brand-purple. Custom utilities: .glass, .glow-green, .pixel-grid, .scanlines, .pixel-border. Use cn() for conditional classes. --radius: 0 (sharp corners by default).
State: nuqs for URL search params (wrapped in <NuqsAdapter> in layout). No global state manager.
Always use TanStack Form (@tanstack/react-form) with Zod for all forms. No react-hook-form, no manual state.
"use client";
import { useForm } from "@tanstack/react-form";
import * as z from "zod";
import { Button } from "@/components/ui/button";
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
import { Input } from "@/components/ui/input";
const formSchema = z.object({
title: z.string().min(5, "Title must be at least 5 characters."),
});
export function MyForm() {
const form = useForm({
defaultValues: { title: "" },
validators: { onSubmit: formSchema },
onSubmit: async ({ value }) => {
// handle submit
},
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit();
}}
>
<FieldGroup>
<form.Field
name="title"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid;
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>Title</FieldLabel>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
/>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
);
}}
/>
</FieldGroup>
<Button type="submit">Submit</Button>
</form>
);
}- Zod schema first — define
formSchemawithz.object()at module level validators.onSubmit— pass schema touseForm({ validators: { onSubmit: formSchema } })- Render prop pattern — use
children={(field) => ...}onform.Field, NOT the function-as-child JSX pattern - Invalid state —
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid - Always wire:
value={field.state.value},onBlur={field.handleBlur},onChange={(e) => field.handleChange(e.target.value)} - Accessibility:
data-invalid={isInvalid}on<Field>,aria-invalid={isInvalid}on the control - Errors:
{isInvalid && <FieldError errors={field.state.meta.errors} />} - Reset:
<Button type="button" onClick={() => form.reset()}>Reset</Button>
| Control | Value binding | Change handler |
|---|---|---|
<Input>, <Textarea> |
value={field.state.value} |
onChange={(e) => field.handleChange(e.target.value)} |
<Select> |
value={field.state.value} |
onValueChange={field.handleChange} |
<Checkbox> |
checked={field.state.value} |
onCheckedChange={(checked) => field.handleChange(checked === true)} |
<Switch> |
checked={field.state.value} |
onCheckedChange={field.handleChange} |
<RadioGroup> |
value={field.state.value} |
onValueChange={field.handleChange} |
Use mode="array" on the parent field. Access items via bracket notation:
<form.Field name="emails" mode="array">
{(field) => (
<FieldGroup>
{field.state.value.map((_, index) => (
<form.Field key={index} name={`emails[${index}].address`}>
{(subField) => {
/* same pattern as scalar fields */
}}
</form.Field>
))}
<Button type="button" onClick={() => field.pushValue({ address: "" })}>
Add
</Button>
</FieldGroup>
)}
</form.Field>- Add:
field.pushValue(item) - Remove:
field.removeValue(index) - Zod:
z.array(z.object({...})).min(1, "At least one required")
shared/lib/auth.ts— Better Auth configurationshared/lib/prisma.ts— Prisma client singletonshared/lib/utils.ts—cn()utility (clsx + tailwind-merge)prisma/schema.prisma— Database schemanext.config.ts— Next.js configurationapp/globals.css— Tailwind v4 theme, CSS variablesapp/layout.tsx— Root layout (ThemeProvider, NuqsAdapter, Navbar, Footer)
Run bun run fix before committing. Focus on business logic, naming, architecture, and edge cases — Ultracite handles the rest.