feat: rework starter kit for self-hosted deploy (Vite/React + Fastify/Drizzle/Postgres)

Replace Next.js/Supabase/Vercel stack with a self-hostable setup: Vite+React
frontend, Fastify+Drizzle+PostgreSQL backend with own JWT auth, deploy via
git pull + rsync + systemd (checkout/service separated, updates pulled from
Gitea). Adds a reference auth route (register/login/me) as the canonical
pattern for new routes. Updates all skills/rules/agents/docs to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BMdBJfD9TT6XUU2NqGG1PN
This commit is contained in:
patrickandClaude Sonnet 5 committed 2026-09-08 11:42:47 +02:00
1 parent a893084a88
commit 4a19335a1e
99 files changed
+8898 -9835

No files matched your search

+5 -6
View File
@@ -1,6 +1,6 @@
--- ---
name: Backend Developer name: Backend Developer
description: Builds APIs, database schemas, and server-side logic with Supabase description: Builds APIs, database schemas, and server-side logic with Fastify + Drizzle + PostgreSQL
model: opus model: opus
maxTurns: 50 maxTurns: 50
tools: tools:
@@ -13,16 +13,15 @@ tools:
- AskUserQuestion - AskUserQuestion
--- ---
You are a Backend Developer building APIs, database schemas, and server-side logic with Supabase. You are a Backend Developer building APIs, database schemas, and server-side logic with Fastify, Drizzle ORM, and PostgreSQL.
Key rules: Key rules:
- ALWAYS enable Row Level Security on every new table - ALWAYS check authentication (`request.jwtVerify()`) and authorization (ownership) explicitly in route handlers — there is no RLS backstop
- Create RLS policies for SELECT, INSERT, UPDATE, DELETE
- Validate all inputs with Zod schemas on POST/PUT endpoints - Validate all inputs with Zod schemas on POST/PUT endpoints
- Add database indexes on frequently queried columns - Add database indexes on frequently queried columns
- Use Supabase joins instead of N+1 query loops - Use Drizzle joins/relational queries instead of N+1 query loops
- Never hardcode secrets in source code - Never hardcode secrets in source code
- Always check authentication before processing requests - Generate migrations with `npm run db:generate`, review the SQL before applying with `npm run db:migrate`
Read `.claude/rules/backend.md` for detailed backend rules. Read `.claude/rules/backend.md` for detailed backend rules.
Read `.claude/rules/security.md` for security requirements. Read `.claude/rules/security.md` for security requirements.
+4 -4
View File
@@ -1,6 +1,6 @@
--- ---
name: Frontend Developer name: Frontend Developer
description: Builds UI components with React, Next.js, Tailwind CSS, and shadcn/ui description: Builds UI components with React, Vite, Tailwind CSS, and shadcn/ui
model: opus model: opus
maxTurns: 50 maxTurns: 50
tools: tools:
@@ -13,11 +13,11 @@ tools:
- AskUserQuestion - AskUserQuestion
--- ---
You are a Frontend Developer building UI with React, Next.js, Tailwind CSS, and shadcn/ui. You are a Frontend Developer building UI with React, Vite, Tailwind CSS, and shadcn/ui.
Key rules: Key rules:
- ALWAYS check shadcn/ui components before creating custom ones: `ls src/components/ui/` - ALWAYS check shadcn/ui components before creating custom ones: `ls frontend/src/components/ui/`
- If a shadcn component is missing, install it: `npx shadcn@latest add <name> --yes` - If a shadcn component is missing, install it: `npx shadcn@latest add <name> --yes` (run inside `frontend/`)
- Use Tailwind CSS exclusively for styling (no inline styles, no CSS modules) - Use Tailwind CSS exclusively for styling (no inline styles, no CSS modules)
- Follow the component architecture from the feature spec's Tech Design section - Follow the component architecture from the feature spec's Tech Design section
- Implement loading, error, and empty states for all components - Implement loading, error, and empty states for all components
+24 -16
View File
@@ -1,32 +1,40 @@
--- ---
paths: paths:
- "src/app/api/**" - "backend/src/routes/**"
- "src/lib/supabase*" - "backend/src/db/**"
- "supabase/**"
--- ---
# Backend Development Rules # Backend Development Rules
## Database (Supabase) ## Database (PostgreSQL + Drizzle)
- ALWAYS enable Row Level Security on every table - Every table gets explicit foreign keys with `onDelete` behavior defined
- Create RLS policies for SELECT, INSERT, UPDATE, DELETE
- Add indexes on columns used in WHERE, ORDER BY, and JOIN clauses - Add indexes on columns used in WHERE, ORDER BY, and JOIN clauses
- Use foreign keys with ON DELETE CASCADE where appropriate - Auth/ownership checks happen in route handlers, not in the database — there is no RLS equivalent
- Never skip RLS - security first - Migrations generated via `npm run db:generate`, applied via `npm run db:migrate` — never hand-edit generated SQL after it has been applied
## API Routes ## API Routes (Fastify)
- Validate all inputs using Zod schemas before processing - Validate all inputs using Zod schemas before processing
- Always check authentication: verify user session exists - Always check authentication: verify JWT via `request.jwtVerify()` before touching protected data
- Return meaningful error messages with appropriate HTTP status codes - Return meaningful error messages with appropriate HTTP status codes
- Use `.limit()` on all list queries - Paginate all list queries (`limit`/`offset`, sane default limit)
## Query Patterns ## Query Patterns
- Use Supabase joins instead of N+1 query loops - Use Drizzle relational queries / joins instead of N+1 query loops
- Use `unstable_cache` from Next.js for rarely-changing data - Wrap multi-statement writes in a transaction (`db.transaction(...)`)
- Always handle errors from Supabase responses - Always handle and log errors from `pg` / Drizzle calls
## Security ## Security
- Never hardcode secrets in source code - Never hardcode secrets in source code
- Use environment variables for all credentials - Use environment variables for all credentials (see `.env.example`)
- Validate and sanitize all user input - Validate and sanitize all user input
- Use parameterized queries (Supabase handles this) - Drizzle uses parameterized queries by default — never build SQL via string concatenation
## Reference Pattern: Auth Routes
`backend/src/routes/auth.ts` is the canonical example new routes should follow:
- Zod schema (`credentialsSchema`) parsed with `safeParse`, 400 on failure with `error.issues[0].message`
- Passwords hashed with `bcryptjs` (`bcrypt.hash(password, 12)`), never stored or logged in plaintext
- Duplicate/invalid-credential checks return generic messages ("Invalid email or password") — never reveal whether the email exists on login
- Protected routes use `{ preHandler: app.authenticate }` (decorator defined in `server.ts`, verifies the JWT via `request.jwtVerify()`)
- Auth endpoints get a tighter route-level rate limit (`config: { rateLimit: { max: 5, timeWindow: "1 minute" } }`) on top of the global limit
- `app.jwt.sign({ sub: user.id, email: user.email })` — the JWT payload shape is declared once in `backend/src/types.d.ts` (`FastifyJWT` module augmentation), so `request.user` is typed everywhere without casts
- New protected/authenticated routes should follow this same shape: typed payload via `types.d.ts`, `app.authenticate` as `preHandler`, never `request.user as any`
+9 -9
View File
@@ -1,17 +1,16 @@
--- ---
paths: paths:
- "src/components/**" - "frontend/src/components/**"
- "src/app/**/page.tsx" - "frontend/src/**/*.tsx"
- "src/app/**/layout.tsx" - "frontend/src/hooks/**"
- "src/hooks/**"
--- ---
# Frontend Development Rules # Frontend Development Rules
## shadcn/ui First (MANDATORY) ## shadcn/ui First (MANDATORY)
- Before creating ANY UI component, check if shadcn/ui has it: `ls src/components/ui/` - Before creating ANY UI component, check if shadcn/ui has it: `ls frontend/src/components/ui/`
- NEVER create custom implementations of: Button, Input, Select, Checkbox, Switch, Dialog, Modal, Alert, Toast, Table, Tabs, Card, Badge, Dropdown, Popover, Tooltip, Navigation, Sidebar, Breadcrumb - NEVER create custom implementations of: Button, Input, Select, Checkbox, Switch, Dialog, Modal, Alert, Toast, Table, Tabs, Card, Badge, Dropdown, Popover, Tooltip, Navigation, Sidebar, Breadcrumb
- If a shadcn component is missing, install it: `npx shadcn@latest add <name> --yes` - If a shadcn component is missing, install it: `npx shadcn@latest add <name> --yes` (run inside `frontend/`)
- Custom components are ONLY for business-specific compositions that internally use shadcn primitives - Custom components are ONLY for business-specific compositions that internally use shadcn primitives
## Import Pattern ## Import Pattern
@@ -28,7 +27,8 @@ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
- Keep components small and focused - Keep components small and focused
- Use TypeScript interfaces for all props - Use TypeScript interfaces for all props
## Auth Best Practices (Supabase) ## Auth Best Practices (own JWT backend)
- Use `window.location.href` for post-login redirect (not `router.push`) - Store the JWT in memory/state, not `localStorage`, if XSS exposure is a concern for the project; document the choice in the feature spec
- Always verify `data.session` exists before redirecting - After login, redirect via `react-router`'s `navigate()` once the auth state is confirmed set
- Always reset loading state in all code paths (success, error, finally) - Always reset loading state in all code paths (success, error, finally)
- Attach the JWT as `Authorization: Bearer <token>` on every API call to `/api/**`
+2 -2
View File
@@ -25,8 +25,8 @@ Before starting ANY work, check if the project has been initialized:
- Commit format: `type(PROJ-X): description` - Commit format: `type(PROJ-X): description`
- Types: feat, fix, refactor, test, docs, deploy, chore - Types: feat, fix, refactor, test, docs, deploy, chore
- Check existing features before creating new ones: `ls features/ | grep PROJ-` - Check existing features before creating new ones: `ls features/ | grep PROJ-`
- Check existing components before building: `git ls-files src/components/` - Check existing components before building: `git ls-files frontend/src/components/`
- Check existing APIs before building: `git ls-files src/app/api/` - Check existing APIs before building: `git ls-files backend/src/routes/`
## Human-in-the-Loop ## Human-in-the-Loop
- Always ask for user approval before finalizing deliverables - Always ask for user approval before finalizing deliverables
+10 -11
View File
@@ -1,18 +1,16 @@
--- ---
paths: paths:
- "src/app/api/**" - "backend/src/**"
- ".env*" - ".env*"
- "supabase/**" - "backend/drizzle.config.ts"
- "next.config.*"
--- ---
# Security Rules # Security Rules
## Secrets Management ## Secrets Management
- NEVER commit secrets, API keys, or credentials to git - NEVER commit secrets, API keys, or credentials to git
- Use `.env.local` for local development (already in .gitignore) - Use `.env` for local/server config (already in `.gitignore`), document required vars in `.env.example` with dummy values
- Use `NEXT_PUBLIC_` prefix ONLY for values safe to expose in browser - Frontend env vars must be explicitly prefixed `VITE_` and contain nothing secret — anything with that prefix ships to the browser
- Document all required env vars in `.env.local.example` with dummy values
## Input Validation ## Input Validation
- Validate ALL user input on the server side with Zod - Validate ALL user input on the server side with Zod
@@ -20,17 +18,18 @@ paths:
- Sanitize data before database insertion - Sanitize data before database insertion
## Authentication ## Authentication
- Always verify authentication before processing API requests - Always verify the JWT (`request.jwtVerify()`) before processing protected API requests
- Use Supabase RLS as a second line of defense - Authorization/ownership checks live in route handlers — there is no database-level RLS backstop, so don't skip them
- Implement rate limiting on authentication endpoints - Implement rate limiting on authentication endpoints (`@fastify/rate-limit`, tighter limits on `/api/auth/**`)
## Security Headers ## Security Headers
- X-Frame-Options: DENY - X-Frame-Options: DENY
- X-Content-Type-Options: nosniff - X-Content-Type-Options: nosniff
- Referrer-Policy: origin-when-cross-origin - Referrer-Policy: origin-when-cross-origin
- Strict-Transport-Security with includeSubDomains - Strict-Transport-Security with includeSubDomains
- Set via `@fastify/helmet` on the backend, or nginx `add_header` directives at the reverse proxy
## Code Review Triggers ## Code Review Triggers
- Any changes to RLS policies require explicit user approval
- Any changes to authentication flow require explicit user approval - Any changes to authentication flow require explicit user approval
- Any new environment variables must be documented in .env.local.example - Any changes to database migrations require explicit user approval before running `db:migrate` against a server
- Any new environment variables must be documented in `.env.example`
+2 -2
View File
@@ -21,8 +21,8 @@ NEVER write code or show implementation details:
## Before Starting ## Before Starting
1. Read `features/INDEX.md` to understand project context 1. Read `features/INDEX.md` to understand project context
2. Check existing components: `git ls-files src/components/` 2. Check existing components: `git ls-files frontend/src/components/`
3. Check existing APIs: `git ls-files src/app/api/` 3. Check existing APIs: `git ls-files backend/src/routes/`
4. Read the feature spec the user references 4. Read the feature spec the user references
## Workflow ## Workflow
+37 -31
View File
@@ -1,6 +1,6 @@
--- ---
name: backend name: backend
description: Build APIs, database schemas, and server-side logic with Supabase. Use after frontend is built. description: Build APIs, database schemas, and server-side logic with Fastify + Drizzle + PostgreSQL. Use after frontend is built.
argument-hint: [feature-spec-path] argument-hint: [feature-spec-path]
user-invocable: true user-invocable: true
context: fork context: fork
@@ -11,20 +11,20 @@ model: opus
# Backend Developer # Backend Developer
## Role ## Role
You are an experienced Backend Developer. You read feature specs + tech design and implement APIs, database schemas, and server-side logic using Supabase and Next.js. You are an experienced Backend Developer. You read feature specs + tech design and implement APIs, database schemas, and server-side logic using Fastify, Drizzle ORM, and PostgreSQL.
## Before Starting ## Before Starting
1. Read `features/INDEX.md` for project context 1. Read `features/INDEX.md` for project context
2. Read the feature spec referenced by the user (including Tech Design section) 2. Read the feature spec referenced by the user (including Tech Design section)
3. Check existing APIs: `git ls-files src/app/api/` 3. Check existing routes: `git ls-files backend/src/routes/`
4. Check existing database patterns: `git log --oneline -S "CREATE TABLE" -10` 4. Check existing schema: `cat backend/src/db/schema.ts`
5. Check existing lib files: `ls src/lib/` 5. Check existing migrations: `ls backend/drizzle/`
## Workflow ## Workflow
### 1. Read Feature Spec + Design ### 1. Read Feature Spec + Design
- Understand the data model from Solution Architect - Understand the data model from Solution Architect
- Identify tables, relationships, and RLS requirements - Identify tables, relationships, and authorization requirements
- Identify API endpoints needed - Identify API endpoints needed
### 2. Ask Technical Questions ### 2. Ask Technical Questions
@@ -35,21 +35,21 @@ Use `AskUserQuestion` for:
- What specific input validations are required? - What specific input validations are required?
### 3. Create Database Schema ### 3. Create Database Schema
- Write SQL for new tables in Supabase SQL Editor - Add/extend tables in `backend/src/db/schema.ts` (Drizzle)
- Enable Row Level Security on EVERY table - Run `npm run db:generate` to produce the migration, review the generated SQL
- Create RLS policies for all CRUD operations
- Add indexes on performance-critical columns (WHERE, ORDER BY, JOIN) - Add indexes on performance-critical columns (WHERE, ORDER BY, JOIN)
- Use foreign keys with ON DELETE CASCADE where appropriate - Use foreign keys with `onDelete: "cascade"` where appropriate
- Run `npm run db:migrate` against the dev database
### 4. Create API Routes ### 4. Create API Routes
- Create route handlers in `/src/app/api/` - Create route handlers in `backend/src/routes/`, register them in `server.ts`
- Implement CRUD operations - Implement CRUD operations
- Add Zod input validation on all POST/PUT endpoints - Add Zod input validation on all POST/PUT endpoints
- Add proper error handling with meaningful messages - Add proper error handling with meaningful messages
- Always check authentication (verify user session) - Always check authentication (`request.jwtVerify()`) and authorization (ownership) explicitly — there is no RLS backstop
### 5. Connect Frontend ### 5. Connect Frontend
- Update frontend components to use real API endpoints - Update frontend components to use real API endpoints (`fetch`/`axios` against `/api/**`, proxied by Vite in dev)
- Replace any mock data or localStorage with API calls - Replace any mock data or localStorage with API calls
- Handle loading and error states - Handle loading and error states
@@ -62,30 +62,36 @@ If your context was compacted mid-task:
1. Re-read the feature spec you're implementing 1. Re-read the feature spec you're implementing
2. Re-read `features/INDEX.md` for current status 2. Re-read `features/INDEX.md` for current status
3. Run `git diff` to see what you've already changed 3. Run `git diff` to see what you've already changed
4. Run `git ls-files src/app/api/` to see current API state 4. Run `git ls-files backend/src/routes/` to see current API state
5. Continue from where you left off - don't restart or duplicate work 5. Continue from where you left off - don't restart or duplicate work
## Output Format Examples ## Output Format Examples
### Database Migration ### Drizzle Schema
```sql ```ts
CREATE TABLE tasks ( export const tasks = pgTable("tasks", {
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id: uuid("id").primaryKey().defaultRandom(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
title TEXT NOT NULL, title: text("title").notNull(),
status TEXT CHECK (status IN ('todo', 'in_progress', 'done')) DEFAULT 'todo', status: text("status", { enum: ["todo", "in_progress", "done"] }).notNull().default("todo"),
created_at TIMESTAMPTZ DEFAULT NOW() createdAt: timestamp("created_at").notNull().defaultNow(),
); }, (table) => ({
userIdx: index("idx_tasks_user_id").on(table.userId),
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY; statusIdx: index("idx_tasks_status").on(table.status),
}));
CREATE POLICY "Users see own tasks" ON tasks
FOR SELECT USING (auth.uid() = user_id);
CREATE INDEX idx_tasks_user_id ON tasks(user_id);
CREATE INDEX idx_tasks_status ON tasks(status);
``` ```
### Route Handler
```ts
app.get("/api/tasks", { preHandler: app.authenticate }, async (request) => {
const userId = request.user.sub;
return db.select().from(tasks).where(eq(tasks.userId, userId)).limit(50);
});
```
## Reference Implementation
`backend/src/routes/auth.ts` + `backend/src/types.d.ts` show the canonical route pattern (Zod validation, `app.authenticate` preHandler, typed JWT payload). Follow this shape for new routes rather than inventing a new one. See [.claude/rules/backend.md](../../rules/backend.md#reference-pattern-auth-routes) for the annotated breakdown.
## Production References ## Production References
- See [database-optimization.md](../../docs/production/database-optimization.md) for query optimization - See [database-optimization.md](../../docs/production/database-optimization.md) for query optimization
- See [rate-limiting.md](../../docs/production/rate-limiting.md) for rate limiting setup - See [rate-limiting.md](../../docs/production/rate-limiting.md) for rate limiting setup
+14 -14
View File
@@ -1,33 +1,33 @@
# Backend Implementation Checklist # Backend Implementation Checklist
## Core Checklist ## Core Checklist
- [ ] Checked existing tables/APIs via git before creating new ones - [ ] Checked existing tables/routes via git before creating new ones
- [ ] Database tables created in Supabase - [ ] Database tables added to Drizzle schema (`backend/src/db/schema.ts`)
- [ ] Row Level Security enabled on ALL new tables - [ ] Migration generated (`npm run db:generate`) and applied (`npm run db:migrate`)
- [ ] RLS policies created for SELECT, INSERT, UPDATE, DELETE
- [ ] Indexes created on performance-critical columns - [ ] Indexes created on performance-critical columns
- [ ] Foreign keys set with appropriate ON DELETE behavior - [ ] Foreign keys set with appropriate `onDelete` behavior
- [ ] All planned API endpoints implemented in `/src/app/api/` - [ ] All planned endpoints implemented in `backend/src/routes/` — follow the pattern in `backend/src/routes/auth.ts` (Zod `safeParse`, typed JWT payload via `types.d.ts`, `app.authenticate` preHandler)
- [ ] Authentication verified (no access without valid session) - [ ] Authentication verified (no access without a valid JWT)
- [ ] Ownership/authorization checked explicitly in the route handler
- [ ] Input validation with Zod on all POST/PUT requests - [ ] Input validation with Zod on all POST/PUT requests
- [ ] Meaningful error messages with correct HTTP status codes - [ ] Meaningful error messages with correct HTTP status codes
- [ ] No TypeScript errors in API routes - [ ] No TypeScript errors in routes
- [ ] All endpoints tested manually - [ ] All endpoints tested manually
- [ ] No hardcoded secrets in source code - [ ] No hardcoded secrets in source code
- [ ] Frontend connected to real API endpoints - [ ] Frontend connected to real API endpoints
- [ ] User has reviewed and approved - [ ] User has reviewed and approved
## Verification (run before marking complete) ## Verification (run before marking complete)
- [ ] `npm run build` passes without errors - [ ] `npm run build` passes without errors (backend)
- [ ] All acceptance criteria from feature spec addressed in API - [ ] All acceptance criteria from feature spec addressed in the API
- [ ] All API endpoints return correct status codes (test with curl or browser) - [ ] All endpoints return correct status codes (test with curl or Thunder Client)
- [ ] `features/INDEX.md` status updated to "In Progress" - [ ] `features/INDEX.md` status updated to "In Progress"
- [ ] Code committed to git - [ ] Code committed to git
## Performance Checklist ## Performance Checklist
- [ ] All frequently filtered columns have indexes - [ ] All frequently filtered columns have indexes
- [ ] No N+1 queries (use Supabase joins instead of loops) - [ ] No N+1 queries (use Drizzle joins/relational queries instead of loops)
- [ ] All list queries use `.limit()` - [ ] All list endpoints paginate (limit/offset)
- [ ] Zod validation on all write endpoints - [ ] Zod validation on all write endpoints
- [ ] Slow queries cached where appropriate (optional for MVP) - [ ] Slow queries cached where appropriate (optional for MVP)
- [ ] Rate limiting on public-facing APIs (optional for MVP) - [ ] Rate limiting on public-facing routes (optional for MVP)
+57 -42
View File
@@ -1,7 +1,7 @@
--- ---
name: deploy name: deploy
description: Deploy to Vercel with production-ready checks, error tracking, and security headers setup. description: Deploy to a self-managed Linux server via git pull + rsync + systemd, with production-ready checks.
argument-hint: [feature-spec-path or "to Vercel"] argument-hint: [feature-spec-path or "to production"]
user-invocable: true user-invocable: true
allowed-tools: Read, Write, Edit, Glob, Grep, Bash, AskUserQuestion allowed-tools: Read, Write, Edit, Glob, Grep, Bash, AskUserQuestion
model: sonnet model: sonnet
@@ -10,7 +10,16 @@ model: sonnet
# DevOps Engineer # DevOps Engineer
## Role ## Role
You are an experienced DevOps Engineer handling deployment, environment setup, and production readiness. You are an experienced DevOps Engineer handling deployment, environment setup, and production readiness on a self-hosted server (no PaaS/Vercel).
## Server Layout Convention
Checkout and running service are kept separate, so a `git pull` never silently swaps out a running process's code:
```
/opt/<project>/repo git checkout only, pulled from Gitea
/opt/<project>/backend running backend service, own .env, own node_modules
/var/www/<project> built frontend static files, served by nginx
```
## Before Starting ## Before Starting
1. Read `features/INDEX.md` to know what is being deployed 1. Read `features/INDEX.md` to know what is being deployed
@@ -21,83 +30,90 @@ You are an experienced DevOps Engineer handling deployment, environment setup, a
## Workflow ## Workflow
### 1. Pre-Deployment Checks ### 1. Pre-Deployment Checks
- [ ] `npm run build` succeeds locally - [ ] `npm run build` succeeds locally (frontend and backend)
- [ ] `npm run lint` passes - [ ] `npm run lint` passes
- [ ] QA Engineer has approved the feature (check feature spec) - [ ] QA Engineer has approved the feature (check feature spec)
- [ ] No Critical/High bugs in test report - [ ] No Critical/High bugs in test report
- [ ] All environment variables documented in `.env.local.example` - [ ] All environment variables documented in `.env.example`
- [ ] No secrets committed to git - [ ] No secrets committed to git
- [ ] All database migrations applied in Supabase (if applicable) - [ ] All Drizzle migrations generated and committed
- [ ] All code committed and pushed to remote - [ ] All code committed and pushed to Gitea
### 2. Vercel Setup (first deployment only) ### 2. Server Setup (first deployment only)
Guide the user through: Guide the user through, on the target server:
- [ ] Create Vercel project: `npx vercel` or via vercel.com - [ ] `git clone <gitea-url> /opt/<project>/repo`
- [ ] Connect GitHub repository for auto-deploy on push - [ ] Create `/opt/<project>/backend`, copy backend files in, create `.env` from `.env.example`
- [ ] Add all environment variables from `.env.local.example` in Vercel Dashboard - [ ] Create PostgreSQL database + role for the app
- [ ] Build settings: Framework Preset = Next.js (auto-detected) - [ ] Create a systemd unit for the backend (`/etc/systemd/system/<project>-backend.service`), `ExecStart=node dist/server.js`, `WorkingDirectory=/opt/<project>/backend`, `EnvironmentFile=/opt/<project>/backend/.env`
- [ ] Configure domain (or use default `*.vercel.app`) - [ ] Configure nginx: reverse proxy `/api/` to the backend port, serve `/var/www/<project>` as static root for everything else
- [ ] `systemctl enable --now <project>-backend`
### 3. Deploy ### 3. Deploy (update)
- Push to main branch → Vercel auto-deploys Run from a machine with SSH access to the server:
- Or manual: `npx vercel --prod` ```bash
- Monitor build in Vercel Dashboard ssh <user>@<server> "cd /opt/<project>/repo && git pull --ff-only \
&& rsync -a --exclude='.env' --exclude='node_modules' /opt/<project>/repo/backend/ /opt/<project>/backend/ \
&& cd /opt/<project>/backend && npm ci --omit=dev && npm run db:migrate \
&& systemctl restart <project>-backend \
&& cd /opt/<project>/repo/frontend && npm ci && npm run build \
&& rsync -a --delete dist/ /var/www/<project>/"
```
- Frontend-only change → skip the backend rsync/migrate/restart block, just rebuild and rsync `dist/`
- Updates are pulled from Gitea (`git pull --ff-only`) — never edit code directly on the server
### 4. Post-Deployment Verification ### 4. Post-Deployment Verification
- [ ] Production URL loads correctly - [ ] Production URL loads correctly
- [ ] Deployed feature works as expected - [ ] Deployed feature works as expected
- [ ] Database connections work (if applicable) - [ ] Database connections work (check backend logs: `journalctl -u <project>-backend -f`)
- [ ] Authentication flows work (if applicable) - [ ] Authentication flows work (if applicable)
- [ ] No errors in browser console - [ ] No errors in browser console
- [ ] No errors in Vercel function logs - [ ] No errors in `journalctl -u <project>-backend`
### 5. Production-Ready Essentials ### 5. Production-Ready Essentials
For first deployment, guide the user through these setup guides: For first deployment, guide the user through these setup guides:
**Error Tracking (5 min):** See [error-tracking.md](../../docs/production/error-tracking.md) **Error Tracking:** See [error-tracking.md](../../docs/production/error-tracking.md)
**Security Headers (copy-paste):** See [security-headers.md](../../docs/production/security-headers.md) **Security Headers:** See [security-headers.md](../../docs/production/security-headers.md)
**Performance Check:** See [performance.md](../../docs/production/performance.md) **Performance Check:** See [performance.md](../../docs/production/performance.md)
**Database Optimization:** See [database-optimization.md](../../docs/production/database-optimization.md) **Database Optimization:** See [database-optimization.md](../../docs/production/database-optimization.md)
**Rate Limiting (optional):** See [rate-limiting.md](../../docs/production/rate-limiting.md) **Rate Limiting:** See [rate-limiting.md](../../docs/production/rate-limiting.md)
### 6. Post-Deployment Bookkeeping ### 6. Post-Deployment Bookkeeping
- Update feature spec: Add deployment section with production URL and date - Update feature spec: Add deployment section with date
- Update `features/INDEX.md`: Set status to **Deployed** - Update `features/INDEX.md`: Set status to **Deployed**
- Create git tag: `git tag -a v1.X.0-PROJ-X -m "Deploy PROJ-X: [Feature Name]"` - Create git tag: `git tag -a v1.X.0-PROJ-X -m "Deploy PROJ-X: [Feature Name]"`
- Push tag: `git push origin v1.X.0-PROJ-X` - Push tag: `git push origin v1.X.0-PROJ-X`
## Common Issues ## Common Issues
### Build fails on Vercel but works locally ### Build works locally but fails on server
- Check Node.js version (Vercel may use different version) - Check Node.js version matches between local and server (`node -v`)
- Ensure all dependencies are in package.json (not just devDependencies) - Ensure all dependencies are in `dependencies`, not just `devDependencies`, if `npm ci --omit=dev` is used for the backend
- Review Vercel build logs for specific error
### Environment variables not available ### Environment variables not available
- Verify vars are set in Vercel Dashboard (Settings → Environment Variables) - Verify `.env` exists in `/opt/<project>/backend` (it is excluded from rsync on purpose, so it survives deploys)
- Client-side vars need `NEXT_PUBLIC_` prefix - Frontend vars need the `VITE_` prefix and are baked in at build time — rebuild after changing them
- Redeploy after adding new env vars (they don't apply retroactively)
### Database connection errors ### Database connection errors
- Verify Supabase URL and anon key in Vercel env vars - Verify `DATABASE_URL` in the server's `.env`
- Check RLS policies allow the operations being attempted - Check the app's database role has the needed grants
- Verify Supabase project is not paused (free tier pauses after inactivity) - `journalctl -u <project>-backend` for the actual driver error
## Rollback Instructions ## Rollback Instructions
If production is broken: If production is broken:
1. **Immediate:** Vercel Dashboard → Deployments → Click "..." on previous working deployment → "Promote to Production" 1. **Immediate:** on the server, `cd /opt/<project>/repo && git checkout <last-good-tag>`, redo step 3's rsync/build for that commit
2. **Fix locally:** Debug the issue, `npm run build`, commit, push 2. **Fix locally:** debug the issue, `npm run build`, commit, push
3. Vercel auto-deploys the fix 3. Redeploy via step 3
## Full Deployment Checklist ## Full Deployment Checklist
- [ ] Pre-deployment checks all pass - [ ] Pre-deployment checks all pass
- [ ] Vercel build successful - [ ] Build successful (frontend + backend)
- [ ] Production URL loads and works - [ ] Production URL loads and works
- [ ] Feature tested in production environment - [ ] Feature tested in production environment
- [ ] No console errors, no Vercel log errors - [ ] No console errors, no backend log errors
- [ ] Error tracking setup (Sentry or alternative) - [ ] Error tracking setup
- [ ] Security headers configured in next.config - [ ] Security headers configured (nginx or `@fastify/helmet`)
- [ ] Lighthouse score checked (target > 90) - [ ] Lighthouse score checked (target > 90)
- [ ] Feature spec updated with deployment info - [ ] Feature spec updated with deployment info
- [ ] `features/INDEX.md` updated to Deployed - [ ] `features/INDEX.md` updated to Deployed
@@ -108,6 +124,5 @@ If production is broken:
``` ```
deploy(PROJ-X): Deploy [feature name] to production deploy(PROJ-X): Deploy [feature name] to production
- Production URL: https://your-app.vercel.app
- Deployed: YYYY-MM-DD - Deployed: YYYY-MM-DD
``` ```
+14 -14
View File
@@ -1,6 +1,6 @@
--- ---
name: frontend name: frontend
description: Build UI components with React, Next.js, Tailwind CSS, and shadcn/ui. Use after architecture is designed. description: Build UI components with React, Vite, Tailwind CSS, and shadcn/ui. Use after architecture is designed.
argument-hint: [feature-spec-path] argument-hint: [feature-spec-path]
user-invocable: true user-invocable: true
context: fork context: fork
@@ -11,15 +11,15 @@ model: opus
# Frontend Developer # Frontend Developer
## Role ## Role
You are an experienced Frontend Developer. You read feature specs + tech design and implement the UI using React, Next.js, Tailwind CSS, and shadcn/ui. You are an experienced Frontend Developer. You read feature specs + tech design and implement the UI using React, Vite, Tailwind CSS, and shadcn/ui.
## Before Starting ## Before Starting
1. Read `features/INDEX.md` for project context 1. Read `features/INDEX.md` for project context
2. Read the feature spec referenced by the user (including Tech Design section) 2. Read the feature spec referenced by the user (including Tech Design section)
3. Check installed shadcn/ui components: `ls src/components/ui/` 3. Check installed shadcn/ui components: `ls frontend/src/components/ui/`
4. Check existing custom components: `ls src/components/*.tsx 2>/dev/null` 4. Check existing custom components: `ls frontend/src/components/*.tsx 2>/dev/null`
5. Check existing hooks: `ls src/hooks/ 2>/dev/null` 5. Check existing hooks: `ls frontend/src/hooks/ 2>/dev/null`
6. Check existing pages: `ls src/app/` 6. Check existing pages/routes: `ls frontend/src/`
## Workflow ## Workflow
@@ -43,19 +43,19 @@ If no design specs exist, ask the user:
- Accessibility requirements beyond defaults (WCAG 2.1 AA)? - Accessibility requirements beyond defaults (WCAG 2.1 AA)?
### 4. Implement Components ### 4. Implement Components
- Create components in `/src/components/` - Create components in `frontend/src/components/`
- ALWAYS use shadcn/ui for standard UI elements (check `src/components/ui/` first!) - ALWAYS use shadcn/ui for standard UI elements (check `frontend/src/components/ui/` first!)
- If a shadcn component is missing, install it: `npx shadcn@latest add <name> --yes` - If a shadcn component is missing, install it: `npx shadcn@latest add <name> --yes` (run inside `frontend/`)
- Only create custom components as compositions of shadcn primitives - Only create custom components as compositions of shadcn primitives
- Use Tailwind CSS for all styling - Use Tailwind CSS for all styling
### 5. Integrate into Pages ### 5. Integrate into Pages
- Add components to pages in `/src/app/` - Add components to routes/pages in `frontend/src/`
- Set up routing if needed - Set up routing (`react-router-dom`) if needed
- Connect to backend APIs or localStorage as specified in tech design - Connect to backend APIs (`/api/**`, proxied by Vite in dev) or localStorage as specified in tech design
### 6. User Review ### 6. User Review
- Tell the user to test in browser (localhost:3000) - Tell the user to test in browser (localhost:5173)
- Ask: "Does the UI look right? Any changes needed?" - Ask: "Does the UI look right? Any changes needed?"
- Iterate based on feedback - Iterate based on feedback
@@ -64,7 +64,7 @@ If your context was compacted mid-task:
1. Re-read the feature spec you're implementing 1. Re-read the feature spec you're implementing
2. Re-read `features/INDEX.md` for current status 2. Re-read `features/INDEX.md` for current status
3. Run `git diff` to see what you've already changed 3. Run `git diff` to see what you've already changed
4. Run `git ls-files src/components/ | head -20` to see current component state 4. Run `git ls-files frontend/src/components/ | head -20` to see current component state
5. Continue from where you left off - don't restart or duplicate work 5. Continue from where you left off - don't restart or duplicate work
## After Completion: Backend & QA Handoff ## After Completion: Backend & QA Handoff
+3 -3
View File
@@ -31,9 +31,9 @@ Read these files to understand where the project stands:
- Deployment section exists (added by /deploy) - Deployment section exists (added by /deploy)
4. **Check Codebase:** Quick scan of what's been built 4. **Check Codebase:** Quick scan of what's been built
- `ls src/components/*.tsx 2>/dev/null` → Custom components - `ls frontend/src/components/*.tsx 2>/dev/null` → Custom components
- `ls src/app/api/ 2>/dev/null` → API routes - `ls backend/src/routes/ 2>/dev/null` → API routes
- `ls src/components/ui/` → Installed shadcn components - `ls frontend/src/components/ui/` → Installed shadcn components
### Step 2: Determine Next Action ### Step 2: Determine Next Action
+1 -1
View File
@@ -8,7 +8,7 @@ Add this section to the END of the feature spec `/features/PROJ-X.md`:
## QA Test Results ## QA Test Results
**Tested:** YYYY-MM-DD **Tested:** YYYY-MM-DD
**App URL:** http://localhost:3000 **App URL:** http://localhost:5173
**Tester:** QA Engineer (AI) **Tester:** QA Engineer (AI)
### Acceptance Criteria Status ### Acceptance Criteria Status
+2 -2
View File
@@ -100,8 +100,8 @@ feat: Initialize project - PRD and X feature specifications
Use this mode when the project already has a PRD and the user wants to add a new feature. Use this mode when the project already has a PRD and the user wants to add a new feature.
### Phase 1: Understand the Feature ### Phase 1: Understand the Feature
1. Check existing components: `git ls-files src/components/` 1. Check existing components: `git ls-files frontend/src/components/`
2. Check existing APIs: `git ls-files src/app/api/` 2. Check existing APIs: `git ls-files backend/src/routes/`
3. Ensure you are not duplicating an existing feature 3. Ensure you are not duplicating an existing feature
Ask the user interactive questions to clarify: Ask the user interactive questions to clarify:
+5 -2
View File
@@ -11,12 +11,15 @@
- As a [user type], I want to [action] so that [goal] - As a [user type], I want to [action] so that [goal]
## Acceptance Criteria ## Acceptance Criteria
_Each one must be testable by a QA engineer without asking a follow-up question._
- [ ] Criterion 1 - [ ] Criterion 1
- [ ] Criterion 2 - [ ] Criterion 2
## Edge Cases ## Edge Cases
- What happens when...? _Concrete failure/boundary scenarios, not rhetorical questions — state the expected behavior._
- How do we handle...? - Duplicate input: [expected behavior]
- Network/API failure mid-action: [expected behavior]
- Concurrent edits by two users: [expected behavior]
## Technical Requirements (optional) ## Technical Requirements (optional)
- Performance: < 200ms response time - Performance: < 200ms response time
-9
View File
@@ -1,9 +0,0 @@
# Supabase (Optional - remove if not using backend)
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url_here
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key_here
# Add more environment variables as needed
# STRIPE_SECRET_KEY=sk_...
# SMTP_HOST=smtp.sendgrid.net
# SMTP_USER=your_smtp_user
# SMTP_PASS=your_smtp_password
+8 -21
View File
@@ -1,20 +1,14 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies # dependencies
/node_modules node_modules/
/.pnp
.pnp.js
.yarn/install-state.gz
# testing # build output
/coverage frontend/dist/
backend/dist/
# next.js # env files
/.next/ .env
/out/ .env.local
!*.env.example
# production
/build
# misc # misc
.DS_Store .DS_Store
@@ -25,15 +19,8 @@ npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# claude code personal settings # claude code personal settings
.claude/settings.local.json .claude/settings.local.json
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts
+33 -18
View File
@@ -1,30 +1,37 @@
# AI Coding Starter Kit # AI Coding Starter Kit
> A Next.js template with an AI-powered development workflow using specialized skills for Requirements, Architecture, Frontend, Backend, QA, and Deployment. > A self-hosted Vite/React + Fastify template with an AI-powered development workflow using specialized skills for Requirements, Architecture, Frontend, Backend, QA, and Deployment.
## Tech Stack ## Tech Stack
- **Framework:** Next.js 16 (App Router), TypeScript - **Frontend:** Vite + React 19 + TypeScript
- **Styling:** Tailwind CSS + shadcn/ui (copy-paste components) - **Styling:** Tailwind CSS + shadcn/ui (copy-paste components)
- **Backend:** Supabase (PostgreSQL + Auth + Storage) - optional - **Backend:** Fastify + Drizzle ORM + PostgreSQL, own JWT auth
- **Deployment:** Vercel - **Deployment:** self-hosted Linux server — git pull + rsync + systemd, updates pulled from Gitea
- **Validation:** Zod + react-hook-form - **Validation:** Zod + react-hook-form
- **State:** React useState / Context API - **State:** React useState / Context API
## Project Structure ## Project Structure
``` ```
src/ frontend/
app/ Pages (Next.js App Router) src/
components/ main.tsx, App.tsx Entry points
ui/ shadcn/ui components (NEVER recreate these) components/
hooks/ Custom React hooks ui/ shadcn/ui components (NEVER recreate these)
lib/ Utilities (supabase.ts, utils.ts) hooks/ Custom React hooks
features/ Feature specifications (PROJ-X-name.md) lib/ Utilities (utils.ts)
INDEX.md Feature status overview backend/
src/
server.ts Fastify app entry
routes/ Route handlers
db/ Drizzle schema + connection
drizzle/ Generated migrations
features/ Feature specifications (PROJ-X-name.md)
INDEX.md Feature status overview
docs/ docs/
PRD.md Product Requirements Document PRD.md Product Requirements Document
production/ Production guides (Sentry, security, performance) production/ Production guides (error tracking, security, performance)
``` ```
## Development Workflow ## Development Workflow
@@ -32,9 +39,9 @@ docs/
1. `/requirements` - Create feature spec from idea 1. `/requirements` - Create feature spec from idea
2. `/architecture` - Design tech architecture (PM-friendly, no code) 2. `/architecture` - Design tech architecture (PM-friendly, no code)
3. `/frontend` - Build UI components (shadcn/ui first!) 3. `/frontend` - Build UI components (shadcn/ui first!)
4. `/backend` - Build APIs, database, RLS policies 4. `/backend` - Build APIs, database schema, auth checks
5. `/qa` - Test against acceptance criteria + security audit 5. `/qa` - Test against acceptance criteria + security audit
6. `/deploy` - Deploy to Vercel + production-ready checks 6. `/deploy` - Deploy to the server + production-ready checks
## Feature Tracking ## Feature Tracking
@@ -47,14 +54,22 @@ All features tracked in `features/INDEX.md`. Every skill reads it at start and u
- **Single Responsibility:** One feature per spec file - **Single Responsibility:** One feature per spec file
- **shadcn/ui first:** NEVER create custom versions of installed shadcn components - **shadcn/ui first:** NEVER create custom versions of installed shadcn components
- **Human-in-the-loop:** All workflows have user approval checkpoints - **Human-in-the-loop:** All workflows have user approval checkpoints
- **No RLS backstop:** authorization/ownership checks are explicit in Fastify route handlers, not enforced by the database
## Build & Test Commands ## Build & Test Commands
```bash ```bash
npm run dev # Development server (localhost:3000) # frontend/
npm run dev # Vite dev server (localhost:5173)
npm run build # Production build npm run build # Production build
npm run lint # ESLint npm run lint # ESLint
npm run start # Production server
# backend/
npm run dev # tsx watch (localhost:4000)
npm run build # tsc build
npm run start # node dist/server.js
npm run db:generate # generate Drizzle migration
npm run db:migrate # apply migrations
``` ```
## Product Context ## Product Context
+53 -53
View File
@@ -1,6 +1,6 @@
# AI Coding Starter Kit # AI Coding Starter Kit
> Build production-ready web apps faster with AI-powered Skills handling Requirements, Architecture, Development, QA, and Deployment. > Build production-ready web apps faster with AI-powered Skills handling Requirements, Architecture, Development, QA, and Deployment. Self-hosted, no vendor lock-in — Vite/React + Fastify/PostgreSQL, deployed via git pull + rsync + systemd.
This template uses [Claude Code](https://docs.anthropic.com/en/docs/claude-code) with modern Skills, Rules, and Sub-Agents to provide a complete AI-powered development workflow. This template uses [Claude Code](https://docs.anthropic.com/en/docs/claude-code) with modern Skills, Rules, and Sub-Agents to provide a complete AI-powered development workflow.
@@ -9,30 +9,28 @@ This template uses [Claude Code](https://docs.anthropic.com/en/docs/claude-code)
### 1. Clone & Install ### 1. Clone & Install
```bash ```bash
git clone https://github.com/YOUR_USERNAME/ai-coding-starter-kit.git my-project git clone <your-gitea-url>/ai-coding-starter-kit.git my-project
cd my-project cd my-project
npm install cd frontend && npm install && cd ..
cd backend && npm install && cd ..
``` ```
### 2. (Optional) Supabase Setup ### 2. Set Up PostgreSQL
If you need a backend: 1. Create a local PostgreSQL database and role for the app
2. Copy `backend/.env.example` to `backend/.env`
3. Set `DATABASE_URL` and `JWT_SECRET`
4. Run `cd backend && npm run db:generate && npm run db:migrate`
1. Create Supabase Project: [supabase.com](https://supabase.com) Skip the backend entirely if you're building frontend-only (landing pages, portfolios, etc.) — just run the frontend.
2. Copy `.env.local.example` to `.env.local`
3. Add your Supabase credentials
4. Uncomment the Supabase client in `src/lib/supabase.ts`
Skip this step if you're building frontend-only (landing pages, portfolios, etc.)
### 3. Start Development ### 3. Start Development
```bash ```bash
npm run dev cd backend && npm run dev # localhost:4000
cd frontend && npm run dev # localhost:5173, proxies /api to the backend
``` ```
Open [http://localhost:3000](http://localhost:3000) in your browser.
### 4. Initialize Your Project ### 4. Initialize Your Project
Open Claude Code and describe your project. The `/requirements` skill automatically detects that this is a fresh project and enters **Init Mode**: Open Claude Code and describe your project. The `/requirements` skill automatically detects that this is a fresh project and enters **Init Mode**:
@@ -61,7 +59,7 @@ After project initialization, build features one at a time using skills:
/frontend Build the UI for features/PROJ-1-user-auth.md /frontend Build the UI for features/PROJ-1-user-auth.md
/backend Build the API for features/PROJ-1-user-auth.md /backend Build the API for features/PROJ-1-user-auth.md
/qa Test features/PROJ-1-user-auth.md /qa Test features/PROJ-1-user-auth.md
/deploy Deploy to Vercel /deploy Deploy to your server
``` ```
Each skill suggests the next step when it finishes. Handoffs are always user-initiated. Each skill suggests the next step when it finishes. Handoffs are always user-initiated.
@@ -77,9 +75,9 @@ To add more features later, run `/requirements` again - it detects the existing
| Requirements Engineer | `/requirements` | Creates feature specs with user stories, acceptance criteria, edge cases | | Requirements Engineer | `/requirements` | Creates feature specs with user stories, acceptance criteria, edge cases |
| Solution Architect | `/architecture` | Designs PM-friendly tech architecture (no code, only high-level design) | | Solution Architect | `/architecture` | Designs PM-friendly tech architecture (no code, only high-level design) |
| Frontend Developer | `/frontend` | Builds UI with React, Tailwind CSS, and shadcn/ui | | Frontend Developer | `/frontend` | Builds UI with React, Tailwind CSS, and shadcn/ui |
| Backend Developer | `/backend` | Builds APIs, database schemas, RLS policies with Supabase | | Backend Developer | `/backend` | Builds APIs, database schema, and auth with Fastify + Drizzle + PostgreSQL |
| QA Engineer | `/qa` | Tests features against acceptance criteria + security audit | | QA Engineer | `/qa` | Tests features against acceptance criteria + security audit |
| DevOps | `/deploy` | Deploys to Vercel with production-ready checks | | DevOps | `/deploy` | Deploys to your server (git pull + rsync + systemd) with production-ready checks |
| Help | `/help` | Context-aware guide: shows where you are and what to do next | | Help | `/help` | Context-aware guide: shows where you are and what to do next |
### How Skills Work ### How Skills Work
@@ -99,7 +97,7 @@ To add more features later, run `/requirements` again - it detects the existing
3. Build /frontend --> UI components implemented 3. Build /frontend --> UI components implemented
/backend --> APIs + database (if needed) /backend --> APIs + database (if needed)
4. Test /qa --> Test results added to feature spec 4. Test /qa --> Test results added to feature spec
5. Ship /deploy --> Deployed to Vercel 5. Ship /deploy --> Deployed to your server
``` ```
### Feature Tracking ### Feature Tracking
@@ -119,12 +117,13 @@ Every skill reads this file at start and updates it when done, preventing duplic
| Category | Tool | Why? | | Category | Tool | Why? |
|----------|------|------| |----------|------|------|
| **Framework** | Next.js 16 | React + Server Components + App Router | | **Frontend** | Vite + React 19 | Fast dev server, no framework lock-in |
| **Language** | TypeScript | Type safety | | **Language** | TypeScript | Type safety |
| **Styling** | Tailwind CSS | Utility-first CSS | | **Styling** | Tailwind CSS | Utility-first CSS |
| **UI Library** | shadcn/ui | Copy-paste, customizable components | | **UI Library** | shadcn/ui | Copy-paste, customizable components |
| **Backend** | Supabase (optional) | PostgreSQL + Auth + Storage + Realtime | | **Backend** | Fastify + Drizzle + PostgreSQL | Lightweight, typed, self-hostable |
| **Deployment** | Vercel | Zero-config Next.js hosting | | **Auth** | Own JWT (`@fastify/jwt`) | No third-party auth dependency |
| **Deployment** | git pull + rsync + systemd | Runs on any Linux server, updates pulled from Gitea |
| **Validation** | Zod | Runtime type validation | | **Validation** | Zod | Runtime type validation |
--- ---
@@ -140,7 +139,7 @@ ai-coding-starter-kit/
| +-- rules/ <-- Auto-applied coding rules | +-- rules/ <-- Auto-applied coding rules
| | +-- general.md Git workflow, feature tracking | | +-- general.md Git workflow, feature tracking
| | +-- frontend.md shadcn/ui, component standards | | +-- frontend.md shadcn/ui, component standards
| | +-- backend.md RLS, validation, queries | | +-- backend.md Validation, queries, migrations
| | +-- security.md Secrets, headers, auth | | +-- security.md Secrets, headers, auth
| +-- skills/ <-- Invocable workflows (/command) | +-- skills/ <-- Invocable workflows (/command)
| | +-- requirements/SKILL.md /requirements | | +-- requirements/SKILL.md /requirements
@@ -160,18 +159,23 @@ ai-coding-starter-kit/
+-- docs/ +-- docs/
| +-- PRD.md <-- Product Requirements Document | +-- PRD.md <-- Product Requirements Document
| +-- production/ <-- Production setup guides | +-- production/ <-- Production setup guides
| +-- error-tracking.md Sentry setup (5 min) | +-- error-tracking.md Error tracking setup
| +-- security-headers.md XSS/Clickjacking protection | +-- security-headers.md XSS/Clickjacking protection
| +-- performance.md Lighthouse, optimization | +-- performance.md Lighthouse, optimization
| +-- database-optimization.md Indexing, N+1, caching | +-- database-optimization.md Indexing, N+1, caching
| +-- rate-limiting.md Upstash Redis | +-- rate-limiting.md @fastify/rate-limit
+-- src/ +-- frontend/
| +-- app/ <-- Pages (Next.js App Router) | +-- src/
| +-- components/ | | +-- main.tsx, App.tsx <-- Entry points
| | +-- ui/ <-- shadcn/ui components (35+ installed) | | +-- components/ui/ <-- shadcn/ui components (35+ installed)
| +-- hooks/ <-- Custom React hooks | | +-- hooks/ <-- Custom React hooks
| +-- lib/ <-- Utilities | | +-- lib/ <-- Utilities
+-- public/ <-- Static files | +-- public/ <-- Static files
+-- backend/
+-- src/
+-- server.ts <-- Fastify entry
+-- routes/ <-- Route handlers
+-- db/ <-- Drizzle schema + connection
``` ```
--- ---
@@ -195,7 +199,7 @@ Run `/requirements` with your feature idea. The skill will:
### 3. Add shadcn/ui Components (as needed) ### 3. Add shadcn/ui Components (as needed)
35+ components are pre-installed. Add more as needed: 35+ components are pre-installed. Add more as needed (run inside `frontend/`):
```bash ```bash
npx shadcn@latest add [component-name] npx shadcn@latest add [component-name]
``` ```
@@ -203,8 +207,8 @@ npx shadcn@latest add [component-name]
### 4. Production Setup (first deployment) ### 4. Production Setup (first deployment)
When you're ready to deploy, the `/deploy` skill guides you through: When you're ready to deploy, the `/deploy` skill guides you through:
- Vercel setup and deployment - Server setup (systemd unit, nginx reverse proxy, PostgreSQL)
- Error tracking with Sentry - Error tracking
- Security headers configuration - Security headers configuration
- Performance monitoring with Lighthouse - Performance monitoring with Lighthouse
@@ -222,7 +226,7 @@ Each skill is a structured workflow that Claude Code discovers automatically. Sk
| `/requirements` | Inline | Needs live interaction with user | | `/requirements` | Inline | Needs live interaction with user |
| `/architecture` | Inline | Short output, user reviews in real-time | | `/architecture` | Inline | Short output, user reviews in real-time |
| `/frontend` | Sub-agent (forked) | Heavy file editing, lots of output | | `/frontend` | Sub-agent (forked) | Heavy file editing, lots of output |
| `/backend` | Sub-agent (forked) | Heavy file editing, SQL, API code | | `/backend` | Sub-agent (forked) | Heavy file editing, schema, route code |
| `/qa` | Sub-agent (forked) | Systematic testing, lots of output | | `/qa` | Sub-agent (forked) | Systematic testing, lots of output |
| `/deploy` | Inline | Deployment needs user oversight | | `/deploy` | Inline | Deployment needs user oversight |
| `/help` | Inline | Quick status check and guidance | | `/help` | Inline | Quick status check and guidance |
@@ -288,36 +292,32 @@ This template is designed as a starting point. Customize it for your team:
Standalone guides in `docs/production/`: Standalone guides in `docs/production/`:
| Guide | Setup Time | What It Does | | Guide | What It Does |
|-------|-----------|-------------| |-------|-------------|
| [Error Tracking](docs/production/error-tracking.md) | 5 min | Sentry integration for automatic error capture | | [Error Tracking](docs/production/error-tracking.md) | Automatic error capture setup |
| [Security Headers](docs/production/security-headers.md) | 2 min | XSS, Clickjacking, MIME sniffing protection | | [Security Headers](docs/production/security-headers.md) | XSS, Clickjacking, MIME sniffing protection |
| [Performance](docs/production/performance.md) | 10 min | Lighthouse checks, image optimization, caching | | [Performance](docs/production/performance.md) | Lighthouse checks, image optimization, caching |
| [Database Optimization](docs/production/database-optimization.md) | 15 min | Indexing, N+1 prevention, query optimization | | [Database Optimization](docs/production/database-optimization.md) | Indexing, N+1 prevention, query optimization |
| [Rate Limiting](docs/production/rate-limiting.md) | 10 min | Upstash Redis for API abuse prevention | | [Rate Limiting](docs/production/rate-limiting.md) | `@fastify/rate-limit` for API abuse prevention |
--- ---
## Scripts ## Scripts
```bash ```bash
npm run dev # Development server (localhost:3000) # frontend/
npm run dev # Vite dev server (localhost:5173)
npm run build # Production build npm run build # Production build
npm run start # Production server
npm run lint # ESLint npm run lint # ESLint
# backend/
npm run dev # tsx watch (localhost:4000)
npm run build # tsc build
npm run start # node dist/server.js
``` ```
--- ---
## Author
Created by **Alex Sprogis** – AI Product Engineer & Content Creator.
- [YouTube](https://www.youtube.com/@alex.sprogis)
- [Website](https://alexsprogis.de)
---
## License ## License
MIT License - feel free to use for your projects! MIT License - feel free to use for your projects!
+3
View File
@@ -0,0 +1,3 @@
DATABASE_URL=postgresql://user:password@localhost:5432/app
JWT_SECRET=change-me
PORT=4000
+10
View File
@@ -0,0 +1,10 @@
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
+2760
View File
File diff suppressed because it is too large. Load diff
+32
View File
@@ -0,0 +1,32 @@
{
"name": "ai-coding-starter-kit-backend",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "tsx watch src/server.ts",
"build": "tsc -p .",
"start": "node dist/server.js",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate"
},
"dependencies": {
"@fastify/cors": "^10.0.1",
"@fastify/jwt": "^10.2.2",
"@fastify/rate-limit": "^10.1.1",
"bcryptjs": "^2.4.3",
"dotenv": "^16.4.5",
"drizzle-orm": "^0.45.2",
"fastify": "^5.1.0",
"pg": "^8.13.1",
"zod": "^4.3.5"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/node": "^20",
"@types/pg": "^8.11.10",
"drizzle-kit": "^0.31.10",
"tsx": "^4.19.2",
"typescript": "^5"
}
}
+7
View File
@@ -0,0 +1,7 @@
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import * as schema from "./schema.js";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle(pool, { schema });
+8
View File
@@ -0,0 +1,8 @@
import { pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: uuid("id").primaryKey().defaultRandom(),
email: text("email").notNull().unique(),
passwordHash: text("password_hash").notNull(),
createdAt: timestamp("created_at").notNull().defaultNow(),
});
+60
View File
@@ -0,0 +1,60 @@
import type { FastifyInstance } from "fastify";
import bcrypt from "bcryptjs";
import { z } from "zod";
import { eq } from "drizzle-orm";
import { db } from "../db/index.js";
import { users } from "../db/schema.js";
const credentialsSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
export async function authRoutes(app: FastifyInstance) {
app.post("/api/auth/register", {
config: { rateLimit: { max: 5, timeWindow: "1 minute" } },
}, async (request, reply) => {
const parsed = credentialsSchema.safeParse(request.body);
if (!parsed.success) {
return reply.code(400).send({ error: parsed.error.issues[0].message });
}
const { email, password } = parsed.data;
const existing = await db.select().from(users).where(eq(users.email, email)).limit(1);
if (existing.length > 0) {
return reply.code(409).send({ error: "Email already registered" });
}
const passwordHash = await bcrypt.hash(password, 12);
const [user] = await db
.insert(users)
.values({ email, passwordHash })
.returning({ id: users.id, email: users.email });
const token = app.jwt.sign({ sub: user.id, email: user.email });
return reply.code(201).send({ token });
});
app.post("/api/auth/login", {
config: { rateLimit: { max: 5, timeWindow: "1 minute" } },
}, async (request, reply) => {
const parsed = credentialsSchema.safeParse(request.body);
if (!parsed.success) {
return reply.code(400).send({ error: parsed.error.issues[0].message });
}
const { email, password } = parsed.data;
const [user] = await db.select().from(users).where(eq(users.email, email)).limit(1);
if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
return reply.code(401).send({ error: "Invalid email or password" });
}
const token = app.jwt.sign({ sub: user.id, email: user.email });
return reply.send({ token });
});
app.get("/api/auth/me", { preHandler: app.authenticate }, async (request) => {
const { sub, email } = request.user;
return { id: sub, email };
});
}
+5
View File
@@ -0,0 +1,5 @@
import type { FastifyInstance } from "fastify";
export async function healthRoutes(app: FastifyInstance) {
app.get("/api/health", async () => ({ status: "ok" }));
}
+30
View File
@@ -0,0 +1,30 @@
import "dotenv/config";
import Fastify, { type FastifyRequest, type FastifyReply } from "fastify";
import cors from "@fastify/cors";
import jwt from "@fastify/jwt";
import rateLimit from "@fastify/rate-limit";
import { healthRoutes } from "./routes/health.js";
import { authRoutes } from "./routes/auth.js";
const app = Fastify({ logger: true });
await app.register(cors, { origin: true });
await app.register(rateLimit, { max: 100, timeWindow: "1 minute" });
await app.register(jwt, { secret: process.env.JWT_SECRET! });
app.decorate("authenticate", async (request: FastifyRequest, reply: FastifyReply) => {
try {
await request.jwtVerify();
} catch {
reply.code(401).send({ error: "Unauthorized" });
}
});
await app.register(healthRoutes);
await app.register(authRoutes);
const port = Number(process.env.PORT ?? 4000);
app.listen({ port, host: "0.0.0.0" }).catch((err) => {
app.log.error(err);
process.exit(1);
});
+15
View File
@@ -0,0 +1,15 @@
import "fastify";
import "@fastify/jwt";
declare module "fastify" {
interface FastifyInstance {
authenticate(request: FastifyRequest, reply: FastifyReply): Promise<void>;
}
}
declare module "@fastify/jwt" {
interface FastifyJWT {
payload: { sub: string; email: string };
user: { sub: string; email: string };
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true
},
"include": ["src"]
}
+9 -5
View File
@@ -1,10 +1,12 @@
# Product Requirements Document # Product Requirements Document
> Filled out by `/requirements` in Init Mode, or manually before the first `/requirements` run. Keep every section concrete enough that a stranger could read it and know what NOT to build.
## Vision ## Vision
_Describe what you are building and why._ _2-3 sentences: what is this product, who is it for, why does it need to exist. Not a feature list — the problem it solves._
## Target Users ## Target Users
_Who will use this product? Describe their needs and pain points._ _Name the actual user type(s), not "everyone". For each: their concrete pain point today, and what they're using instead of this product._
## Core Features (Roadmap) ## Core Features (Roadmap)
@@ -15,14 +17,16 @@ _Who will use this product? Describe their needs and pain points._
| P1 | _Feature 3_ | Planned | | P1 | _Feature 3_ | Planned |
| P2 | _Feature 4_ | Planned | | P2 | _Feature 4_ | Planned |
P0 = cannot ship without it. P1 = next after MVP validates. P2 = explicitly deferred, not "maybe never."
## Success Metrics ## Success Metrics
_How will you measure success? (e.g., user signups, retention, task completion rate)_ _Concrete, measurable, with a number or threshold — not "users are happy". E.g. "50 signups in first month", "task completion rate > 80%"._
## Constraints ## Constraints
_Budget, timeline, technical limitations, team size._ _Real limits that shape decisions: timeline, budget, team size, must-run-on-existing-server, no third-party SaaS dependencies, etc._
## Non-Goals ## Non-Goals
_What are you explicitly NOT building in this version?_ _Explicitly out of scope for this version — the things people will ask for that you're deliberately not building yet, and why._
--- ---
+45 -41
View File
@@ -4,17 +4,20 @@
Create indexes on columns used in WHERE, ORDER BY, or JOIN clauses: Create indexes on columns used in WHERE, ORDER BY, or JOIN clauses:
```sql ```ts
-- Without index: ~500ms at 100k rows // backend/src/db/schema.ts
SELECT * FROM tasks WHERE user_id = 'abc123' ORDER BY created_at DESC; export const tasks = pgTable("tasks", {
id: uuid("id").primaryKey().defaultRandom(),
-- After creating index: <10ms userId: uuid("user_id").notNull(),
CREATE INDEX idx_tasks_user_id_created ON tasks(user_id, created_at DESC); createdAt: timestamp("created_at").notNull().defaultNow(),
}, (table) => ({
userCreatedIdx: index("idx_tasks_user_id_created").on(table.userId, table.createdAt.desc()),
}));
``` ```
**Rule of thumb:** If a column appears in WHERE or ORDER BY and the table will have >1000 rows, add an index. **Rule of thumb:** If a column appears in WHERE or ORDER BY and the table will have >1000 rows, add an index.
Always include indexes in your migration SQL alongside CREATE TABLE. Always include indexes in the schema alongside the table definition, so they ship with the same migration.
## 2. Avoid N+1 Queries ## 2. Avoid N+1 Queries
@@ -22,18 +25,21 @@ The most common performance problem with ORMs and query builders:
```typescript ```typescript
// Bad: N+1 (1 query for users + N queries for tasks) // Bad: N+1 (1 query for users + N queries for tasks)
const { data: users } = await supabase.from('users').select('*') const allUsers = await db.select().from(users);
for (const user of users) { for (const user of allUsers) {
const { data: tasks } = await supabase const userTasks = await db.select().from(tasks).where(eq(tasks.userId, user.id));
.from('tasks')
.select('*')
.eq('user_id', user.id)
} }
// Good: Single query with join (1 query total) // Good: single query with a join
const { data } = await supabase const rows = await db
.from('users') .select()
.select('*, tasks(*)') .from(users)
.leftJoin(tasks, eq(tasks.userId, users.id));
// Or with Drizzle's relational query API (if relations are defined)
const usersWithTasks = await db.query.users.findMany({
with: { tasks: true },
});
``` ```
## 3. Always Limit Results ## 3. Always Limit Results
@@ -41,34 +47,32 @@ const { data } = await supabase
Never return unbounded results from the database: Never return unbounded results from the database:
```typescript ```typescript
// Bad: Returns ALL rows // Bad: returns ALL rows
const { data } = await supabase.from('tasks').select('*') const rows = await db.select().from(tasks);
// Good: Returns max 50 rows // Good: capped
const { data } = await supabase.from('tasks').select('*').limit(50) const rows = await db.select().from(tasks).limit(50);
// Better: Paginated // Better: paginated
const { data } = await supabase const rows = await db.select().from(tasks).limit(50).offset(page * 50);
.from('tasks')
.select('*')
.range(0, 49) // First 50 rows
``` ```
## 4. Caching Strategy ## 4. Caching Strategy
For data that changes rarely (dashboard stats, config, categories): For data that changes rarely (dashboard stats, config, categories), cache in the backend process (or a small in-memory LRU) rather than hitting Postgres on every request:
```typescript ```typescript
import { unstable_cache } from 'next/cache' let cachedCategories: Category[] | null = null;
let cachedAt = 0;
export const getCategories = unstable_cache( export async function getCategories() {
async () => { if (cachedCategories && Date.now() - cachedAt < 3600_000) {
const { data } = await supabase.from('categories').select('*') return cachedCategories;
return data }
}, cachedCategories = await db.select().from(categories);
['categories'], // Cache key cachedAt = Date.now();
{ revalidate: 3600 } // Refresh every hour return cachedCategories;
) }
``` ```
**When to cache:** **When to cache:**
@@ -78,14 +82,14 @@ export const getCategories = unstable_cache(
**When NOT to cache:** **When NOT to cache:**
- User-specific data that changes frequently - User-specific data that changes frequently
- Real-time data (use Supabase Realtime instead) - Data that needs to be live across multiple backend instances (use a shared cache like Redis instead of in-process memory)
## 5. Select Only What You Need ## 5. Select Only What You Need
```typescript ```typescript
// Bad: Fetches all columns // Bad: fetches all columns
const { data } = await supabase.from('users').select('*') const rows = await db.select().from(users);
// Good: Fetches only needed columns // Good: fetches only needed columns
const { data } = await supabase.from('users').select('id, name, avatar_url') const rows = await db.select({ id: users.id, email: users.email }).from(users);
``` ```
+34 -18
View File
@@ -1,32 +1,48 @@
# Error Tracking Setup (Sentry) # Error Tracking Setup (Sentry, self-hosted friendly)
Track production errors automatically so you know about issues before your users report them. Track production errors automatically so you know about issues before your users report them. Sentry's SaaS free tier works fine even on a self-hosted app — it's just a DSN endpoint, no coupling to any specific host.
## Setup (5 minutes) ## Setup (5 minutes)
### 1. Create Sentry Account ### 1. Create Sentry Account
- Go to [sentry.io](https://sentry.io) (free tier available for small apps) - Go to [sentry.io](https://sentry.io) (free tier available for small apps)
- Create a new project and select "Next.js" - Create a project of type "Node" for the backend and one of type "React" for the frontend (or one combined project)
### 2. Install Next.js Integration ### 2. Install SDKs
```bash ```bash
npx @sentry/wizard@latest -i nextjs # backend/
``` npm install @sentry/node
This automatically:
- Installs `@sentry/nextjs`
- Creates `sentry.client.config.ts` and `sentry.server.config.ts`
- Updates `next.config.ts` with Sentry webpack plugin
### 3. Add Environment Variables # frontend/
Add to `.env.local` (local) and Vercel Dashboard (production): npm install @sentry/react
```
### 3. Initialize
Backend (`backend/src/server.ts`, before anything else runs):
```ts
import * as Sentry from "@sentry/node";
Sentry.init({ dsn: process.env.SENTRY_DSN });
```
Frontend (`frontend/src/main.tsx`, before rendering):
```ts
import * as Sentry from "@sentry/react";
Sentry.init({ dsn: import.meta.env.VITE_SENTRY_DSN });
```
### 4. Add Environment Variables
Backend `.env`:
```bash ```bash
SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx
NEXT_PUBLIC_SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx ```
SENTRY_AUTH_TOKEN=sntrys_xxx # For source maps upload Frontend `.env` (baked in at build time, so rebuild after changing it):
```bash
VITE_SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx
``` ```
### 4. Verify Setup ### 5. Verify Setup
Trigger a test error and check Sentry Dashboard: Trigger a test error and check the Sentry Dashboard:
```typescript ```typescript
// Temporary test - remove after verification // Temporary test - remove after verification
throw new Error("Sentry test error") throw new Error("Sentry test error")
@@ -34,10 +50,10 @@ throw new Error("Sentry test error")
## What You Get ## What You Get
- Automatic error capture (client + server) - Automatic error capture (client + server)
- Stack traces with source maps - Stack traces with source maps (upload via `@sentry/vite-plugin` / `@sentry/esbuild-plugin` in the build step)
- Error grouping and deduplication - Error grouping and deduplication
- Email alerts for new errors - Email alerts for new errors
- Performance monitoring (optional) - Performance monitoring (optional)
## Alternative ## Alternative
**Vercel Error Tracking** - Built-in, simpler, but fewer features. Available in Vercel Dashboard under "Monitoring". Self-hosted **GlitchTip** — an open-source, Sentry-protocol-compatible error tracker you can run on your own server if sending errors to a third-party SaaS is not acceptable.
+17 -27
View File
@@ -15,21 +15,23 @@
// Bad - unoptimized, no lazy loading // Bad - unoptimized, no lazy loading
<img src="/large-image.jpg" /> <img src="/large-image.jpg" />
// Good - Next.js Image component // Good - explicit dimensions + native lazy loading
import Image from 'next/image' <img src="/large-image.jpg" width={800} height={600} loading="lazy" alt="Description" />
<Image src="/large-image.jpg" width={800} height={600} alt="Description" />
``` ```
Next.js Image automatically: resizes, lazy-loads, serves WebP format. Pre-resize/compress source images (e.g. with `sharp` in a build step, or an image CDN) — Vite does not do this automatically like Next.js Image did.
### Large JavaScript Bundle ### Large JavaScript Bundle
Use dynamic imports for heavy components that aren't needed on initial load: Use dynamic imports for heavy components that aren't needed on initial load:
```tsx ```tsx
import dynamic from 'next/dynamic' import { lazy, Suspense } from "react";
const HeavyChart = dynamic(() => import('./HeavyChart'), { const HeavyChart = lazy(() => import("./HeavyChart"));
loading: () => <p>Loading chart...</p>,
}) <Suspense fallback={<p>Loading chart...</p>}>
<HeavyChart />
</Suspense>
``` ```
Vite code-splits dynamic imports automatically — check `npm run build`'s output chunk sizes.
### Missing Loading States ### Missing Loading States
Always show feedback during data fetching: Always show feedback during data fetching:
@@ -41,27 +43,15 @@ if (isLoading) return <Skeleton className="h-12 w-full" />
``` ```
### No Caching Strategy ### No Caching Strategy
Cache slow database queries with `unstable_cache`: Cache slow backend queries in-process (see [database-optimization.md](database-optimization.md)) rather than re-querying Postgres on every request for rarely-changing data.
```typescript
import { unstable_cache } from 'next/cache'
export const getStats = unstable_cache(
async () => {
const { data } = await supabase.from('stats').select('*')
return data
},
['dashboard-stats'],
{ revalidate: 3600 } // Refresh every hour
)
```
## Quick Wins Checklist ## Quick Wins Checklist
- [ ] All images use `next/image` component - [ ] Images have explicit width/height and `loading="lazy"` where below the fold
- [ ] Heavy components use dynamic imports - [ ] Heavy components use `React.lazy` + `Suspense`
- [ ] Loading states show skeleton/spinner - [ ] Loading states show skeleton/spinner
- [ ] Fonts loaded with `next/font` - [ ] Fonts loaded via `@font-face`/Google Fonts `<link>`, with `font-display: swap`
- [ ] No unnecessary client-side JavaScript (`"use client"` only when needed) - [ ] Production build (`npm run build`) checked for unexpectedly large chunks
## Automated Monitoring ## Automated Monitoring
- **Vercel Analytics** - Automatic on Pro plan, shows Core Web Vitals - Self-hosted **Plausible** or **Umami** for privacy-friendly traffic analytics
- **Vercel Speed Insights** - Real user performance data - `web-vitals` npm package to report Core Web Vitals from real users to your own backend/logging
+26 -72
View File
@@ -1,92 +1,46 @@
# Rate Limiting # Rate Limiting
Prevent abuse, DDoS attacks, and excessive API usage. Prevent abuse, brute-force, and excessive API usage.
## When to Add Rate Limiting ## When to Add Rate Limiting
- **MVP:** Optional (focus on features first) - **MVP:** Optional (focus on features first)
- **Production with users:** Recommended on auth endpoints and public APIs - **Production with users:** Recommended on auth endpoints and public APIs
- **Public-facing APIs:** Required - **Public-facing APIs:** Required
## Setup with Upstash Redis ## Setup with @fastify/rate-limit
### 1. Install Dependencies No external service needed — in-memory by default, works fine for a single backend instance.
### 1. Install Dependency
```bash ```bash
npm install @upstash/ratelimit @upstash/redis cd backend && npm install @fastify/rate-limit
``` ```
### 2. Create Upstash Account ### 2. Register Globally
- Go to [upstash.com](https://upstash.com) (free tier: 10k requests/day)
- Create a Redis database
- Copy REST URL and token
### 3. Add Environment Variables
```bash
# .env.local
UPSTASH_REDIS_REST_URL=https://xxx.upstash.io
UPSTASH_REDIS_REST_TOKEN=xxx
```
### 4. Create Rate Limiter
```typescript ```typescript
// src/lib/rate-limit.ts // backend/src/server.ts
import { Ratelimit } from '@upstash/ratelimit' import rateLimit from "@fastify/rate-limit";
import { Redis } from '@upstash/redis'
export const ratelimit = new Ratelimit({ await app.register(rateLimit, {
redis: Redis.fromEnv(), max: 100,
limiter: Ratelimit.slidingWindow(10, '10 s'), // 10 requests per 10 seconds timeWindow: "1 minute",
}) });
``` ```
### 5. Use in API Routes ### 3. Tighter Limits on Specific Routes
```typescript ```typescript
// src/app/api/example/route.ts // backend/src/routes/auth.ts
import { ratelimit } from '@/lib/rate-limit' app.post("/api/auth/login", {
import { NextRequest, NextResponse } from 'next/server' config: {
rateLimit: { max: 5, timeWindow: "1 minute" },
export async function POST(request: NextRequest) { },
const ip = request.headers.get('x-forwarded-for') ?? 'anonymous' }, async (request, reply) => {
const { success, limit, remaining } = await ratelimit.limit(ip) // login handler
});
if (!success) {
return NextResponse.json(
{ error: 'Too many requests' },
{
status: 429,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
},
}
)
}
// Process request normally...
}
``` ```
### 6. Use in Middleware (Global) ### 4. Response on Limit Exceeded
```typescript `@fastify/rate-limit` returns HTTP 429 automatically with `Retry-After` and `X-RateLimit-*` headers — no manual handling needed.
// middleware.ts
import { ratelimit } from '@/lib/rate-limit'
import { NextRequest, NextResponse } from 'next/server'
export async function middleware(request: NextRequest) {
// Only rate limit API routes
if (request.nextUrl.pathname.startsWith('/api/')) {
const ip = request.headers.get('x-forwarded-for') ?? 'anonymous'
const { success } = await ratelimit.limit(ip)
if (!success) {
return NextResponse.json({ error: 'Too Many Requests' }, { status: 429 })
}
}
}
export const config = {
matcher: '/api/:path*',
}
```
## Recommended Limits ## Recommended Limits
@@ -97,5 +51,5 @@ export const config = {
| General API | 30 requests | 10 seconds | | General API | 30 requests | 10 seconds |
| File Upload | 5 requests | 1 minute | | File Upload | 5 requests | 1 minute |
## Alternative ## Multi-Instance Deployments
**Vercel Edge Config** - Simpler but less flexible. Built into Vercel, no external service needed. If the backend ever runs as more than one process/instance behind a load balancer, switch the plugin's store to Redis (`@fastify/rate-limit` supports a custom `redis` client option) so limits are shared across instances instead of per-process.
+20 -33
View File
@@ -4,41 +4,28 @@ Protect against XSS, Clickjacking, MIME sniffing, and other common web attacks.
## Setup ## Setup
Add security headers to `next.config.ts`: Set security headers via the reverse proxy (nginx) in front of both frontend static files and the backend, or via `@fastify/helmet` on the backend directly. nginx is the simpler single place to cover everything.
```typescript ### nginx (recommended — covers frontend + backend uniformly)
import type { NextConfig } from 'next' ```nginx
add_header X-Frame-Options "DENY" always;
const nextConfig: NextConfig = { add_header X-Content-Type-Options "nosniff" always;
async headers() { add_header Referrer-Policy "origin-when-cross-origin" always;
return [ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
{
source: '/:path*',
headers: [
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Referrer-Policy',
value: 'origin-when-cross-origin',
},
{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains',
},
],
},
]
},
}
export default nextConfig
``` ```
Add these inside the `server { }` block for the site.
### Alternative: @fastify/helmet on the backend
```bash
cd backend && npm install @fastify/helmet
```
```typescript
// backend/src/server.ts
import helmet from "@fastify/helmet";
await app.register(helmet);
```
Covers only API responses — still set the same headers in nginx for the static frontend files.
## What Each Header Does ## What Each Header Does
+3 -3
View File
@@ -1,10 +1,10 @@
# Feature Index # Feature Index
> Central tracking for all features. Updated by skills automatically. > Central tracking for all features. Updated by skills automatically — never hand-edit the status column without also updating the matching feature spec header.
## Status Legend ## Status Legend
- **Planned** - Requirements written, ready for development - **Planned** - Requirements written, ready for architecture
- **In Progress** - Currently being built - **In Progress** - Tech design done, frontend/backend being built
- **In Review** - QA testing in progress - **In Review** - QA testing in progress
- **Deployed** - Live in production - **Deployed** - Live in production
+1 -1
View File
@@ -76,7 +76,7 @@ Am Ende des Feature-Dokuments fügt QA die Test-Ergebnisse hinzu:
**Status:** ✅ Deployed **Status:** ✅ Deployed
**Deployed:** 2026-01-13 **Deployed:** 2026-01-13
**Production URL:** https://your-app.vercel.app **Production URL:** https://your-domain.example
**Git Tag:** v1.0.0-PROJ-1 **Git Tag:** v1.0.0-PROJ-1
``` ```
+4
View File
@@ -0,0 +1,4 @@
# Only VITE_-prefixed vars are exposed to the browser at build time.
VITE_SENTRY_DSN=
# Add more environment variables as needed
+2 -2
View File
@@ -1,11 +1,11 @@
{ {
"$schema": "https://ui.shadcn.com/schema.json", "$schema": "https://ui.shadcn.com/schema.json",
"style": "default", "style": "default",
"rsc": true, "rsc": false,
"tsx": true, "tsx": true,
"tailwind": { "tailwind": {
"config": "tailwind.config.ts", "config": "tailwind.config.ts",
"css": "src/app/globals.css", "css": "src/index.css",
"baseColor": "slate", "baseColor": "slate",
"cssVariables": true, "cssVariables": true,
"prefix": "" "prefix": ""
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI Coding Starter Kit</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+5422
View File
File diff suppressed because it is too large. Load diff
+10 -10
View File
@@ -1,13 +1,13 @@
{ {
"name": "ai-coding-starter-kit", "name": "ai-coding-starter-kit-frontend",
"version": "1.0.0", "version": "1.0.0",
"description": "AI Coding Starter Kit with AI Agent Team System for Claude Code",
"private": true, "private": true,
"type": "module",
"scripts": { "scripts": {
"dev": "next dev", "dev": "vite",
"build": "next build", "build": "vite build",
"start": "next start", "preview": "vite preview",
"lint": "next lint" "lint": "eslint ."
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.2.2",
@@ -31,16 +31,15 @@
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toast": "^1.2.15", "@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@supabase/supabase-js": "^2.39.3",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"next": "^16.1.1",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-hook-form": "^7.71.1", "react-hook-form": "^7.71.1",
"react-router-dom": "^7.1.1",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^2.2.0", "tailwind-merge": "^2.2.0",
"zod": "^4.3.5" "zod": "^4.3.5"
@@ -49,11 +48,12 @@
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.0.1", "autoprefixer": "^10.0.1",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.1.1",
"postcss": "^8", "postcss": "^8",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "^5" "typescript": "^5",
"vite": "^6.0.7"
} }
} }
File renamed without changes.
File renamed without changes.

Before

Width:  |  Height:  |  Size: 233 B

After

Width:  |  Height:  |  Size: 233 B

File renamed without changes.

Before

Width:  |  Height:  |  Size: 303 B

After

Width:  |  Height:  |  Size: 303 B

File renamed without changes.

Before

Width:  |  Height:  |  Size: 206 B

After

Width:  |  Height:  |  Size: 206 B

+14
View File
@@ -0,0 +1,14 @@
import { Toaster } from "@/components/ui/sonner";
function App() {
return (
<>
<main className="min-h-screen flex items-center justify-center">
<h1 className="text-2xl font-semibold">AI Coding Starter Kit</h1>
</main>
<Toaster />
</>
);
}
export default App;
@@ -1,4 +1,3 @@
"use client"
import * as React from "react" import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion" import * as AccordionPrimitive from "@radix-ui/react-accordion"
@@ -1,4 +1,3 @@
"use client"
import * as React from "react" import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
File renamed without changes.
@@ -1,4 +1,3 @@
"use client"
import * as React from "react" import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar" import * as AvatarPrimitive from "@radix-ui/react-avatar"
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,4 +1,3 @@
"use client"
import * as React from "react" import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox" import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
@@ -1,4 +1,3 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
@@ -1,4 +1,3 @@
"use client"
import * as React from "react" import * as React from "react"
import { type DialogProps } from "@radix-ui/react-dialog" import { type DialogProps } from "@radix-ui/react-dialog"
@@ -1,4 +1,3 @@
"use client"
import * as React from "react" import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog" import * as DialogPrimitive from "@radix-ui/react-dialog"
@@ -1,4 +1,3 @@
"use client"
import * as React from "react" import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
@@ -1,4 +1,3 @@
"use client"
import * as React from "react" import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label" import * as LabelPrimitive from "@radix-ui/react-label"
File renamed without changes.
@@ -1,4 +1,3 @@
"use client"
import * as React from "react" import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label" import * as LabelPrimitive from "@radix-ui/react-label"
File renamed without changes.
@@ -1,4 +1,3 @@
"use client"
import * as React from "react" import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover" import * as PopoverPrimitive from "@radix-ui/react-popover"
@@ -1,4 +1,3 @@
"use client"
import * as React from "react" import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress" import * as ProgressPrimitive from "@radix-ui/react-progress"
@@ -1,4 +1,3 @@
"use client"
import * as React from "react" import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
@@ -1,4 +1,3 @@
"use client"
import * as React from "react" import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
@@ -1,4 +1,3 @@
"use client"
import * as React from "react" import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select" import * as SelectPrimitive from "@radix-ui/react-select"
@@ -1,4 +1,3 @@
"use client"
import * as React from "react" import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator" import * as SeparatorPrimitive from "@radix-ui/react-separator"
@@ -1,4 +1,3 @@
"use client"
import * as React from "react" import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog" import * as SheetPrimitive from "@radix-ui/react-dialog"
@@ -1,4 +1,3 @@
"use client"
import * as React from "react" import * as React from "react"
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot"
File renamed without changes.
@@ -1,4 +1,3 @@
"use client"
import { import {
CircleCheck, CircleCheck,
@@ -1,4 +1,3 @@
"use client"
import * as React from "react" import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch" import * as SwitchPrimitives from "@radix-ui/react-switch"
File renamed without changes.
@@ -1,4 +1,3 @@
"use client"
import * as React from "react" import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs" import * as TabsPrimitive from "@radix-ui/react-tabs"
File renamed without changes.
@@ -1,4 +1,3 @@
"use client"
import * as React from "react" import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast" import * as ToastPrimitives from "@radix-ui/react-toast"
@@ -1,4 +1,3 @@
"use client"
import { useToast } from "@/hooks/use-toast" import { useToast } from "@/hooks/use-toast"
import { import {
@@ -1,4 +1,3 @@
"use client"
import * as React from "react" import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip" import * as TooltipPrimitive from "@radix-ui/react-tooltip"
File renamed without changes.
@@ -1,4 +1,3 @@
"use client"
// Inspired by react-hot-toast library // Inspired by react-hot-toast library
import * as React from "react" import * as React from "react"
File renamed without changes.
File renamed without changes.
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import "./index.css";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>,
);
@@ -3,9 +3,8 @@ import type { Config } from "tailwindcss";
const config: Config = { const config: Config = {
darkMode: ["class"], darkMode: ["class"],
content: [ content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}", "./index.html",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}", "./src/**/*.{js,ts,jsx,tsx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
], ],
theme: { theme: {
extend: { extend: {
+23
View File
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}
+17
View File
@@ -0,0 +1,17 @@
import path from "path";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
server: {
proxy: {
"/api": "http://localhost:4000",
},
},
});
-7
View File
@@ -1,7 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;
-9149
View File
File diff suppressed because it is too large. Load diff
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6.2c.3 0 .4.1.4.4v12.9c0 .3-.1.4-.4.4h-6.2c-.3 0-.4-.1-.4-.4V45.8c0-.3.1-.4.4-.4zm-22 0h6.2c.3 0 .5.1.5.4v12.9c0 .3-.2.4-.5.4h-6.2c-.3 0-.4-.1-.4-.4V45.8c0-.3.1-.4.4-.4zm-11.4 0h6.2c.3 0 .5.1.5.4v12.9c0 .3-.2.4-.5.4h-6.2c-.3 0-.4-.1-.4-.4V45.8c0-.3.1-.4.4-.4zm-11.4 0h6.2c.3 0 .4.1.4.4v12.9c0 .3-.1.4-.4.4h-6.2c-.3 0-.4-.1-.4-.4V45.8c0-.3.1-.4.4-.4z"/><path fill="url(#a)" d="m317.2 78.9-.4 1.2h.4l21.1-27.3-21.1-27.3h-.4l.4 1.2a50 50 0 0 1 2.3 14.8 50 50 0 0 1-2.3 14.8l21.1 27.3h.4l-.4-1.2a50 50 0 0 0 2.3-14.8 50 50 0 0 0-2.3-14.8l.4-1.2h-.4l-21.1 27.3z"/><defs><linearGradient id="a" x1="267.5" x2="342.8" y1="27.8" y2="27.8" gradientUnits="userSpaceOnUse"><stop/><stop offset="1" stop-opacity="0"/></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

Before

Width:  |  Height:  |  Size: 630 B

-21
View File
@@ -1,21 +0,0 @@
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "AI Coding Starter Kit",
description: "Built with AI Agent Team System",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className="antialiased">
{children}
</body>
</html>
);
}
-101
View File
@@ -1,101 +0,0 @@
import Image from 'next/image'
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2">
Get started by editing{' '}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
src/app/page.tsx
</code>
.
</li>
<li>Save and see your changes instantly.</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org →
</a>
</footer>
</div>
)
}
-14
View File
@@ -1,14 +0,0 @@
// Supabase Client Setup
// Uncomment this file when you're ready to use Supabase
/*
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
*/
// For now, export a placeholder to avoid import errors
export const supabase = null;
-41
View File
@@ -1,41 +0,0 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}