This repository is developed collaboratively by humans and AI coding agents.
These guidelines ensure:
You are acting as a senior software engineer.
Your responsibilities:
This project is developed by vibecoders — developers who focus on product vision and user experience, but may not always specify every technical detail. The AI agent must compensate by being proactively professional.
The agent must independently apply best practices, even when not explicitly asked:
| Area | Default behavior |
|---|---|
| Security | Add auth checks, RLS policies, input validation |
| Error handling | Wrap async operations, show user-friendly messages, log details |
| Types | Create TypeScript interfaces for all data structures |
| Testing | Add tests for new service functions and utilities |
| Documentation | Update docs when adding features or changing architecture |
| Performance | Use pagination, selective queries, proper indexes |
| Accessibility | Use semantic HTML, add aria labels, ensure keyboard navigation |
Only ask when:
Do NOT ask when:
User → Frontend (Next.js App Router) → Supabase Client → PostgreSQL Database
→ Supabase Edge Functions (business logic)
→ Supabase Auth (authentication)
→ Supabase Storage (file uploads)
| Layer | Technology | Responsibility |
|---|---|---|
| Frontend | Next.js (App Router) + TypeScript + Tailwind CSS | UI rendering, SSR/SSG, user interaction, API calls |
| Auth | Supabase Auth | User sessions, JWT tokens, OAuth providers |
| API | Supabase Client + Edge Functions | Data access, business logic, secure operations |
| Database | PostgreSQL via Supabase | Schema, RLS policies, migrations |
| Hosting | GitHub Pages (static export) | Static site deployment via GitHub Actions, automatic HTTPS |
| Layer | Does | Does NOT |
|---|---|---|
| Frontend | Renders UI, handles interaction, calls APIs | Contain business logic, validate data authoritatively, access DB directly |
| Edge Functions | Business logic, secret handling, complex queries | Render UI, manage state |
| Database | Schema integrity, RLS enforcement, data validation | Contain application logic in stored procedures (unless justified) |
repo/
├── AGENTS.md # This file — AI agent guidelines
├── README.md # Project overview and setup
├── ARCHITECTURE.md # Detailed architecture docs
│
├── src/
│ ├── components/ # Reusable UI components
│ ├── pages/ # Page-level components / routes
│ ├── services/ # API service layer (Supabase calls)
│ ├── lib/ # Shared utilities, Supabase client init
│ ├── types/ # TypeScript type definitions
│ └── utils/ # Pure helper functions
│
├── public/ # Static assets (served to users)
│
├── supabase/
│ ├── migrations/ # Numbered SQL migration files
│ ├── functions/ # Edge Functions
│ └── seed/ # Seed data for development
│
├── tests/ # All test files
│ ├── services/ # Service layer tests
│ ├── functions/ # Edge Function tests
│ └── utils/ # Utility function tests
│
├── docs/ # Feature documentation
│
├── dev/ # Internal developer notes (NOT served to frontend)
│ ├── strategy/ # Product strategy, roadmap, priorities
│ └── notes/ # Technical research, architecture decisions
│
├── .env.example # Template for environment variables
└── .github/
└── workflows/ # CI/CD pipeline definitions
dev/ folder is for internal developer use only — its contents must never be served or bundled into the frontend application.Create one Supabase client instance in src/lib/supabase.ts:
import { createClient } from '@supabase/supabase-js'
import type { Database } from '../types/database.types'
export const supabase = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
anon key is used in the frontend. Never expose the service_role key.service_role key is only used inside Edge Functions.supabase gen types for type safety.// ✅ Frontend: uses anon key + user JWT (via RLS)
const { data, error } = await supabase
.from('todos')
.select('*')
.eq('user_id', user.id)
// ✅ Edge Function: uses service_role for admin operations
const supabaseAdmin = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
001_create_users.sql, 002_add_profiles.sqlid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
TIMESTAMPTZ (not TIMESTAMP) — always store timezone-aware timestamps.IF NOT EXISTS).up and down logic when feasible.supabase db reset before committing.| Element | Convention | Example |
|---|---|---|
| Tables | snake_case, plural | user_profiles |
| Columns | snake_case | first_name |
| Indexes | idx_{table}_{column} |
idx_todos_user_id |
| Constraints | {table}_{type}_{column} |
todos_fk_user_id |
| Migrations | NNN_description.sql |
003_add_user_roles.sql |
All tables must have RLS enabled. No exceptions.
auth.uid() for user-specific data.-- Enable RLS
ALTER TABLE todos ENABLE ROW LEVEL SECURITY;
-- Users can only see their own data
CREATE POLICY "Users can view own todos"
ON todos FOR SELECT
USING (user_id = auth.uid());
-- Users can only insert their own data
CREATE POLICY "Users can create own todos"
ON todos FOR INSERT
WITH CHECK (user_id = auth.uid());
-- Users can only update their own data
CREATE POLICY "Users can update own todos"
ON todos FOR UPDATE
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());
-- Users can only delete their own data
CREATE POLICY "Users can delete own todos"
ON todos FOR DELETE
USING (user_id = auth.uid());
auth.uid() only in USING but not WITH CHECK.service_role key.supabase/functions/
├── my-function/
│ └── index.ts # Entry point
├── shared/ # Shared utilities across functions
│ └── cors.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
serve(async (req) => {
// CORS handling
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
try {
// Auth verification
const authHeader = req.headers.get('Authorization')
if (!authHeader) throw new Error('Missing authorization')
// Create authenticated client
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
{ global: { headers: { Authorization: authHeader } } }
)
// Business logic here
const { data, error } = await supabase.from('table').select('*')
if (error) throw error
return new Response(JSON.stringify(data), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
} catch (err) {
return new Response(JSON.stringify({ error: err.message }), {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})
}
})
src/services/) for all Supabase calls — components should not call Supabase directly.// ✅ Always handle Supabase errors explicitly
const { data, error } = await supabase.from('todos').select('*')
if (error) {
console.error('Failed to fetch todos:', error.message)
// Show user-friendly message — never expose raw error details
showToast('Could not load your todos. Please try again.')
return
}
// ✅ Structured error handling with appropriate status codes
try {
// ... logic
} catch (err) {
console.error('Edge function error:', {
function: 'process-payment',
error: err.message,
timestamp: new Date().toISOString()
})
return new Response(
JSON.stringify({ error: 'An unexpected error occurred' }),
{ status: 500 }
)
}
async operation must have error handling — no unhandled promise rejections.any type unless absolutely necessary (document why).| Element | Convention | Example |
|---|---|---|
| Variables, functions | camelCase | getUserProfile |
| Components | PascalCase | TodoListItem |
| Files | kebab-case | todo-list-item.tsx |
| Types/Interfaces | PascalCase | UserProfile |
| Constants | SCREAMING_SNAKE | MAX_RETRY_COUNT |
| Database columns | snake_case | created_at |
// ✅ Early return pattern
async function getUser(id: string): Promise<User | null> {
if (!id) return null
const { data, error } = await supabase
.from('users')
.select('*')
.eq('id', id)
.single()
if (error) {
console.error('getUser failed:', error.message)
return null
}
return data
}
Before adding any dependency:
service_role key, API secrets, or database credentials in frontend code..env.example with all required variables (without values)..env files — ensure .gitignore includes them.Deno.env.get().# .env.example — commit this file (without values)
NEXT_PUBLIC_SUPABASE_URL= # Supabase project URL
NEXT_PUBLIC_SUPABASE_ANON_KEY= # Supabase anon/public key (safe for frontend)
# Edge Functions only (never in frontend)
SUPABASE_SERVICE_ROLE_KEY= # Admin key — server-side only
SUPABASE_DB_URL= # Direct DB connection (if needed)
NEXT_PUBLIC_ so they are bundled by Next.js..env.example.WHERE, JOIN, and ORDER BY.select('column1, column2') instead of select('*') when possible.// ✅ Efficient: single query with join
const { data } = await supabase
.from('todos')
.select('*, category:categories(name)')
.eq('user_id', user.id)
.range(0, 19) // Paginate: first 20 items
// ❌ Inefficient: N+1 queries
const { data: todos } = await supabase.from('todos').select('*')
for (const todo of todos) {
const { data: category } = await supabase
.from('categories')
.select('name')
.eq('id', todo.category_id) // One query per todo!
}
| Branch | Purpose | Rules |
|---|---|---|
main |
Production | Never push directly. Deploy via merge only. |
dev |
Development integration | PRs from feature branches. |
feature/* |
Individual features | Branch from dev, PR back to dev. |
fix/* |
Bug fixes | Branch from dev or main (hotfix). |
Use conventional commits:
feat: add user profile page
fix: correct RLS policy for shared todos
chore: update Supabase client to v2.39
docs: add Edge Function deployment guide
tests/
├── services/ # Service layer tests
├── functions/ # Edge Function tests
├── utils/ # Utility function tests
└── setup.ts # Test configuration and helpers
/tests, mirroring the src/ structure.supabase start).README.md — project overview, setup instructions, tech stackARCHITECTURE.md — system design, data flow, key decisionsdocs/ — feature-specific documentation, API behavior, deployment guidesWhen implementing a feature, follow this sequence:
Before committing code, verify every item:
any types without justificationThis repository should remain:
Prefer clarity over cleverness. If a junior developer can’t understand it, simplify it.