
Senior Fullstack
- 1.1k installs
- 23.5k repo stars
- Updated July 17, 2026
- alirezarezvani/claude-skills
senior-fullstack is an agent skill that scaffolds production-grade fullstack projects and runs code quality audits with stack recommendations for developers starting or evaluating multi-tier web applications.
About
senior-fullstack is a fullstack development toolkit with three Python CLI tools and four JSON customization profiles. project_scaffolder.py generates Next.js 14+ App Router, FastAPI-React, MERN, and Django-React boilerplate with Docker, TypeScript, and environment templates. fullstack_decision_engine.py ranks profiles—saas-startup, enterprise-scale, internal-tool, and marketing-site—from team size, cadence, traffic, and budget inputs. code_quality_analyzer.py scores security issues and complexity across a codebase. Seven forcing questions in references/forcing_questions.md gate architecture decisions before scaffolding. The cs-fullstack-engineer agent forks into api-design-reviewer, database-designer, and ci-cd-pipeline-builder specialists. Reach for senior-fullstack when scaffolding a new web app, comparing stacks, or auditing an existing fullstack repository.
- Generates complete project structures for Next.js 14+, FastAPI+React, MERN, and Django+React templates
- Performs code quality analysis with security and complexity scoring
- Delivers tech stack selection guidance and comparison
- Supports trigger phrases for scaffolding, boilerplate generation, and codebase audits
- 4 supported fullstack templates with TypeScript, databases, and modern tooling pre-configured
Senior Fullstack by the numbers
- 1,075 all-time installs (skills.sh)
- +29 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #958 of 16,565 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
npx skills add https://github.com/alirezarezvani/claude-skills --skill senior-fullstackAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.1k |
|---|---|
| repo stars | ★ 23.5k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 17, 2026 |
| Repository | alirezarezvani/claude-skills ↗ |
How do you scaffold a production fullstack project?
Instantly scaffold production-grade fullstack projects and receive expert stack recommendations plus automated code quality audits.
Who is it for?
Fullstack developers choosing between Next.js, FastAPI, MERN, or Django stacks who want automated scaffolding plus security and complexity audits.
Skip if: Mobile-native apps, single-language backend-only services, or repositories that already have finalized architecture needing no scaffolding or stack comparison.
When should I use this skill?
User asks to scaffold a new project, create a Next.js app, set up FastAPI with React, analyze code quality, or compare fullstack stacks
What you get
Generated project directories with Docker and TypeScript configs, stack profile recommendation JSON, and code-quality audit reports
- Scaffolded project repository
- Stack profile recommendation JSON
- Code quality audit report
By the numbers
- Ships 3 Python CLI scripts: project_scaffolder, fullstack_decision_engine, code_quality_analyzer
- Includes 4 scaffold templates and 4 JSON customization profiles
- Walks 7 forcing questions before locking architecture decisions
Files
Senior Fullstack
Fullstack development skill with project scaffolding and code quality analysis tools.
---
Table of Contents
---
Trigger Phrases
Use this skill when you hear:
- "scaffold a new project"
- "create a Next.js app"
- "set up FastAPI with React"
- "analyze code quality"
- "check for security issues in codebase"
- "what stack should I use"
- "set up a fullstack project"
- "generate project boilerplate"
---
Tools
Decision Engine
Deterministic profile picker. Given four assumptions (team-size, cadence, user-facing, budget) plus optional traffic/sensitivity inputs, ranks the four built-in profiles and returns the matched profile with SLO floor and named approver chain. Refuses to recommend a profile without the four required inputs.
Usage:
# See all options
python scripts/fullstack_decision_engine.py --help
# Run against a sample input
python scripts/fullstack_decision_engine.py --sample
# Pick a profile from real inputs
python scripts/fullstack_decision_engine.py \
--team-size-12mo 8 --cadence daily --user-facing true --budget 5000 \
--traffic-p99-rps 50 --data-sensitivity pii-only
# JSON output for downstream tools
python scripts/fullstack_decision_engine.py --sample --output jsonReturns: matched profile name, score, matched/violated constraints, stack recommendation, anti-recommendations, SLO floor, named-approver chain, and canon references.
The engine encodes the same matrix the conversational grill walks through — use it directly when inputs are already known, or via the cs-fullstack-engineer agent for the question-by-question grill.
---
Project Scaffolder
Generates fullstack project structures with boilerplate code.
Supported Templates:
nextjs- Next.js 14+ with App Router, TypeScript, Tailwind CSSfastapi-react- FastAPI backend + React frontend + PostgreSQLmern- MongoDB, Express, React, Node.js with TypeScriptdjango-react- Django REST Framework + React frontend
Usage:
# List available templates
python scripts/project_scaffolder.py --list-templates
# Create Next.js project
python scripts/project_scaffolder.py nextjs my-app
# Create FastAPI + React project
python scripts/project_scaffolder.py fastapi-react my-api
# Create MERN stack project
python scripts/project_scaffolder.py mern my-project
# Create Django + React project
python scripts/project_scaffolder.py django-react my-app
# Specify output directory
python scripts/project_scaffolder.py nextjs my-app --output ./projects
# JSON output
python scripts/project_scaffolder.py nextjs my-app --jsonParameters:
| Parameter | Description |
|---|---|
template | Template name (nextjs, fastapi-react, mern, django-react) |
project_name | Name for the new project directory |
--output, -o | Output directory (default: current directory) |
--list-templates, -l | List all available templates |
--json | Output in JSON format |
Output includes:
- Project structure with all necessary files
- Package configurations (package.json, requirements.txt)
- TypeScript configuration
- Docker and docker-compose setup
- Environment file templates
- Next steps for running the project
---
Code Quality Analyzer
Analyzes fullstack codebases for quality issues.
Analysis Categories:
- Security vulnerabilities (hardcoded secrets, injection risks)
- Code complexity metrics (cyclomatic complexity, nesting depth)
- Dependency health (outdated packages, known CVEs)
- Test coverage estimation
- Documentation quality
Usage:
# Analyze current directory
python scripts/code_quality_analyzer.py .
# Analyze specific project
python scripts/code_quality_analyzer.py /path/to/project
# Verbose output with detailed findings
python scripts/code_quality_analyzer.py . --verbose
# JSON output
python scripts/code_quality_analyzer.py . --json
# Save report to file
python scripts/code_quality_analyzer.py . --output report.jsonParameters:
| Parameter | Description |
|---|---|
project_path | Path to project directory (default: current directory) |
--verbose, -v | Show detailed findings |
--json | Output in JSON format |
--output, -o | Write report to file |
Output includes:
- Overall score (0-100) with letter grade
- Security issues by severity (critical, high, medium, low)
- High complexity files
- Vulnerable dependencies with CVE references
- Test coverage estimate
- Documentation completeness
- Prioritized recommendations
Sample Output:
============================================================
CODE QUALITY ANALYSIS REPORT
============================================================
Overall Score: 75/100 (Grade: C)
Files Analyzed: 45
Total Lines: 12,500
--- SECURITY ---
Critical: 1
High: 2
Medium: 5
--- COMPLEXITY ---
Average Complexity: 8.5
High Complexity Files: 3
--- RECOMMENDATIONS ---
1. [P0] SECURITY
Issue: Potential hardcoded secret detected
Action: Remove or secure sensitive data at line 42---
Workflows
Workflow 1: Start New Project
1. Choose appropriate stack based on requirements (see Stack Decision Matrix) 2. Scaffold project structure 3. Verify scaffold: confirm package.json (or requirements.txt) exists 4. Run initial quality check — address any P0 issues before proceeding 5. Set up development environment
# 1. Scaffold project
python scripts/project_scaffolder.py nextjs my-saas-app
# 2. Verify scaffold succeeded
ls my-saas-app/package.json
# 3. Navigate and install
cd my-saas-app
npm install
# 4. Configure environment
cp .env.example .env.local
# 5. Run quality check
python scripts/code_quality_analyzer.py .
# 6. Start development
npm run devWorkflow 2: Audit Existing Codebase
1. Run code quality analysis 2. Review security findings — fix all P0 (critical) issues immediately 3. Re-run analyzer to confirm P0 issues are resolved 4. Create tickets for P1/P2 issues
# 1. Full analysis
python scripts/code_quality_analyzer.py /path/to/project --verbose
# 2. Generate detailed report
python scripts/code_quality_analyzer.py /path/to/project --json --output audit.json
# 3. After fixing P0 issues, re-run to verify
python scripts/code_quality_analyzer.py /path/to/project --verboseWorkflow 3: Stack Selection
Use the tech stack guide to evaluate options:
1. SEO Required? → Next.js with SSR 2. API-heavy backend? → Separate FastAPI or NestJS 3. Real-time features? → Add WebSocket layer 4. Team expertise → Match stack to team skills
See references/tech_stack_guide.md for detailed comparison.
---
Reference Guides
Architecture Patterns (references/architecture_patterns.md)
- Frontend component architecture (Atomic Design, Container/Presentational)
- Backend patterns (Clean Architecture, Repository Pattern)
- API design (REST conventions, GraphQL schema design)
- Database patterns (connection pooling, transactions, read replicas)
- Caching strategies (cache-aside, HTTP cache headers)
- Authentication architecture (JWT + refresh tokens, sessions)
Development Workflows (references/development_workflows.md)
- Local development setup (Docker Compose, environment config)
- Git workflows (trunk-based, conventional commits)
- CI/CD pipelines (GitHub Actions examples)
- Testing strategies (unit, integration, E2E)
- Code review process (PR templates, checklists)
- Deployment strategies (blue-green, canary, feature flags)
- Monitoring and observability (logging, metrics, health checks)
Tech Stack Guide (references/tech_stack_guide.md)
- Frontend frameworks comparison (Next.js, React+Vite, Vue)
- Backend frameworks (Express, Fastify, NestJS, FastAPI, Django)
- Database selection (PostgreSQL, MongoDB, Redis)
- ORMs (Prisma, Drizzle, SQLAlchemy)
- Authentication solutions (Auth.js, Clerk, custom JWT)
- Deployment platforms (Vercel, Railway, AWS)
- Stack recommendations by use case (MVP, SaaS, Enterprise)
---
Quick Reference
Stack Decision Matrix
| Requirement | Recommendation |
|---|---|
| SEO-critical site | Next.js with SSR |
| Internal dashboard | React + Vite |
| API-first backend | FastAPI or Fastify |
| Enterprise scale | NestJS + PostgreSQL |
| Rapid prototype | Next.js API routes |
| Document-heavy data | MongoDB |
| Complex queries | PostgreSQL |
Common Issues
| Issue | Solution |
|---|---|
| N+1 queries | Use DataLoader or eager loading |
| Slow builds | Check bundle size, lazy load |
| Auth complexity | Use Auth.js or Clerk |
| Type errors | Enable strict mode in tsconfig |
| CORS issues | Configure middleware properly |
---
Assumptions and Verifiable Success Criteria (Karpathy discipline)
Before this skill scaffolds, recommends, or modifies any code, the following four assumptions MUST be surfaced. If any are unknown, the skill stops and walks the Forcing-question library instead.
1. Team size today + 12-month headcount — drives architecture (monolith / modular / services). Sam Newman: "MonolithFirst." 2. Deployment cadence target — drives CI/CD spend and feature-flag investment. Accelerate (Forsgren et al. 2018). 3. User-facing vs. internal vs. marketing-site — drives stack pick and a11y/perf budget. 4. Monthly cloud + SaaS budget ceiling — drives the build-vs-managed-service split.
Verifiable success criteria (Karpathy #4) — every recommendation this skill emits must include three machine-checkable numbers:
- An API latency target (p50, p95, p99 in ms)
- A frontend perf target (LCP, INP, CLS on mobile-4G)
- An uptime / SLO target
If any of those three is not stated, the recommendation is incomplete — go back to Q7 of the forcing-question library.
The scripts/fullstack_decision_engine.py tool encodes these checks: it refuses to recommend a profile without all four assumption inputs and prints the verifiable thresholds for the matched profile.
---
Customization profiles
Four built-in profiles in profiles/ calibrate every recommendation:
| Profile | When to pick | Cloud ceiling | Pattern |
|---|---|---|---|
saas-startup | < 10 eng, customer-facing, daily+ cadence | $8K/mo | Modular monolith on Next.js + Postgres |
enterprise-scale | 50+ eng, regulated, per-PR with gates | $250K/mo | Domain-bounded services + platform team |
internal-tool | ≤ 5 eng, auth-walled, < 100 DAU | $500/mo | Retool-first; thin custom stack if forced |
marketing-site | SEO-dependent, near-zero write | $200/mo | Static-first (Astro / 11ty / Next-static) |
Pick a profile via:
python scripts/fullstack_decision_engine.py \
--team-size 6 --team-size-12mo 12 \
--cadence daily --user-facing true --budget 5000 \
--traffic-p99-rps 45 --data-sensitivity pii-onlyThe tool returns the best-fit profile, the tradeoff against the runner-up (if within 15%), the stack recommendation, the anti-patterns to avoid on that profile, and the named-approver chain. This tool never auto-approves.
To add a custom profile: copy profiles/saas-startup.json to profiles/<your-org>.json, adjust the constraints and stack_recommendations blocks, and rerun. The JSON is the customization surface — no code changes needed.
---
Composition map
This skill does NOT reimplement scope owned by the POWERFUL-tier specialists. It forks into them. See references/composition_map.md for the full routing table. Key forks:
| Concern | Fork into |
|---|---|
| API contract review | engineering/skills/api-design-reviewer/ |
| Database schema design | engineering/skills/database-designer/ |
| Reliability / SLO design | engineering/slo-architect/ |
| CI/CD pipeline | engineering/skills/ci-cd-pipeline-builder/ |
| Performance profiling | engineering/skills/performance-profiler/ |
| Pre-commit Karpathy review | engineering/karpathy-coder/ |
| Pre-flight architecture grill | engineering/grill-me/ |
The cs-fullstack-engineer agent (in agents/engineering/cs-fullstack-engineer.md) orchestrates these forks via context: fork. Invoke it from another agent with Agent({subagent_type: "cs-fullstack-engineer", prompt: "..."}) or via the slash command /cs:fullstack-review <your problem>.
---
Forcing-question library (Matt Pocock grill)
Before locking any architecture or stack decision, walk the seven forcing questions in references/forcing_questions.md. Each has a recommended answer, canon citation, and kill criterion. The discipline:
1. One question per turn. No bundling. 2. Always recommend the answer with cited canon. 3. Track answers in a working file (e.g., /tmp/fullstack-grill-<date>.md). 4. If a kill criterion trips, stop. Do not scaffold around an unresolved gap. 5. After Q7, run fullstack_decision_engine.py with the seven answers as inputs.
Summary of the seven questions (full content in the reference):
1. Team size today + 12-month headcount? 2. Deployment cadence — per-PR, daily, weekly, quarterly? 3. Customer-facing, internal tool, or marketing site? 4. One-year p50 / p99 traffic forecast? 5. Hiring against the stack or training the team? 6. Year-one monthly cloud + SaaS ceiling? 7. Three verifiable success criteria with numeric targets?
---
Invocation from other agents and skills
This skill is invokable by any other agent or skill via three surfaces:
1. Slash command: /cs:fullstack-review <prompt> — runs the full grill + decision engine + composition routing. 2. Agent subagent: Agent({subagent_type: "cs-fullstack-engineer", prompt: "..."}) — forks context, returns ≤ 200-word digest. 3. Direct tool call: python scripts/fullstack_decision_engine.py ... — deterministic profile match without the conversational grill (use when inputs are already known).
See agents/engineering/cs-fullstack-engineer.md for the full invocation contract.
{
"$schema": "https://json-schema.org/draft-07/schema#",
"profile_name": "enterprise-scale",
"description": "Customer-facing product, 50+ engineers, $50M+ ARR, regulated (SOC2 / HIPAA / PCI-DSS). Cadence: per-PR with strict gates. Cloud ceiling: $50K+/mo. Engineering org has dedicated platform team.",
"version": "1.0.0",
"constraints": {
"team_size_min": 50,
"team_size_year_one_max": 200,
"deployment_cadence": "per-pr-with-gates",
"cloud_budget_monthly_usd_ceiling": 250000,
"user_facing": true,
"data_sensitivity_tier": "regulated-pii-phi-or-pci"
},
"stack_recommendations": {
"primary": "domain-bounded-services-with-shared-platform",
"frontend": {
"framework": "next-or-remix-or-internal-platform",
"rendering": "ssr-or-rsc-with-edge-caching",
"styling": "design-system-tokens-on-tailwind-or-stitches",
"state": "react-query-plus-zustand-or-redux-toolkit"
},
"backend": {
"pattern": "domain-bounded-services-or-modular-monolith-per-bounded-context",
"language": "polyglot-acceptable-with-3-language-cap",
"framework_options": ["spring-boot", "go-gin", "fastapi", "rails", "dotnet-minimal"],
"database": "postgresql-with-read-replicas-plus-warehouse",
"cache": "redis-cluster",
"queue": "kafka-or-sqs-with-explicit-ownership"
},
"infrastructure": {
"hosting": ["eks", "gke", "aks", "self-hosted-k8s"],
"service_mesh": "istio-or-linkerd-if-services-cross-30",
"observability": "datadog-or-honeycomb-plus-grafana",
"feature_flags": "launchdarkly-or-internal-platform",
"secrets": "vault-or-secrets-manager-required"
}
},
"anti_recommendations": {
"monolith-no-modular-boundaries": "kill — review cycle breaks past 30 engineers",
"single-database-no-read-replicas": "kill — fan-out reads at this scale collapse the primary",
"no-feature-flags": "kill — gates are mandatory at per-PR cadence",
"no-platform-team": "warn — dedicate 8-15% of headcount or velocity halves (Team Topologies, Skelton 2019)",
"no-disaster-recovery-runbook": "kill — RTO/RPO must be documented and tested"
},
"success_thresholds": {
"p50_api_latency_ms": 50,
"p95_api_latency_ms": 150,
"p99_api_latency_ms": 400,
"frontend_lcp_ms_mobile_4g": 2000,
"frontend_inp_ms": 100,
"uptime_target": 0.9995,
"deployment_time_minutes_max": 30,
"test_coverage_min": 0.8,
"security_scan_severity_max": "high",
"rpo_minutes_max": 15,
"rto_minutes_max": 60
},
"ci_cd": {
"platform": "github-actions-or-gitlab-or-buildkite",
"stages": ["lint", "typecheck", "unit-test", "integration-test", "sast", "sca", "container-scan", "build", "deploy-canary", "smoke-test", "deploy-prod"],
"gates": ["all-tests-pass", "no-high-or-critical-vulns", "sast-baseline-met", "code-coverage-floor", "perf-baseline-not-regressed"]
},
"named_approver_chain": {
"stack_change": "principal-engineer + architecture-review-board",
"production-data-migration": "data-team-lead + dba + on-call + change-advisory-board",
"external-service-add": "principal-engineer + security-review + finance-approval"
},
"canon_references": [
"Matthew Skelton & Manuel Pais, Team Topologies (2019) — platform team sizing",
"Sam Newman, Building Microservices 2e (2021) — bounded-context boundaries",
"Google SRE Workbook — SLO/SLI/error-budget discipline at scale",
"Susan Fowler, Production-Ready Microservices (2017) — eight pillars",
"ITIL v4 / Change Advisory Board pattern for regulated industries"
]
}
{
"$schema": "https://json-schema.org/draft-07/schema#",
"profile_name": "internal-tool",
"description": "Internal-only application (admin panel, ops dashboard, employee tool). Auth-walled, low traffic (< 100 DAU), no SEO, no public a11y requirements. Goal: ship features fast, not engineer for scale.",
"version": "1.0.0",
"constraints": {
"team_size_max": 5,
"deployment_cadence": "weekly-or-on-demand",
"cloud_budget_monthly_usd_ceiling": 500,
"user_facing": false,
"data_sensitivity_tier": "internal-or-pii-only"
},
"stack_recommendations": {
"primary": "internal-tool-platform-or-thin-stack",
"consider_no_code_first": [
"retool",
"appsmith-self-host",
"budibase-self-host",
"tooljet-self-host"
],
"if_custom_required": {
"frontend": "next-pages-router-or-vite-react-with-shadcn",
"backend": "next-api-routes-or-fastapi-or-django-admin",
"database": "postgresql-shared-with-prod-via-read-replica-OR-supabase",
"auth": "okta-saml-or-google-workspace-sso"
}
},
"anti_recommendations": {
"design-system-investment": "kill — four-user audience does not measure design polish",
"wcag-aaa-compliance": "kill — auth-walled means screen-reader audience is known and small",
"edge-cdn-for-mobile-4g": "kill — internal users are on corporate networks",
"feature-flags": "warn — usually unnecessary at this scale, push-and-pray is fine for non-customer-facing changes",
"kubernetes": "kill — single container on Fly.io / Render / EC2 t3.small is overkill already",
"microservices": "kill — full-stop"
},
"success_thresholds": {
"p50_api_latency_ms": 300,
"p95_api_latency_ms": 1000,
"p99_api_latency_ms": 3000,
"frontend_lcp_ms_corporate_network": 3000,
"uptime_target": 0.99,
"deployment_time_minutes_max": 30,
"test_coverage_min": 0.3
},
"ci_cd": {
"platform": "github-actions-minimal",
"stages": ["lint", "test", "build", "deploy"],
"gates": ["tests-pass"]
},
"named_approver_chain": {
"stack_change": "tech-lead",
"production-data-migration": "tech-lead",
"external-service-add": "tech-lead"
},
"canon_references": [
"Donald Reinertsen, Principles of Product Development Flow (2009) — minimize WIP and ceremony",
"Basecamp / 37signals, REWORK (2010) — under-engineer internal tools",
"Retool internal benchmarks (2024) — 80% of internal-tool teams over-invest in a custom build"
]
}
{
"$schema": "https://json-schema.org/draft-07/schema#",
"profile_name": "marketing-site",
"description": "SEO-dependent public marketing surface (landing pages, blog, docs, pricing). Heavy read, near-zero write. Goal: maximize Core Web Vitals + SEO crawlability. Build separate from the product codebase.",
"version": "1.0.0",
"constraints": {
"team_size_max": 3,
"deployment_cadence": "per-pr",
"cloud_budget_monthly_usd_ceiling": 200,
"user_facing": true,
"data_sensitivity_tier": "public-only",
"read_write_ratio_min": 100
},
"stack_recommendations": {
"primary": "static-or-mostly-static",
"frontend": {
"framework_options_ranked": ["astro", "next.js-static-export", "11ty", "hugo"],
"rendering": "static-generation-with-island-hydration",
"styling": "tailwind-or-css-modules",
"cms": "headless-cms-or-mdx-in-repo"
},
"backend": "no-backend-or-edge-functions-only",
"infrastructure": {
"hosting": ["cloudflare-pages", "vercel", "netlify"],
"cdn": "included-with-host",
"analytics": "plausible-or-fathom-or-self-host-umami",
"forms": "edge-function-to-webhook-or-formspree"
}
},
"anti_recommendations": {
"next-app-router-for-marketing": "warn — RSC over-engineers marketing-site needs; static-first wins on CWV and DX",
"spa-for-marketing": "kill — SEO catastrophe; no JS-rendered marketing site ranks competitively in 2026",
"custom-backend": "kill — no writes means no backend",
"personalization-at-edge": "warn — defer until SEO baseline is established",
"third-party-tag-soup": "kill — every third-party script costs LCP and INP",
"ga4-default": "warn — GA4 hurts CWV scores; prefer cookieless alternatives"
},
"success_thresholds": {
"lcp_ms_mobile_4g": 1500,
"inp_ms": 100,
"cls": 0.05,
"ttfb_ms": 300,
"lighthouse_seo_score_min": 95,
"lighthouse_accessibility_score_min": 95,
"lighthouse_performance_score_min": 95,
"uptime_target": 0.999,
"deployment_time_minutes_max": 5
},
"ci_cd": {
"platform": "github-actions-or-host-native",
"stages": ["lint", "build", "lighthouse-ci", "deploy-preview", "deploy-prod"],
"gates": ["lighthouse-perf >= 95", "lighthouse-seo >= 95", "no-broken-links"]
},
"named_approver_chain": {
"stack_change": "engineering-lead + marketing-lead",
"external-service-add": "engineering-lead + marketing-lead + cfo-if-monthly-cost-over-100"
},
"canon_references": [
"Web Almanac (HTTP Archive, 2025) — marketing-site CWV distribution",
"Addy Osmani, Web Performance for the Modern Web (2024) — static-first defaults",
"Google SearchGuide (2024) — Core Web Vitals as ranking signal",
"Plausible Analytics whitepaper (2023) — third-party tag impact on CWV",
"Astro framework docs — Islands Architecture (Eisenberg, 2021)"
]
}
{
"$schema": "https://json-schema.org/draft-07/schema#",
"profile_name": "saas-startup",
"description": "Customer-facing SaaS product, < 10 engineers, < $10M ARR. Pre-product-market-fit or just past it. Cadence: daily+. Cloud ceiling: $3-8K/mo. Hiring against the stack via standard recruiter pipeline.",
"version": "1.0.0",
"constraints": {
"team_size_max": 10,
"team_size_year_one_max": 25,
"deployment_cadence": "daily-or-per-pr",
"cloud_budget_monthly_usd_ceiling": 8000,
"user_facing": true,
"data_sensitivity_tier": "pii-only"
},
"stack_recommendations": {
"primary": "next-app-router-postgres",
"frontend": {
"framework": "next.js-14+",
"rendering": "rsc-default",
"styling": "tailwind",
"state": "server-state-via-rsc-plus-zustand-for-client"
},
"backend": {
"pattern": "modular-monolith",
"language": "typescript-or-python",
"framework_options": ["next-api-routes", "fastapi", "express"],
"database": "postgresql",
"cache": "redis-optional",
"queue": "defer-until-needed"
},
"infrastructure": {
"hosting": ["vercel", "fly.io", "render", "railway"],
"database_hosting": ["neon", "supabase", "rds-postgres-small"],
"observability": "single-tool-only-sentry-or-axiom",
"feature_flags": "growthbook-self-host-or-defer"
}
},
"anti_recommendations": {
"microservices": "kill — wrong team-size shape",
"kubernetes": "kill — Vercel/Fly already manages the runtime",
"event-sourcing": "kill — pre-PMF YAGNI",
"graphql-federation": "kill — no second team to federate with",
"kafka": "kill — Postgres LISTEN/NOTIFY or PgQ handles the load at this scale",
"service-mesh": "kill — no services to mesh"
},
"success_thresholds": {
"p50_api_latency_ms": 100,
"p95_api_latency_ms": 300,
"p99_api_latency_ms": 800,
"frontend_lcp_ms_mobile_4g": 2500,
"frontend_inp_ms": 200,
"uptime_target": 0.995,
"deployment_time_minutes_max": 15,
"test_coverage_min": 0.6
},
"ci_cd": {
"platform": "github-actions",
"stages": ["lint", "typecheck", "test", "build", "deploy-preview", "deploy-prod"],
"gates": ["test-pass", "no-critical-deps-vulns"]
},
"named_approver_chain": {
"stack_change": "tech-lead",
"production-data-migration": "tech-lead + on-call",
"external-service-add": "tech-lead + cfo (cost-aware)"
},
"canon_references": [
"Will Larson, An Elegant Puzzle (2019) — team-size-to-architecture mapping",
"Sam Newman, Building Microservices 2e (2021), ch. 1 — MonolithFirst",
"Tomasz Tunguz, Cloud Cost Discipline essays (2020-2024)",
"DORA State of DevOps 2023 — Elite cadence on monolith outperforms Medium cadence on microservices"
]
}
Fullstack Architecture Patterns
Proven architectural patterns for scalable fullstack applications covering frontend, backend, and their integration.
---
Table of Contents
- Frontend Architecture
- Backend Architecture
- API Design Patterns
- Database Patterns
- Caching Strategies
- Authentication Architecture
---
Frontend Architecture
Component Architecture
Atomic Design Pattern
Organize components in hierarchical levels:
src/components/
├── atoms/ # Button, Input, Icon
├── molecules/ # SearchInput, FormField
├── organisms/ # Header, Footer, Sidebar
├── templates/ # PageLayout, DashboardLayout
└── pages/ # Home, Profile, SettingsWhen to use: Large applications with design systems and multiple teams.
Container/Presentational Pattern
// Presentational - pure rendering, no state
function UserCard({ name, email, avatar }: UserCardProps) {
return (
<div className="card">
<img src={avatar} alt={name} />
<h3>{name}</h3>
<p>{email}</p>
</div>
);
}
// Container - handles data fetching and state
function UserCardContainer({ userId }: { userId: string }) {
const { data, loading } = useUser(userId);
if (loading) return <Skeleton />;
return <UserCard {...data} />;
}When to use: When you need clear separation between UI and logic.
State Management Patterns
Server State vs Client State
| Type | Examples | Tools |
|---|---|---|
| Server State | User data, API responses | React Query, SWR |
| Client State | UI toggles, form inputs | Zustand, Jotai |
| URL State | Filters, pagination | Next.js router |
React Query for Server State:
function useUsers(filters: Filters) {
return useQuery({
queryKey: ["users", filters],
queryFn: () => api.getUsers(filters),
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 30 * 60 * 1000, // 30 minutes
});
}
// Mutations with optimistic updates
function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.updateUser,
onMutate: async (newUser) => {
await queryClient.cancelQueries({ queryKey: ["users"] });
const previous = queryClient.getQueryData(["users"]);
queryClient.setQueryData(["users"], (old) =>
old.map(u => u.id === newUser.id ? newUser : u)
);
return { previous };
},
onError: (err, newUser, context) => {
queryClient.setQueryData(["users"], context.previous);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
},
});
}---
Backend Architecture
Clean Architecture
src/
├── domain/ # Business entities, no dependencies
│ ├── entities/ # User, Order, Product
│ └── interfaces/ # Repository interfaces
├── application/ # Use cases, application logic
│ ├── use-cases/ # CreateOrder, UpdateUser
│ └── services/ # OrderService, AuthService
├── infrastructure/ # External concerns
│ ├── database/ # Repository implementations
│ ├── http/ # Controllers, middleware
│ └── external/ # Third-party integrations
└── shared/ # Cross-cutting concerns
├── errors/
└── utils/Dependency Flow: domain ← application ← infrastructure
Repository Pattern:
// Domain interface
interface UserRepository {
findById(id: string): Promise<User | null>;
findByEmail(email: string): Promise<User | null>;
save(user: User): Promise<User>;
delete(id: string): Promise<void>;
}
// Infrastructure implementation
class PostgresUserRepository implements UserRepository {
constructor(private db: Database) {}
async findById(id: string): Promise<User | null> {
const row = await this.db.query(
"SELECT * FROM users WHERE id = $1",
[id]
);
return row ? this.toEntity(row) : null;
}
private toEntity(row: UserRow): User {
return new User({
id: row.id,
email: row.email,
name: row.name,
createdAt: row.created_at,
});
}
}Middleware Pipeline
// Express middleware chain
app.use(cors());
app.use(helmet());
app.use(requestId());
app.use(logger());
app.use(authenticate());
app.use(rateLimit());
app.use("/api", routes);
app.use(errorHandler());
// Custom middleware example
function requestId() {
return (req: Request, res: Response, next: NextFunction) => {
req.id = req.headers["x-request-id"] || crypto.randomUUID();
res.setHeader("x-request-id", req.id);
next();
};
}
function errorHandler() {
return (err: Error, req: Request, res: Response, next: NextFunction) => {
const status = err instanceof AppError ? err.status : 500;
const message = status === 500 ? "Internal Server Error" : err.message;
logger.error({ err, requestId: req.id });
res.status(status).json({ error: message, requestId: req.id });
};
}---
API Design Patterns
REST Best Practices
Resource Naming:
- Use nouns, not verbs:
/usersnot/getUsers - Use plural:
/usersnot/user - Nest for relationships:
/users/{id}/orders
HTTP Methods:
| Method | Purpose | Idempotent |
|---|---|---|
| GET | Retrieve | Yes |
| POST | Create | No |
| PUT | Replace | Yes |
| PATCH | Partial update | No |
| DELETE | Remove | Yes |
Response Envelope:
// Success response
{
"data": { /* resource */ },
"meta": {
"requestId": "abc-123",
"timestamp": "2024-01-15T10:30:00Z"
}
}
// Paginated response
{
"data": [/* items */],
"pagination": {
"page": 1,
"pageSize": 20,
"total": 150,
"totalPages": 8
}
}
// Error response
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input",
"details": [
{ "field": "email", "message": "Invalid email format" }
]
},
"meta": { "requestId": "abc-123" }
}GraphQL Architecture
Schema-First Design:
type Query {
user(id: ID!): User
users(filter: UserFilter, page: PageInput): UserConnection!
}
type Mutation {
createUser(input: CreateUserInput!): UserPayload!
updateUser(id: ID!, input: UpdateUserInput!): UserPayload!
}
type User {
id: ID!
email: String!
profile: Profile
orders(first: Int, after: String): OrderConnection!
}
type UserPayload {
user: User
errors: [Error!]
}Resolver Pattern:
const resolvers = {
Query: {
user: async (_, { id }, { dataSources }) => {
return dataSources.userAPI.findById(id);
},
},
User: {
// Field resolver for related data
orders: async (user, { first, after }, { dataSources }) => {
return dataSources.orderAPI.findByUserId(user.id, { first, after });
},
},
};DataLoader for N+1 Prevention:
const userLoader = new DataLoader(async (userIds: string[]) => {
const users = await db.query(
"SELECT * FROM users WHERE id = ANY($1)",
[userIds]
);
// Return in same order as input
return userIds.map(id => users.find(u => u.id === id));
});---
Database Patterns
Connection Pooling
// PostgreSQL with connection pool
const pool = new Pool({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
max: 20, // Maximum connections
idleTimeoutMillis: 30000, // Close idle connections
connectionTimeoutMillis: 2000,
});
// Prisma with connection pool
const prisma = new PrismaClient({
datasources: {
db: {
url: `${process.env.DATABASE_URL}?connection_limit=20&pool_timeout=10`,
},
},
});Transaction Patterns
// Unit of Work pattern
async function transferFunds(from: string, to: string, amount: number) {
return await prisma.$transaction(async (tx) => {
const sender = await tx.account.update({
where: { id: from },
data: { balance: { decrement: amount } },
});
if (sender.balance < 0) {
throw new InsufficientFundsError();
}
await tx.account.update({
where: { id: to },
data: { balance: { increment: amount } },
});
return tx.transaction.create({
data: { fromId: from, toId: to, amount },
});
});
}Read Replicas
// Route reads to replica
const readDB = new PrismaClient({
datasources: { db: { url: process.env.READ_DATABASE_URL } },
});
const writeDB = new PrismaClient({
datasources: { db: { url: process.env.WRITE_DATABASE_URL } },
});
class UserRepository {
async findById(id: string) {
return readDB.user.findUnique({ where: { id } });
}
async create(data: CreateUserData) {
return writeDB.user.create({ data });
}
}---
Caching Strategies
Cache Layers
Request → CDN Cache → Application Cache → Database Cache → DatabaseCache-Aside Pattern:
async function getUser(id: string): Promise<User> {
const cacheKey = `user:${id}`;
// 1. Try cache
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// 2. Fetch from database
const user = await db.user.findUnique({ where: { id } });
if (!user) throw new NotFoundError();
// 3. Store in cache
await redis.set(cacheKey, JSON.stringify(user), "EX", 3600);
return user;
}
// Invalidate on update
async function updateUser(id: string, data: UpdateData): Promise<User> {
const user = await db.user.update({ where: { id }, data });
await redis.del(`user:${id}`);
return user;
}HTTP Cache Headers:
// Immutable assets (hashed filenames)
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
// API responses
res.setHeader("Cache-Control", "private, max-age=0, must-revalidate");
res.setHeader("ETag", generateETag(data));
// Static pages
res.setHeader("Cache-Control", "public, max-age=3600, stale-while-revalidate=86400");---
Authentication Architecture
JWT + Refresh Token Flow
1. User logs in → Server returns access token (15min) + refresh token (7d)
2. Client stores tokens (httpOnly cookie for refresh, memory for access)
3. Access token expires → Client uses refresh token to get new pair
4. Refresh token expires → User must log in againImplementation:
// Token generation
function generateTokens(user: User) {
const accessToken = jwt.sign(
{ sub: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: "15m" }
);
const refreshToken = jwt.sign(
{ sub: user.id, tokenVersion: user.tokenVersion },
process.env.REFRESH_SECRET,
{ expiresIn: "7d" }
);
return { accessToken, refreshToken };
}
// Refresh endpoint
app.post("/auth/refresh", async (req, res) => {
const refreshToken = req.cookies.refreshToken;
try {
const payload = jwt.verify(refreshToken, process.env.REFRESH_SECRET);
const user = await db.user.findUnique({ where: { id: payload.sub } });
// Check token version (invalidation mechanism)
if (user.tokenVersion !== payload.tokenVersion) {
throw new Error("Token revoked");
}
const tokens = generateTokens(user);
setRefreshCookie(res, tokens.refreshToken);
res.json({ accessToken: tokens.accessToken });
} catch {
res.status(401).json({ error: "Invalid refresh token" });
}
});Session-Based Auth
// Redis session store
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === "production",
httpOnly: true,
sameSite: "lax",
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
},
}));
// Login
app.post("/auth/login", async (req, res) => {
const user = await authenticate(req.body.email, req.body.password);
req.session.userId = user.id;
res.json({ user });
});
// Middleware
function requireAuth(req, res, next) {
if (!req.session.userId) {
return res.status(401).json({ error: "Authentication required" });
}
next();
}---
Decision Matrix
| Pattern | Complexity | Scalability | When to Use |
|---|---|---|---|
| Monolith | Low | Medium | MVPs, small teams |
| Modular Monolith | Medium | High | Growing teams |
| Microservices | High | Very High | Large orgs, diverse tech |
| REST | Low | High | CRUD APIs, public APIs |
| GraphQL | Medium | High | Complex data needs, mobile apps |
| JWT Auth | Low | High | Stateless APIs, microservices |
| Session Auth | Low | Medium | Traditional web apps |
Fullstack Engineer — Composition Map
Principle (Karpathy #2, Simplicity First): do not reimplement scope that the POWERFUL-tier engineering specialists already own. This skill is the fullstack orchestrator; the specialists are the implementers. Fork into them — do not duplicate them.
This map is the routing table for the cs-fullstack-engineer agent and the /cs:fullstack-review command.
Composition routing table
| User concern | Fork into | When to fork | Path |
|---|---|---|---|
| API contract / REST + GraphQL design / breaking change risk | api-design-reviewer | After Q1–Q3 of the forcing-question library reveal API surface area | ../../../engineering/skills/api-design-reviewer/ |
| Database schema / migration safety / index strategy | database-designer + migration-architect | After Q4 (traffic forecast) — read/write ratio drives schema choice | ../../../engineering/skills/database-designer/, ../../../engineering/skills/migration-architect/ |
| Bundle size, frontend perf, server response perf | performance-profiler | After Q7 success criteria include a latency/LCP target | ../../../engineering/skills/performance-profiler/ |
| Reliability target / SLO / error budget | slo-architect | After Q7 lists an uptime or p99 SLA | ../../../engineering/slo-architect/skills/slo-architect/ |
| CI/CD pipeline (multi-language fullstack) | ci-cd-pipeline-builder | After Q2 cadence is daily / per-PR | ../../../engineering/skills/ci-cd-pipeline-builder/ |
| Dependency vulnerability + license risk | dependency-auditor | Before every major release; before any production push | ../../../engineering/skills/dependency-auditor/ |
| Monorepo tooling (Turbo / Nx / pnpm workspaces) | monorepo-navigator | When team size ≥ 6 and the codebase houses multiple deployable surfaces | ../../../engineering/skills/monorepo-navigator/ |
| API test generation + contract tests | api-test-suite-builder | After API contract is stable | ../../../engineering/skills/api-test-suite-builder/ |
| Observability + golden signals + alert design | observability-designer | Concurrent with SLO design | ../../../engineering/skills/observability-designer/ |
| Architecture onboarding doc for a new team member | codebase-onboarding | When ≥ 3 engineers will touch the code in 90 days | ../../../engineering/skills/codebase-onboarding/ |
| Hardening: AuthZ/AuthN, threat model, sensitive-data handling | senior-security + adversarial-reviewer | Before public launch; before handling PII/PHI/PCI data | ../../../engineering-team/skills/senior-security/, ../../../engineering-team/skills/adversarial-reviewer/ |
| Pre-commit code review (Karpathy 4 principles) | cs-karpathy-reviewer | Before EVERY commit this skill produces | ../../../engineering/karpathy-coder/ |
| Pre-flight grill on a draft architecture | cs-grill-master | Before locking the stack picks | ../../../engineering/grill-me/ |
Composition rules
1. Fork via `context: fork` — the agent forks its own context, runs the sub-skill, returns a ≤ 200-word digest. 2. One sub-skill at a time. Matt Pocock's depth-first rule. Finish the API contract branch before opening the database branch. 3. Honor sub-skill outputs as inputs. If database-designer recommends a schema, the next call to api-design-reviewer must use that schema, not invent one. 4. Never reimplement specialist scope. If the user asks "what's the right index strategy?" do not answer with handcrafted advice — fork into database-designer. 5. Document the chain. Every artifact this skill produces must list the sub-skills it invoked, in order, with the digest paths.
Anti-patterns
- ❌ Calling all sub-skills at the start "to be thorough." Burns context, produces noise.
- ❌ Skipping
cs-karpathy-reviewerbefore committing. Every commit from this skill must pass the diff-noise gate. - ❌ Implementing what
api-design-reviewerwould have caught (e.g., inconsistent REST verbs). Fork first; commit second. - ❌ Treating this skill's recommendations as approvals. Architecture choices must be sign-off-able by a named engineer; this skill never auto-approves.
When to escalate out of fullstack
- Pure-engineering org-design questions (team topologies, manager triggers) → escalate to
cs-vpe-advisor. - Strategic build-vs-buy at the company level → escalate to
cs-cto-advisor. - AI/ML pipeline questions → escalate to
senior-ml-engineer/senior-prompt-engineer. - Data warehouse / lakehouse / dbt questions → escalate to
senior-data-engineer. - Pure security / threat model → escalate to
cs-ciso-advisor(strategic) orsenior-security(tactical).
References
- Karpathy 4 principles →
../../../engineering/karpathy-coder/skills/karpathy-coder/references/karpathy-principles.md - Matt Pocock grill discipline →
../../../engineering/grill-me/skills/grill-me/references/forcing_question_patterns.md - Path-B 11-file contract →
../../../business-operations/CLAUDE.md(canonical statement)
Fullstack Development Workflows
Complete development lifecycle workflows from local setup to production deployment.
---
Table of Contents
- Local Development Setup
- Git Workflows
- CI/CD Pipelines
- Testing Strategies
- Code Review Process
- Deployment Strategies
- Monitoring and Observability
---
Local Development Setup
Docker Compose Development Environment
# docker-compose.yml
version: "3.8"
services:
app:
build:
context: .
target: development
volumes:
- .:/app
- /app/node_modules
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/app
- REDIS_URL=redis://redis:6379
depends_on:
- db
- redis
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: app
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
postgres_data:Multistage Dockerfile:
# Base stage
FROM node:20-alpine AS base
WORKDIR /app
RUN apk add --no-cache libc6-compat
# Development stage
FROM base AS development
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npm", "run", "dev"]
# Builder stage
FROM base AS builder
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM base AS production
ENV NODE_ENV=production
COPY --from=builder /app/package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
USER node
CMD ["node", "dist/index.js"]Environment Configuration
# .env.local (development)
DATABASE_URL="postgresql://user:pass@localhost:5432/app_dev"
REDIS_URL="redis://localhost:6379"
JWT_SECRET="development-secret-change-in-prod"
LOG_LEVEL="debug"
# .env.test
DATABASE_URL="postgresql://user:pass@localhost:5432/app_test"
LOG_LEVEL="error"
# .env.production (via secrets management)
DATABASE_URL="${DATABASE_URL}"
REDIS_URL="${REDIS_URL}"
JWT_SECRET="${JWT_SECRET}"Environment validation:
import { z } from "zod";
const envSchema = z.object({
NODE_ENV: z.enum(["development", "test", "production"]),
DATABASE_URL: z.string().url(),
REDIS_URL: z.string().url().optional(),
JWT_SECRET: z.string().min(32),
PORT: z.coerce.number().default(3000),
});
export const env = envSchema.parse(process.env);---
Git Workflows
Trunk-Based Development
main (protected)
│
├── feature/user-auth (short-lived, 1-2 days max)
│ └── squash merge → main
│
├── feature/payment-flow
│ └── squash merge → main
│
└── release/v1.2.0 (cut from main for hotfixes)Branch naming:
feature/description- New featuresfix/description- Bug fixeschore/description- Maintenance tasksrelease/vX.Y.Z- Release branches
Commit Standards
Conventional Commits:
<type>(<scope>): <description>
[optional body]
[optional footer(s)]Types:
feat: New featurefix: Bug fixdocs: Documentationstyle: Formattingrefactor: Code restructuringtest: Adding testschore: Maintenance
Examples:
feat(auth): add password reset flow
Implement password reset with email verification.
Tokens expire after 1 hour.
Closes #123
---
fix(api): handle null response in user endpoint
The API was returning 500 when user profile was incomplete.
Now returns partial data with null fields.
---
chore(deps): update Next.js to 14.1.0
Breaking changes addressed:
- Updated Image component usage
- Migrated to new metadata APIPre-commit Hooks
// package.json
{
"scripts": {
"prepare": "husky install"
},
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,md}": ["prettier --write"]
}
}# .husky/pre-commit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
# .husky/commit-msg
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx commitlint --edit $1---
CI/CD Pipelines
GitHub Actions
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run type-check
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run test:unit
- run: npm run test:integration
env:
DATABASE_URL: postgresql://test:test@localhost:5432/test
- uses: codecov/codecov-action@v3
build:
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: build
path: dist/
deploy-preview:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: build
path: dist/
# Deploy to preview environment
- name: Deploy Preview
run: |
# Deploy logic here
echo "Deployed to preview-${{ github.event.pull_request.number }}"
deploy-production:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
needs: build
environment: production
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: build
path: dist/
- name: Deploy Production
run: |
# Production deployment
echo "Deployed to production"Database Migrations in CI
# Part of deploy job
- name: Run Migrations
run: |
npx prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
- name: Verify Migration
run: |
npx prisma migrate status---
Testing Strategies
Testing Pyramid
/\
/ \ E2E Tests (10%)
/ \ - Critical user journeys
/──────\
/ \ Integration Tests (20%)
/ \ - API endpoints
/────────────\ - Database operations
/ \
/ \ Unit Tests (70%)
/──────────────────\ - Components, hooks, utilitiesUnit Testing
// Component test with React Testing Library
import { render, screen, fireEvent } from "@testing-library/react";
import { UserForm } from "./UserForm";
describe("UserForm", () => {
it("submits form with valid data", async () => {
const onSubmit = vi.fn();
render(<UserForm onSubmit={onSubmit} />);
fireEvent.change(screen.getByLabelText(/email/i), {
target: { value: "test@example.com" },
});
fireEvent.change(screen.getByLabelText(/name/i), {
target: { value: "John Doe" },
});
fireEvent.click(screen.getByRole("button", { name: /submit/i }));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
email: "test@example.com",
name: "John Doe",
});
});
});
it("shows validation error for invalid email", async () => {
render(<UserForm onSubmit={vi.fn()} />);
fireEvent.change(screen.getByLabelText(/email/i), {
target: { value: "invalid" },
});
fireEvent.click(screen.getByRole("button", { name: /submit/i }));
expect(await screen.findByText(/invalid email/i)).toBeInTheDocument();
});
});Integration Testing
// API integration test
import { createTestClient } from "./test-utils";
import { db } from "@/lib/db";
describe("POST /api/users", () => {
beforeEach(async () => {
await db.user.deleteMany();
});
it("creates user with valid data", async () => {
const client = createTestClient();
const response = await client.post("/api/users", {
email: "new@example.com",
name: "New User",
});
expect(response.status).toBe(201);
expect(response.data.user.email).toBe("new@example.com");
// Verify in database
const user = await db.user.findUnique({
where: { email: "new@example.com" },
});
expect(user).toBeTruthy();
});
it("returns 409 for duplicate email", async () => {
await db.user.create({
data: { email: "existing@example.com", name: "Existing" },
});
const client = createTestClient();
const response = await client.post("/api/users", {
email: "existing@example.com",
name: "Duplicate",
});
expect(response.status).toBe(409);
expect(response.data.error.code).toBe("EMAIL_EXISTS");
});
});E2E Testing with Playwright
// e2e/auth.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Authentication", () => {
test("user can log in and access dashboard", async ({ page }) => {
await page.goto("/login");
await page.fill('[name="email"]', "user@example.com");
await page.fill('[name="password"]', "password123");
await page.click('button[type="submit"]');
await expect(page).toHaveURL("/dashboard");
await expect(page.locator("h1")).toHaveText("Welcome back");
});
test("shows error for invalid credentials", async ({ page }) => {
await page.goto("/login");
await page.fill('[name="email"]', "wrong@example.com");
await page.fill('[name="password"]', "wrongpassword");
await page.click('button[type="submit"]');
await expect(page.locator('[role="alert"]')).toHaveText(
"Invalid email or password"
);
});
});---
Code Review Process
PR Template
## Summary
<!-- Brief description of changes -->
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
## Changes Made
<!-- List specific changes -->
## Testing
- [ ] Unit tests added/updated
- [ ] Integration tests added/updated
- [ ] Manual testing completed
## Screenshots
<!-- If applicable -->
## Checklist
- [ ] Code follows style guidelines
- [ ] Self-review completed
- [ ] Documentation updated
- [ ] No new warningsReview Checklist
Functionality:
- Does the code do what it's supposed to?
- Are edge cases handled?
- Is error handling appropriate?
Code Quality:
- Is the code readable and maintainable?
- Are there any code smells?
- Is there unnecessary duplication?
Performance:
- Are there N+1 queries?
- Is caching used appropriately?
- Are there memory leaks?
Security:
- Is user input validated?
- Are there injection vulnerabilities?
- Is sensitive data protected?
---
Deployment Strategies
Blue-Green Deployment
Load Balancer
│
┌────────────┴────────────┐
│ │
┌────┴────┐ ┌─────┴────┐
│ Blue │ │ Green │
│ (Live) │ │ (Idle) │
└─────────┘ └──────────┘
1. Deploy new version to Green
2. Run smoke tests on Green
3. Switch traffic to Green
4. Blue becomes idle (rollback target)Canary Deployment
Load Balancer
│
┌────────────┴────────────┐
│ │
│ 95% 5% │
▼ ▼
┌─────────┐ ┌──────────┐
│ Stable │ │ Canary │
│ v1.0.0 │ │ v1.1.0 │
└─────────┘ └──────────┘
1. Deploy canary with small traffic %
2. Monitor error rates, latency
3. Gradually increase traffic
4. Full rollout or rollbackFeature Flags
// Feature flag service
const flags = {
newCheckoutFlow: {
enabled: true,
rolloutPercentage: 25,
allowedUsers: ["beta-testers"],
},
};
function isFeatureEnabled(flag: string, userId: string): boolean {
const config = flags[flag];
if (!config?.enabled) return false;
// Check allowed users
if (config.allowedUsers?.includes(userId)) return true;
// Check rollout percentage
const hash = hashUserId(userId);
return hash < config.rolloutPercentage;
}
// Usage
if (isFeatureEnabled("newCheckoutFlow", user.id)) {
return <NewCheckout />;
}
return <LegacyCheckout />;---
Monitoring and Observability
Structured Logging
import pino from "pino";
const logger = pino({
level: process.env.LOG_LEVEL || "info",
formatters: {
level: (label) => ({ level: label }),
},
});
// Request logging middleware
app.use((req, res, next) => {
const start = Date.now();
const requestId = req.headers["x-request-id"] || crypto.randomUUID();
res.on("finish", () => {
logger.info({
type: "request",
requestId,
method: req.method,
path: req.path,
statusCode: res.statusCode,
duration: Date.now() - start,
userAgent: req.headers["user-agent"],
});
});
next();
});
// Application logging
logger.info({ userId: user.id, action: "login" }, "User logged in");
logger.error({ err, orderId }, "Failed to process order");Metrics Collection
import { Counter, Histogram } from "prom-client";
const httpRequestsTotal = new Counter({
name: "http_requests_total",
help: "Total HTTP requests",
labelNames: ["method", "path", "status"],
});
const httpRequestDuration = new Histogram({
name: "http_request_duration_seconds",
help: "HTTP request duration",
labelNames: ["method", "path"],
buckets: [0.1, 0.3, 0.5, 1, 3, 5, 10],
});
// Middleware
app.use((req, res, next) => {
const end = httpRequestDuration.startTimer({
method: req.method,
path: req.route?.path || req.path,
});
res.on("finish", () => {
httpRequestsTotal.inc({
method: req.method,
path: req.route?.path || req.path,
status: res.statusCode,
});
end();
});
next();
});Health Checks
app.get("/health", async (req, res) => {
const checks = {
database: await checkDatabase(),
redis: await checkRedis(),
memory: checkMemory(),
};
const healthy = Object.values(checks).every((c) => c.status === "healthy");
res.status(healthy ? 200 : 503).json({
status: healthy ? "healthy" : "unhealthy",
checks,
timestamp: new Date().toISOString(),
});
});
async function checkDatabase() {
try {
await db.$queryRaw`SELECT 1`;
return { status: "healthy" };
} catch (error) {
return { status: "unhealthy", error: error.message };
}
}
function checkMemory() {
const used = process.memoryUsage();
const heapUsedMB = Math.round(used.heapUsed / 1024 / 1024);
const heapTotalMB = Math.round(used.heapTotal / 1024 / 1024);
return {
status: heapUsedMB < heapTotalMB * 0.9 ? "healthy" : "warning",
heapUsedMB,
heapTotalMB,
};
}---
Quick Reference
Daily Workflow
# 1. Start work
git checkout main && git pull
git checkout -b feature/my-feature
# 2. Develop with hot reload
docker-compose up -d
npm run dev
# 3. Test changes
npm run test
npm run lint
# 4. Commit
git add -A
git commit -m "feat(scope): description"
# 5. Push and create PR
git push -u origin feature/my-feature
gh pr createRelease Workflow
# 1. Ensure main is stable
git checkout main
npm run test:all
# 2. Create release
npm version minor # or major/patch
git push --follow-tags
# 3. Verify deployment
# CI/CD deploys automatically
# Monitor dashboardsFullstack Engineer — Forcing-Question Library
Discipline (Matt Pocock, derived from `engineering/grill-me`, MIT): walk these one at a time. Do not skip ahead. Do not bundle. Answers must be written down. If the user cannot answer one, that is your next investigation — stop and surface the gap.
These seven questions are the Matt Pocock grill that gates every meaningful fullstack decision (stack pick, scale step, build-vs-buy, monolith-vs-microservices). Each has a recommended answer with canon citation and a kill criterion — the condition under which the plan fails the question and the work should stop until the gap is closed.
---
Q1 — "What is your team size today, and what is the credible 12-month engineer headcount?"
Recommended answer: a single integer for today plus a single integer for month 12 (e.g., "4 engineers today, 9 in 12 months, hiring funded through Series A close"). Not a range. Not "growing."
Why it's the first question: every other architecture question collapses on this number. A team of 4 cannot operate a microservice fleet; a team of 80 cannot share a monorepo without tooling. Will Larson is explicit: org structure is the binding constraint on architecture, not the other way around.
Kill criterion: "we're starting microservices day one" with no 30+ engineer plan in 12 months — STOP. Re-plan as modular monolith. (Sam Newman's "MonolithFirst" rule, 2021.)
Canon: Will Larson, An Elegant Puzzle (2019); Sam Newman, Building Microservices 2e (2021); Melvin Conway, How Do Committees Invent? (1968).
---
Q2 — "What is your target deployment cadence — per-PR, daily, weekly, or quarterly?"
Recommended answer: a specific cadence with named bottleneck (e.g., "daily; bottleneck is QA sign-off, fix that first"). The cadence forces every downstream choice: CI/CD spend, environment count, feature-flag investment, observability budget.
Why it matters: the Accelerate program's research (Forsgren/Humble/Kim, 2018) shows elite-cadence teams (per-PR or daily) outperform low-cadence teams on every business metric. But a quarterly-cadence team buying microservice infrastructure has bought the cost without the benefit.
Kill criterion: quarterly cadence with a microservice fleet, or weekly cadence with no feature-flag strategy → STOP. Fix the bottleneck before the architecture investment.
Canon: Forsgren, Humble & Kim, Accelerate (2018); DORA State of DevOps Reports (2014–2024).
---
Q3 — "Customer-facing product, internal tool, or marketing site? Pick exactly one."
Recommended answer: one of the three, written down. "Sort of both" usually means the wrong stack is about to be chosen.
Why it matters: the cost shape is different for each. A marketing site on a Next.js + GraphQL + PostgreSQL stack is paying for capability it will never use. An internal tool built like a public product over-invests in a11y, perf, and theming the four-user audience does not need. A customer-facing product built like an internal tool ships a broken first impression.
Kill criterion: "marketing-site-but-also-the-product-someday" — STOP. Build the marketing site as static (Astro / Hugo / pure HTML). Build the product as a separate codebase. Joining them later is cheap; un-joining them is expensive.
Canon: Donald Reinertsen, Principles of Product Development Flow (2009), Principle E2 (queue length); Jeff Patton, User Story Mapping (2014), on product-vs-tool framing.
---
Q4 — "What is your one-year p50 and p99 traffic forecast in RPS or daily-active users?"
Recommended answer: two numbers grounded in evidence — current baseline × growth multiplier with a named source ("today 12 RPS p50 / 45 RPS p99 from CloudWatch last 30 days; +3× per board commit; year-1 target 36 / 135 RPS"). Not "we want to scale."
Why it matters: premature scale planning is the #1 fullstack cost overrun. Backends sized for unverified hyperscale waste >50% of cloud spend (Vogels, 2016, "10 lessons"). Frontends sized for ten users with edge CDNs / SSR-on-everything are paying for performance their audience does not measure.
Kill criterion: p99 forecast > 100× current with no evidence basis — STOP. Build for 3× headroom; instrument the actual curve.
Canon: Martin Kleppmann, Designing Data-Intensive Applications (2017), ch. 1 on capacity planning; Werner Vogels, 10 Lessons from 10 Years of AWS (2016).
---
Q5 — "Are you hiring against your stack, or training your existing team into it?"
Recommended answer: one of "hiring against it" (with named recruiter pipeline + active reqs) or "training" (with named senior who has shipped this stack before). "Both" is usually neither.
Why it matters: exotic-stack picks (Elm, F#, Erlang, OCaml in 2026) ship the architecture but kill the recruiting curve. A Java-only team picking Rust because "it's safer" pays 6–12 months of velocity tax. A Next.js team picking Remix because "RSCs are confusing" pays the migration tax and the team has no senior.
Kill criterion: stack pick with no on-team senior AND no active reqs AND no recruiter pipeline — STOP. Pick the stack the team has shipped before.
Canon: Camille Fournier, The Manager's Path (2017), ch. 8 on hiring-velocity tax; Andy Grove, High Output Management (1983), on training as throughput investment.
---
Q6 — "What is the year-one monthly cloud + SaaS budget ceiling?"
Recommended answer: a single dollar figure with sign-off (e.g., "$3.5K/mo through year 1, signed off by CFO"). Not "as low as possible." Not "we'll figure it out."
Why it matters: the default fullstack stack in 2026 (Vercel + Supabase + Sentry + LaunchDarkly + Datadog + Auth0 + Stripe + Linear) clears $4K/mo for any non-trivial product before a single customer pays. Without a ceiling, the team picks Big-Co defaults and ships before noticing the burn.
Kill criterion: no ceiling AND default-Vercel-and-five-add-ons stack — STOP. Either set the ceiling or accept the burn in writing (CFO sign-off in chat).
Canon: Tomasz Tunguz, Cloud Cost Discipline essays (2020–2024); Bessemer State of the Cloud (annual); Martin Casado & Sarah Wang, The Cost of Cloud, a Trillion Dollar Paradox (a16z, 2021).
---
Q7 — "What does done look like? Name the verifiable success criteria — three measurable metrics, each with a target."
Recommended answer: three concrete metrics with thresholds (e.g., "p95 API latency < 200ms; first-contentful-paint < 1.8s on mobile-4G; uptime ≥ 99.9% over rolling 30 days"). Each metric must be machine-checkable, not "feels fast."
Why it matters: this is Karpathy Principle #4 (Goal-Driven Execution). Without verifiable success criteria, the implementation "looks right" and ships before anyone notices the regressions. Karpathy's 2026 critique: the LLM (and the human team) cannot loop toward a target if no target is named.
Kill criterion: any answer that does not contain a number — STOP. "Fast," "scalable," "robust," "production-ready," "great UX" are not success criteria.
Canon: Andrej Karpathy, 4 Coding Principles (2026), Principle #4; Tom Gilb, Competitive Engineering (2005), on quantified-quality discipline.
---
How to use this library in a conversation
1. State the rule first — tell the user you'll walk seven questions, one at a time, before recommending any stack or architecture. 2. One question per turn. Never bundle. 3. Recommend the answer. Always cite the canon source for why this is the right shape. 4. Surface the kill criterion. If the user's answer trips it, stop and surface that gap. Do not proceed. 5. Track the answers. Write them to a working file (e.g., /tmp/fullstack-grill-<date>.md) so the conversation can resume. 6. After Q7, recommend the profile. Match the seven answers against the profile JSON files in ../profiles/ and pick the closest fit. Surface ≥ 2 close matches with the tradeoff between them.
Fullstack Tech Stack Guide
Technology selection guide with trade-offs, use cases, and integration patterns for modern fullstack development.
---
Table of Contents
- Frontend Frameworks
- Backend Frameworks
- Databases
- ORMs and Query Builders
- Authentication Solutions
- Deployment Platforms
- Stack Recommendations
---
Frontend Frameworks
Next.js
Best for: Production React apps, SEO-critical sites, full-stack applications
| Pros | Cons |
|---|---|
| Server components, streaming | Learning curve for advanced features |
| Built-in routing, API routes | Vercel lock-in concerns |
| Excellent DX and performance | Bundle size can grow |
| Strong TypeScript support | Complex mental model (client/server) |
When to choose:
- Need SSR/SSG for SEO
- Building a product that may scale
- Want full-stack in one framework
- Team familiar with React
// App Router pattern
// app/users/page.tsx
async function UsersPage() {
const users = await db.user.findMany(); // Server component
return <UserList users={users} />;
}
// app/users/[id]/page.tsx
export async function generateStaticParams() {
const users = await db.user.findMany();
return users.map((user) => ({ id: user.id }));
}React + Vite
Best for: SPAs, dashboards, internal tools
| Pros | Cons |
|---|---|
| Fast development with HMR | No SSR out of the box |
| Simple mental model | Manual routing setup |
| Flexible architecture | No built-in API routes |
| Smaller bundle potential | Need separate backend |
When to choose:
- Building internal dashboards
- SEO not important
- Need maximum flexibility
- Prefer decoupled frontend/backend
Vue 3
Best for: Teams transitioning from jQuery, progressive enhancement
| Pros | Cons |
|---|---|
| Gentle learning curve | Smaller ecosystem than React |
| Excellent documentation | Fewer enterprise adoptions |
| Single-file components | Composition API learning curve |
| Good TypeScript support | Two paradigms (Options/Composition) |
When to choose:
- Team new to modern frameworks
- Progressive enhancement needed
- Prefer official solutions (Pinia, Vue Router)
Comparison Matrix
| Feature | Next.js | React+Vite | Vue 3 | Svelte |
|---|---|---|---|---|
| SSR | Built-in | Manual | Nuxt | SvelteKit |
| Bundle size | Medium | Small | Small | Smallest |
| Learning curve | Medium | Low | Low | Low |
| Enterprise adoption | High | High | Medium | Low |
| Job market | Large | Large | Medium | Small |
---
Backend Frameworks
Node.js Ecosystem
Express.js
import express from "express";
import { userRouter } from "./routes/users";
const app = express();
app.use(express.json());
app.use("/api/users", userRouter);
app.listen(3000);| Pros | Cons |
|---|---|
| Minimal, flexible | No structure opinions |
| Huge middleware ecosystem | Callback-based (legacy) |
| Well understood | Manual TypeScript setup |
Fastify
import Fastify from "fastify";
const app = Fastify({ logger: true });
app.get("/users/:id", {
schema: {
params: { type: "object", properties: { id: { type: "string" } } },
response: { 200: UserSchema },
},
handler: async (request) => {
return db.user.findUnique({ where: { id: request.params.id } });
},
});| Pros | Cons |
|---|---|
| High performance | Smaller ecosystem |
| Built-in validation | Different plugin model |
| TypeScript-first | Less community content |
NestJS
@Controller("users")
export class UsersController {
constructor(private usersService: UsersService) {}
@Get(":id")
findOne(@Param("id") id: string) {
return this.usersService.findOne(id);
}
@Post()
@UseGuards(AuthGuard)
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
}| Pros | Cons |
|---|---|
| Strong architecture | Steep learning curve |
| Full-featured (GraphQL, WebSockets) | Heavy for small projects |
| Enterprise-ready | Decorator complexity |
Python Ecosystem
FastAPI
from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session
app = FastAPI()
@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int, db: Session = Depends(get_db)):
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404)
return user| Pros | Cons |
|---|---|
| Auto-generated docs | Python GIL limitations |
| Type hints → validation | Async ecosystem maturing |
| High performance | Smaller than Django ecosystem |
Django
| Pros | Cons |
|---|---|
| Batteries included | Monolithic |
| Admin panel | ORM limitations |
| Mature ecosystem | Async support newer |
Framework Selection Guide
| Use Case | Recommendation |
|---|---|
| API-first startup | FastAPI or Fastify |
| Enterprise backend | NestJS or Django |
| Microservices | Fastify or Go |
| Rapid prototype | Express or Django |
| Full-stack TypeScript | Next.js API routes |
---
Databases
PostgreSQL
Best for: Most applications, relational data, ACID compliance
-- JSON support
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
profile JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Full-text search
CREATE INDEX users_search_idx ON users
USING GIN (to_tsvector('english', email || ' ' || profile->>'name'));
SELECT * FROM users
WHERE to_tsvector('english', email || ' ' || profile->>'name')
@@ to_tsquery('john');| Feature | Rating |
|---|---|
| ACID compliance | Excellent |
| JSON support | Excellent |
| Full-text search | Good |
| Horizontal scaling | Requires setup |
| Managed options | Many (RDS, Supabase, Neon) |
MongoDB
Best for: Document-heavy apps, flexible schemas, rapid prototyping
// Flexible schema
const userSchema = new Schema({
email: { type: String, required: true, unique: true },
profile: {
name: String,
preferences: Schema.Types.Mixed, // Any structure
},
orders: [{ type: Schema.Types.ObjectId, ref: "Order" }],
});| Feature | Rating |
|---|---|
| Schema flexibility | Excellent |
| Horizontal scaling | Excellent |
| Transactions | Good (4.0+) |
| Joins | Limited |
| Managed options | Atlas |
Redis
Best for: Caching, sessions, real-time features, queues
// Session storage
await redis.set(`session:${sessionId}`, JSON.stringify(user), "EX", 3600);
// Rate limiting
const requests = await redis.incr(`rate:${ip}`);
if (requests === 1) await redis.expire(`rate:${ip}`, 60);
if (requests > 100) throw new TooManyRequestsError();
// Pub/Sub
redis.publish("notifications", JSON.stringify({ userId, message }));Database Selection Matrix
| Requirement | PostgreSQL | MongoDB | MySQL |
|---|---|---|---|
| Complex queries | Best | Limited | Good |
| Schema flexibility | Good (JSONB) | Best | Limited |
| Transactions | Best | Good | Good |
| Horizontal scale | Manual | Built-in | Manual |
| Cloud managed | Many | Atlas | Many |
---
ORMs and Query Builders
Prisma
Best for: TypeScript projects, schema-first development
// schema.prisma
model User {
id String @id @default(cuid())
email String @unique
posts Post[]
profile Profile?
createdAt DateTime @default(now())
}
// Usage - fully typed
const user = await prisma.user.findUnique({
where: { email: "user@example.com" },
include: { posts: true, profile: true },
});
// user.posts is Post[] - TypeScript knows| Pros | Cons |
|---|---|
| Excellent TypeScript | Generated client size |
| Schema migrations | Limited raw SQL support |
| Visual studio | Some edge case limitations |
Drizzle
Best for: SQL-first TypeScript, performance-critical apps
// Schema definition
const users = pgTable("users", {
id: uuid("id").primaryKey().defaultRandom(),
email: varchar("email", { length: 255 }).notNull().unique(),
createdAt: timestamp("created_at").defaultNow(),
});
// Query - SQL-like syntax
const result = await db
.select()
.from(users)
.where(eq(users.email, "user@example.com"))
.leftJoin(posts, eq(posts.userId, users.id));| Pros | Cons |
|---|---|
| Lightweight | Newer, smaller community |
| SQL-like syntax | Fewer integrations |
| Fast runtime | Manual migrations |
SQLAlchemy (Python)
# Model definition
class User(Base):
__tablename__ = "users"
id = Column(UUID, primary_key=True, default=uuid4)
email = Column(String(255), unique=True, nullable=False)
posts = relationship("Post", back_populates="author")
# Query
users = session.query(User)\
.filter(User.email.like("%@example.com"))\
.options(joinedload(User.posts))\
.all()---
Authentication Solutions
Auth.js (NextAuth)
Best for: Next.js apps, social logins
// app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import Credentials from "next-auth/providers/credentials";
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
GitHub,
Credentials({
credentials: { email: {}, password: {} },
authorize: async (credentials) => {
const user = await verifyCredentials(credentials);
return user;
},
}),
],
callbacks: {
jwt({ token, user }) {
if (user) token.role = user.role;
return token;
},
},
});| Pros | Cons |
|---|---|
| Many providers | Next.js focused |
| Session management | Complex customization |
| Database adapters | Breaking changes between versions |
Clerk
Best for: Rapid development, hosted solution
// Middleware
import { clerkMiddleware } from "@clerk/nextjs/server";
export default clerkMiddleware();
// Usage
import { auth } from "@clerk/nextjs/server";
export async function GET() {
const { userId } = await auth();
if (!userId) return new Response("Unauthorized", { status: 401 });
// ...
}| Pros | Cons |
|---|---|
| Beautiful UI components | Vendor lock-in |
| Managed infrastructure | Cost at scale |
| Multi-factor auth | Data residency concerns |
Custom JWT
Best for: Full control, microservices
// Token generation
function generateTokens(user: User) {
const accessToken = jwt.sign(
{ sub: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: "15m" }
);
const refreshToken = jwt.sign(
{ sub: user.id, version: user.tokenVersion },
process.env.REFRESH_SECRET,
{ expiresIn: "7d" }
);
return { accessToken, refreshToken };
}
// Middleware
function authenticate(req, res, next) {
const token = req.headers.authorization?.replace("Bearer ", "");
if (!token) return res.status(401).json({ error: "No token" });
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch {
res.status(401).json({ error: "Invalid token" });
}
}---
Deployment Platforms
Vercel
Best for: Next.js, frontend-focused teams
| Pros | Cons |
|---|---|
| Zero-config Next.js | Expensive at scale |
| Edge functions | Vendor lock-in |
| Preview deployments | Limited backend options |
| Global CDN | Cold starts |
Railway
Best for: Full-stack apps, databases included
| Pros | Cons |
|---|---|
| Simple deployment | Smaller community |
| Built-in databases | Limited regions |
| Good pricing | Fewer integrations |
AWS (ECS/Lambda)
Best for: Enterprise, complex requirements
| Pros | Cons |
|---|---|
| Full control | Complex setup |
| Cost-effective at scale | Steep learning curve |
| Any technology | Requires DevOps knowledge |
Deployment Selection
| Requirement | Platform |
|---|---|
| Next.js simplicity | Vercel |
| Full-stack + DB | Railway, Render |
| Enterprise scale | AWS, GCP |
| Container control | Fly.io, Railway |
| Budget startup | Railway, Render |
---
Stack Recommendations
Startup MVP
Frontend: Next.js 14 (App Router)
Backend: Next.js API Routes
Database: PostgreSQL (Neon/Supabase)
Auth: Auth.js or Clerk
Deploy: Vercel
Cache: Vercel KV or Upstash RedisWhy: Fastest time to market, single deployment, good scaling path.
SaaS Product
Frontend: Next.js 14
Backend: Separate API (FastAPI or NestJS)
Database: PostgreSQL (RDS)
Auth: Custom JWT + Auth.js
Deploy: Vercel (frontend) + AWS ECS (backend)
Cache: Redis (ElastiCache)
Queue: SQS or BullMQWhy: Separation allows independent scaling, team specialization.
Enterprise Application
Frontend: Next.js or React + Vite
Backend: NestJS or Go
Database: PostgreSQL (Aurora)
Auth: Keycloak or Auth0
Deploy: Kubernetes (EKS/GKE)
Cache: Redis Cluster
Queue: Kafka or RabbitMQ
Observability: Datadog or Grafana StackWhy: Maximum control, compliance requirements, team expertise.
Internal Tool
Frontend: React + Vite + Tailwind
Backend: Express or FastAPI
Database: PostgreSQL or SQLite
Auth: OIDC with corporate IdP
Deploy: Docker on internal infrastructureWhy: Simple, low maintenance, integrates with existing systems.
---
Quick Decision Guide
| Question | If Yes → | If No → |
|---|---|---|
| Need SEO? | Next.js SSR | React SPA |
| Complex backend? | Separate API | Next.js routes |
| Team knows Python? | FastAPI | Node.js |
| Need real-time? | Add WebSockets | REST is fine |
| Enterprise compliance? | Self-hosted | Managed services |
| Budget constrained? | Railway/Render | Vercel/AWS |
| Schema changes often? | MongoDB | PostgreSQL |
#!/usr/bin/env python3
"""
Code Quality Analyzer
Analyzes fullstack codebases for quality issues including:
- Code complexity metrics (cyclomatic complexity, cognitive complexity)
- Security vulnerabilities (hardcoded secrets, injection patterns)
- Dependency health (outdated packages, known vulnerabilities)
- Test coverage estimation
- Documentation quality
Usage:
python code_quality_analyzer.py /path/to/project
python code_quality_analyzer.py . --json
python code_quality_analyzer.py /path/to/project --output report.json
"""
import argparse
import json
import os
import re
import sys
from collections import defaultdict
from pathlib import Path
from typing import Dict, List, Optional, Tuple
# File extensions to analyze
FRONTEND_EXTENSIONS = {".ts", ".tsx", ".js", ".jsx", ".vue", ".svelte"}
BACKEND_EXTENSIONS = {".py", ".go", ".java", ".rb", ".php", ".cs"}
CONFIG_EXTENSIONS = {".json", ".yaml", ".yml", ".toml", ".env"}
ALL_CODE_EXTENSIONS = FRONTEND_EXTENSIONS | BACKEND_EXTENSIONS
# Skip patterns
SKIP_DIRS = {"node_modules", "vendor", ".git", "__pycache__", "dist", "build",
".next", ".venv", "venv", "env", "coverage", ".pytest_cache"}
# Security patterns to detect
SECURITY_PATTERNS = {
"hardcoded_secret": {
"pattern": r"(?:password|secret|api_key|apikey|token|auth)[\s]*[=:][\s]*['\"][^'\"]{8,}['\"]",
"severity": "critical",
"message": "Potential hardcoded secret detected"
},
"sql_injection": {
"pattern": r"(?:execute|query|raw)\s*\(\s*[f'\"].*\{.*\}|%s|%d|\$\d",
"severity": "high",
"message": "Potential SQL injection vulnerability"
},
"xss_vulnerable": {
"pattern": r"innerHTML\s*=|v-html",
"severity": "medium",
"message": "Potential XSS vulnerability - unescaped HTML rendering"
},
"unsafe_react_html": {
"pattern": r"__html",
"severity": "medium",
"message": "React unsafe HTML pattern detected - ensure content is sanitized"
},
"insecure_protocol": {
"pattern": r"http://(?!localhost|127\.0\.0\.1)",
"severity": "medium",
"message": "Insecure HTTP protocol used"
},
"debug_code": {
"pattern": r"console\.log|print\(|debugger|DEBUG\s*=\s*True",
"severity": "low",
"message": "Debug code should be removed in production"
},
"todo_fixme": {
"pattern": r"(?:TODO|FIXME|HACK|XXX):",
"severity": "info",
"message": "Unresolved TODO/FIXME comment"
}
}
# Code smell patterns
CODE_SMELL_PATTERNS = {
"long_function": {
"description": "Function exceeds recommended length",
"threshold": 50
},
"deep_nesting": {
"description": "Excessive nesting depth",
"threshold": 4
},
"large_file": {
"description": "File exceeds recommended size",
"threshold": 500
},
"magic_number": {
"pattern": r"(?<![a-zA-Z_])\b(?:[2-9]\d{2,}|\d{4,})\b(?![a-zA-Z_])",
"description": "Magic number should be named constant"
}
}
# Dependency vulnerability patterns (simplified - in production use a CVE database)
KNOWN_VULNERABLE_DEPS = {
"lodash": {"vulnerable_below": "4.17.21", "cve": "CVE-2021-23337"},
"axios": {"vulnerable_below": "0.21.2", "cve": "CVE-2021-3749"},
"minimist": {"vulnerable_below": "1.2.6", "cve": "CVE-2021-44906"},
"jsonwebtoken": {"vulnerable_below": "9.0.0", "cve": "CVE-2022-23529"},
}
def should_skip(path: Path) -> bool:
"""Check if path should be skipped."""
return any(skip in path.parts for skip in SKIP_DIRS)
def count_lines(filepath: Path) -> Tuple[int, int, int]:
"""Count total lines, code lines, and comment lines."""
try:
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
lines = f.readlines()
except Exception:
return 0, 0, 0
total = len(lines)
code = 0
comments = 0
in_block_comment = False
for line in lines:
stripped = line.strip()
if not stripped:
continue
# Block comments
if "/*" in stripped:
in_block_comment = True
if in_block_comment:
comments += 1
if "*/" in stripped:
in_block_comment = False
continue
# Line comments
if stripped.startswith(("//", "#", "--", "'")):
comments += 1
else:
code += 1
return total, code, comments
def calculate_complexity(content: str, language: str) -> Dict:
"""Calculate cyclomatic complexity estimate."""
# Count decision points
decision_patterns = [
r"\bif\b", r"\belse\b", r"\belif\b", r"\bfor\b", r"\bwhile\b",
r"\bcase\b", r"\bcatch\b", r"\b\?\b", r"\b&&\b", r"\b\|\|\b",
r"\band\b", r"\bor\b"
]
complexity = 1 # Base complexity
for pattern in decision_patterns:
complexity += len(re.findall(pattern, content, re.IGNORECASE))
# Count nesting depth
max_depth = 0
current_depth = 0
for char in content:
if char == "{":
current_depth += 1
max_depth = max(max_depth, current_depth)
elif char == "}":
current_depth = max(0, current_depth - 1)
return {
"cyclomatic": complexity,
"max_nesting": max_depth,
"rating": "low" if complexity < 10 else "medium" if complexity < 20 else "high"
}
def analyze_security(filepath: Path, content: str) -> List[Dict]:
"""Scan for security issues."""
issues = []
lines = content.split("\n")
for pattern_name, pattern_info in SECURITY_PATTERNS.items():
regex = re.compile(pattern_info["pattern"], re.IGNORECASE)
for line_num, line in enumerate(lines, 1):
if regex.search(line):
issues.append({
"file": str(filepath),
"line": line_num,
"type": pattern_name,
"severity": pattern_info["severity"],
"message": pattern_info["message"]
})
return issues
def analyze_dependencies(project_path: Path) -> Dict:
"""Analyze project dependencies for issues."""
findings = {
"package_managers": [],
"total_deps": 0,
"outdated": [],
"vulnerable": [],
"recommendations": []
}
# Check package.json
package_json = project_path / "package.json"
if package_json.exists():
findings["package_managers"].append("npm")
try:
with open(package_json) as f:
pkg = json.load(f)
deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
findings["total_deps"] += len(deps)
for dep, version in deps.items():
# Check against known vulnerabilities
if dep in KNOWN_VULNERABLE_DEPS:
vuln = KNOWN_VULNERABLE_DEPS[dep]
# Simplified version check
clean_version = re.sub(r"[^\d.]", "", version)
if clean_version and clean_version < vuln["vulnerable_below"]:
findings["vulnerable"].append({
"package": dep,
"current": version,
"fix_version": vuln["vulnerable_below"],
"cve": vuln["cve"]
})
except Exception:
pass
# Check requirements.txt
requirements = project_path / "requirements.txt"
if requirements.exists():
findings["package_managers"].append("pip")
try:
with open(requirements) as f:
lines = [l.strip() for l in f if l.strip() and not l.startswith("#")]
findings["total_deps"] += len(lines)
except Exception:
pass
# Check go.mod
go_mod = project_path / "go.mod"
if go_mod.exists():
findings["package_managers"].append("go")
return findings
def analyze_test_coverage(project_path: Path) -> Dict:
"""Estimate test coverage based on file analysis."""
test_files = []
source_files = []
for filepath in project_path.rglob("*"):
if should_skip(filepath) or not filepath.is_file():
continue
if filepath.suffix in ALL_CODE_EXTENSIONS:
name = filepath.stem.lower()
if "test" in name or "spec" in name or "_test" in name:
test_files.append(filepath)
elif not name.startswith("_"):
source_files.append(filepath)
source_count = len(source_files)
test_count = len(test_files)
# Estimate coverage ratio
if source_count == 0:
ratio = 0
else:
ratio = min(100, int((test_count / source_count) * 100))
return {
"source_files": source_count,
"test_files": test_count,
"estimated_coverage": ratio,
"rating": "good" if ratio >= 70 else "adequate" if ratio >= 40 else "poor",
"recommendation": None if ratio >= 70 else f"Consider adding more tests ({70 - ratio}% gap to target)"
}
def analyze_documentation(project_path: Path) -> Dict:
"""Analyze documentation quality."""
docs = {
"has_readme": False,
"has_contributing": False,
"has_license": False,
"has_changelog": False,
"api_docs": [],
"score": 0
}
readme_patterns = ["README.md", "README.rst", "README.txt", "readme.md"]
for pattern in readme_patterns:
if (project_path / pattern).exists():
docs["has_readme"] = True
docs["score"] += 30
break
if (project_path / "CONTRIBUTING.md").exists():
docs["has_contributing"] = True
docs["score"] += 15
license_patterns = ["LICENSE", "LICENSE.md", "LICENSE.txt"]
for pattern in license_patterns:
if (project_path / pattern).exists():
docs["has_license"] = True
docs["score"] += 15
break
changelog_patterns = ["CHANGELOG.md", "HISTORY.md", "CHANGES.md"]
for pattern in changelog_patterns:
if (project_path / pattern).exists():
docs["has_changelog"] = True
docs["score"] += 10
break
# Check for API docs
api_doc_dirs = ["docs", "documentation", "api-docs"]
for doc_dir in api_doc_dirs:
doc_path = project_path / doc_dir
if doc_path.is_dir():
docs["api_docs"].append(str(doc_path))
docs["score"] += 30
break
return docs
def analyze_project(project_path: Path) -> Dict:
"""Perform full project analysis."""
results = {
"summary": {
"files_analyzed": 0,
"total_lines": 0,
"code_lines": 0,
"comment_lines": 0
},
"languages": defaultdict(lambda: {"files": 0, "lines": 0}),
"security": {
"critical": [],
"high": [],
"medium": [],
"low": [],
"info": []
},
"complexity": {
"high_complexity_files": [],
"average_complexity": 0
},
"code_smells": [],
"dependencies": {},
"tests": {},
"documentation": {},
"overall_score": 100
}
complexity_scores = []
security_issues = []
# Analyze source files
for filepath in project_path.rglob("*"):
if should_skip(filepath) or not filepath.is_file():
continue
if filepath.suffix not in ALL_CODE_EXTENSIONS:
continue
results["summary"]["files_analyzed"] += 1
# Count lines
total, code, comments = count_lines(filepath)
results["summary"]["total_lines"] += total
results["summary"]["code_lines"] += code
results["summary"]["comment_lines"] += comments
# Track by language
lang = "typescript" if filepath.suffix in {".ts", ".tsx"} else \
"javascript" if filepath.suffix in {".js", ".jsx"} else \
"python" if filepath.suffix == ".py" else \
"go" if filepath.suffix == ".go" else "other"
results["languages"][lang]["files"] += 1
results["languages"][lang]["lines"] += code
# Read file content
try:
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
content = f.read()
except Exception:
continue
# Complexity analysis
complexity = calculate_complexity(content, lang)
complexity_scores.append(complexity["cyclomatic"])
if complexity["rating"] == "high":
results["complexity"]["high_complexity_files"].append({
"file": str(filepath.relative_to(project_path)),
"complexity": complexity["cyclomatic"],
"nesting": complexity["max_nesting"]
})
# Security analysis
issues = analyze_security(filepath.relative_to(project_path), content)
security_issues.extend(issues)
# Code smell: large file
if total > CODE_SMELL_PATTERNS["large_file"]["threshold"]:
results["code_smells"].append({
"file": str(filepath.relative_to(project_path)),
"type": "large_file",
"details": f"{total} lines (threshold: {CODE_SMELL_PATTERNS['large_file']['threshold']})"
})
# Categorize security issues
for issue in security_issues:
severity = issue["severity"]
results["security"][severity].append(issue)
# Calculate average complexity
if complexity_scores:
results["complexity"]["average_complexity"] = round(
sum(complexity_scores) / len(complexity_scores), 1
)
# Dependency analysis
results["dependencies"] = analyze_dependencies(project_path)
# Test coverage analysis
results["tests"] = analyze_test_coverage(project_path)
# Documentation analysis
results["documentation"] = analyze_documentation(project_path)
# Calculate overall score
score = 100
# Deduct for security issues
score -= len(results["security"]["critical"]) * 15
score -= len(results["security"]["high"]) * 10
score -= len(results["security"]["medium"]) * 5
score -= len(results["security"]["low"]) * 2
# Deduct for high complexity
score -= len(results["complexity"]["high_complexity_files"]) * 3
# Deduct for code smells
score -= len(results["code_smells"]) * 2
# Deduct for vulnerable dependencies
score -= len(results["dependencies"].get("vulnerable", [])) * 10
# Deduct for poor test coverage
if results["tests"].get("estimated_coverage", 0) < 50:
score -= 15
elif results["tests"].get("estimated_coverage", 0) < 70:
score -= 5
# Deduct for missing documentation
doc_score = results["documentation"].get("score", 0)
if doc_score < 50:
score -= 10
elif doc_score < 75:
score -= 5
results["overall_score"] = max(0, min(100, score))
results["grade"] = (
"A" if score >= 90 else
"B" if score >= 80 else
"C" if score >= 70 else
"D" if score >= 60 else "F"
)
# Generate recommendations
results["recommendations"] = generate_recommendations(results)
# Convert defaultdict to regular dict for JSON serialization
results["languages"] = dict(results["languages"])
return results
def generate_recommendations(analysis: Dict) -> List[Dict]:
"""Generate prioritized recommendations."""
recs = []
# Critical security issues
for issue in analysis["security"]["critical"][:3]:
recs.append({
"priority": "P0",
"category": "security",
"issue": issue["message"],
"file": issue["file"],
"action": f"Remove or secure sensitive data at line {issue['line']}"
})
# Vulnerable dependencies
for vuln in analysis["dependencies"].get("vulnerable", [])[:3]:
recs.append({
"priority": "P0",
"category": "security",
"issue": f"Vulnerable dependency: {vuln['package']} ({vuln['cve']})",
"action": f"Update to version {vuln['fix_version']} or later"
})
# High security issues
for issue in analysis["security"]["high"][:3]:
recs.append({
"priority": "P1",
"category": "security",
"issue": issue["message"],
"file": issue["file"],
"action": "Review and fix security vulnerability"
})
# Test coverage
tests = analysis.get("tests", {})
if tests.get("estimated_coverage", 0) < 50:
recs.append({
"priority": "P1",
"category": "quality",
"issue": f"Low test coverage: {tests.get('estimated_coverage', 0)}%",
"action": "Add unit tests to improve coverage to at least 70%"
})
# High complexity files
for cplx in analysis["complexity"]["high_complexity_files"][:2]:
recs.append({
"priority": "P2",
"category": "maintainability",
"issue": f"High complexity in {cplx['file']}",
"action": "Refactor to reduce cyclomatic complexity"
})
# Documentation
docs = analysis.get("documentation", {})
if not docs.get("has_readme"):
recs.append({
"priority": "P2",
"category": "documentation",
"issue": "Missing README.md",
"action": "Add README with project overview and setup instructions"
})
return recs[:10]
def print_report(analysis: Dict, verbose: bool = False) -> None:
"""Print human-readable report."""
print("=" * 60)
print("CODE QUALITY ANALYSIS REPORT")
print("=" * 60)
print()
# Summary
summary = analysis["summary"]
print(f"Overall Score: {analysis['overall_score']}/100 (Grade: {analysis['grade']})")
print(f"Files Analyzed: {summary['files_analyzed']}")
print(f"Total Lines: {summary['total_lines']:,}")
print(f"Code Lines: {summary['code_lines']:,}")
print(f"Comment Lines: {summary['comment_lines']:,}")
print()
# Languages
print("--- LANGUAGES ---")
for lang, stats in analysis["languages"].items():
print(f" {lang}: {stats['files']} files, {stats['lines']:,} lines")
print()
# Security
sec = analysis["security"]
total_sec = sum(len(sec[s]) for s in ["critical", "high", "medium", "low"])
print("--- SECURITY ---")
print(f" Critical: {len(sec['critical'])}")
print(f" High: {len(sec['high'])}")
print(f" Medium: {len(sec['medium'])}")
print(f" Low: {len(sec['low'])}")
if total_sec > 0 and verbose:
print(" Issues:")
for severity in ["critical", "high", "medium"]:
for issue in sec[severity][:3]:
print(f" [{severity.upper()}] {issue['file']}:{issue['line']} - {issue['message']}")
print()
# Complexity
cplx = analysis["complexity"]
print("--- COMPLEXITY ---")
print(f" Average Complexity: {cplx['average_complexity']}")
print(f" High Complexity Files: {len(cplx['high_complexity_files'])}")
print()
# Dependencies
deps = analysis["dependencies"]
print("--- DEPENDENCIES ---")
print(f" Package Managers: {', '.join(deps.get('package_managers', ['none']))}")
print(f" Total Dependencies: {deps.get('total_deps', 0)}")
print(f" Vulnerable: {len(deps.get('vulnerable', []))}")
print()
# Tests
tests = analysis["tests"]
print("--- TEST COVERAGE ---")
print(f" Source Files: {tests.get('source_files', 0)}")
print(f" Test Files: {tests.get('test_files', 0)}")
print(f" Estimated Coverage: {tests.get('estimated_coverage', 0)}% ({tests.get('rating', 'unknown')})")
print()
# Documentation
docs = analysis["documentation"]
print("--- DOCUMENTATION ---")
print(f" README: {'Yes' if docs.get('has_readme') else 'No'}")
print(f" LICENSE: {'Yes' if docs.get('has_license') else 'No'}")
print(f" CONTRIBUTING: {'Yes' if docs.get('has_contributing') else 'No'}")
print(f" CHANGELOG: {'Yes' if docs.get('has_changelog') else 'No'}")
print(f" Score: {docs.get('score', 0)}/100")
print()
# Recommendations
if analysis["recommendations"]:
print("--- RECOMMENDATIONS ---")
for i, rec in enumerate(analysis["recommendations"][:10], 1):
print(f"\n{i}. [{rec['priority']}] {rec['category'].upper()}")
print(f" Issue: {rec['issue']}")
print(f" Action: {rec['action']}")
print()
print("=" * 60)
def main():
parser = argparse.ArgumentParser(
description="Analyze fullstack codebase for quality issues",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s /path/to/project
%(prog)s . --verbose
%(prog)s /path/to/project --json --output report.json
"""
)
parser.add_argument(
"project_path",
nargs="?",
default=".",
help="Path to project directory (default: current directory)"
)
parser.add_argument(
"--json",
action="store_true",
help="Output in JSON format"
)
parser.add_argument(
"--output", "-o",
help="Write output to file"
)
parser.add_argument(
"--verbose", "-v",
action="store_true",
help="Show detailed findings"
)
args = parser.parse_args()
project_path = Path(args.project_path).resolve()
if not project_path.exists():
print(f"Error: Path does not exist: {project_path}", file=sys.stderr)
sys.exit(1)
analysis = analyze_project(project_path)
if args.json:
output = json.dumps(analysis, indent=2)
if args.output:
with open(args.output, "w") as f:
f.write(output)
print(f"Report written to {args.output}")
else:
print(output)
else:
print_report(analysis, args.verbose)
if args.output:
with open(args.output, "w") as f:
json.dump(analysis, f, indent=2)
print(f"\nDetailed JSON report written to {args.output}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
fullstack_decision_engine.py — Deterministic fullstack-stack picker with explicit kill criteria.
Stdlib-only. No LLM calls. Same input -> same output. Loads profile JSON files
from ../profiles/ and matches them against caller-supplied constraints, then
returns a ranked recommendation with named-approver chain, success thresholds,
and the kill criteria the choice trips (if any).
Karpathy discipline:
- #1 Think Before Coding: forces caller to supply --team-size, --cadence,
--user-facing, --budget — the four assumptions that decide the stack.
- #2 Simplicity First: does NOT scaffold anything. Picks the profile.
Scaffolding is the existing project_scaffolder.py's job.
- #3 Surgical Changes: prints a digest; never edits files.
- #4 Goal-Driven Execution: every recommendation prints verifiable success
thresholds (latency, uptime, LCP).
Matt Pocock discipline:
- Never auto-approves. Every output names the human(s) who must sign off.
- If two profiles tie within ~10%, the tool surfaces the tie and the
tradeoff — does not pick.
Usage:
python fullstack_decision_engine.py --help
python fullstack_decision_engine.py --sample
python fullstack_decision_engine.py \\
--team-size 6 --team-size-12mo 12 \\
--cadence daily --user-facing true --budget 5000 \\
--traffic-p99-rps 50 --data-sensitivity pii-only
python fullstack_decision_engine.py ... --output json
python fullstack_decision_engine.py --list-profiles
"""
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import dataclass, field, asdict
from pathlib import Path
from typing import Any
SCRIPT_DIR = Path(__file__).resolve().parent
PROFILES_DIR = SCRIPT_DIR.parent / "profiles"
@dataclass
class Inputs:
team_size: int
team_size_12mo: int
cadence: str
user_facing: bool
budget_usd_monthly: int
traffic_p99_rps: int
data_sensitivity: str
read_write_ratio: float
def kill_criteria_check(self) -> list[str]:
"""Return list of self-inconsistent inputs (Karpathy #1)."""
kills: list[str] = []
if self.team_size_12mo < self.team_size:
kills.append(
f"team shrinking ({self.team_size} -> {self.team_size_12mo}): "
"if intentional, plan for handoff/maintenance mode, not new architecture."
)
if self.cadence == "quarterly" and self.user_facing:
kills.append(
"quarterly cadence on a customer-facing product: fix the deployment "
"bottleneck before stack work (Forsgren/Humble/Kim, Accelerate 2018)."
)
if self.budget_usd_monthly < 200 and self.user_facing and self.traffic_p99_rps > 50:
kills.append(
f"budget ceiling ${self.budget_usd_monthly}/mo with {self.traffic_p99_rps} p99 RPS "
"customer-facing: math does not work; raise budget or reduce scope."
)
if self.data_sensitivity in ("phi", "pci") and self.team_size < 4:
kills.append(
f"data sensitivity {self.data_sensitivity!r} with team size {self.team_size}: "
"regulated workloads require named DPO + security owner + DBA review minimum."
)
return kills
@dataclass
class Match:
profile_name: str
score: float
matched_constraints: list[str] = field(default_factory=list)
violated_constraints: list[str] = field(default_factory=list)
profile_data: dict[str, Any] = field(default_factory=dict)
def load_profiles() -> dict[str, dict[str, Any]]:
profiles: dict[str, dict[str, Any]] = {}
if not PROFILES_DIR.exists():
return profiles
for p in sorted(PROFILES_DIR.glob("*.json")):
with p.open() as f:
data = json.load(f)
profiles[data.get("profile_name", p.stem)] = data
return profiles
def score_profile(profile: dict[str, Any], inputs: Inputs) -> Match:
"""Score how well a profile fits the inputs. 0.0–1.0."""
name = profile.get("profile_name", "unknown")
constraints = profile.get("constraints", {})
matched: list[str] = []
violated: list[str] = []
w_total = 0.0
w_matched = 0.0
def check(label: str, ok: bool, weight: float) -> None:
nonlocal w_total, w_matched
w_total += weight
if ok:
w_matched += weight
matched.append(label)
else:
violated.append(label)
if "team_size_max" in constraints:
check(
f"team_size <= {constraints['team_size_max']}",
inputs.team_size <= constraints["team_size_max"],
weight=2.0,
)
if "team_size_min" in constraints:
check(
f"team_size >= {constraints['team_size_min']}",
inputs.team_size >= constraints["team_size_min"],
weight=2.0,
)
if "team_size_year_one_max" in constraints:
check(
f"team_size_12mo <= {constraints['team_size_year_one_max']}",
inputs.team_size_12mo <= constraints["team_size_year_one_max"],
weight=1.5,
)
if "deployment_cadence" in constraints:
target = constraints["deployment_cadence"]
# Profile cadences are explicit alternatives joined by "-or-",
# e.g. "weekly-or-on-demand" → {"weekly", "on-demand"}.
# Modifier suffixes like "-with-gates" are stripped for matching.
allowed = {a.split("-with-")[0] for a in target.split("-or-")}
ok = inputs.cadence in allowed
check(f"cadence ~ {target}", ok, weight=1.5)
if "cloud_budget_monthly_usd_ceiling" in constraints:
check(
f"budget <= ${constraints['cloud_budget_monthly_usd_ceiling']}/mo",
inputs.budget_usd_monthly <= constraints["cloud_budget_monthly_usd_ceiling"],
weight=1.5,
)
if "user_facing" in constraints:
check(
f"user_facing = {constraints['user_facing']}",
inputs.user_facing == constraints["user_facing"],
weight=2.0,
)
if "data_sensitivity_tier" in constraints:
target = constraints["data_sensitivity_tier"]
ok = inputs.data_sensitivity in target or "or" in target
check(f"data_sensitivity ~ {target}", ok, weight=1.0)
if "read_write_ratio_min" in constraints:
check(
f"read_write_ratio >= {constraints['read_write_ratio_min']}",
inputs.read_write_ratio >= constraints["read_write_ratio_min"],
weight=1.0,
)
score = w_matched / w_total if w_total > 0 else 0.0
return Match(
profile_name=name,
score=score,
matched_constraints=matched,
violated_constraints=violated,
profile_data=profile,
)
def rank(profiles: dict[str, dict[str, Any]], inputs: Inputs) -> list[Match]:
matches = [score_profile(p, inputs) for p in profiles.values()]
matches.sort(key=lambda m: m.score, reverse=True)
return matches
def render_markdown(inputs: Inputs, matches: list[Match], kills: list[str]) -> str:
lines: list[str] = []
lines.append("# Fullstack Stack Decision")
lines.append("")
lines.append("## Inputs (your assumptions, Karpathy #1)")
lines.append("")
for k, v in asdict(inputs).items():
lines.append(f"- **{k}**: `{v}`")
lines.append("")
if kills:
lines.append("## Kill criteria tripped — STOP and resolve before proceeding")
lines.append("")
for k in kills:
lines.append(f"- {k}")
lines.append("")
if not matches:
lines.append("No profiles loaded. Check ../profiles/ exists.")
return "\n".join(lines)
top = matches[0]
second = matches[1] if len(matches) > 1 else None
lines.append("## Recommended profile")
lines.append("")
lines.append(f"**{top.profile_name}** — fit score {top.score:.0%}")
lines.append("")
lines.append(f"_{top.profile_data.get('description', '')}_")
lines.append("")
if top.matched_constraints:
lines.append("**Matched constraints:**")
for c in top.matched_constraints:
lines.append(f"- {c}")
lines.append("")
if top.violated_constraints:
lines.append("**Violated constraints (review before locking choice):**")
for c in top.violated_constraints:
lines.append(f"- {c}")
lines.append("")
if second and abs(top.score - second.score) < 0.15:
lines.append(
f"## Close runner-up: {second.profile_name} (fit {second.score:.0%}) — "
"tie within 15%; surface the tradeoff to the user before locking."
)
lines.append("")
stack = top.profile_data.get("stack_recommendations", {})
if stack:
lines.append("## Stack recommendation")
lines.append("")
lines.append("```json")
lines.append(json.dumps(stack, indent=2))
lines.append("```")
lines.append("")
anti = top.profile_data.get("anti_recommendations", {})
if anti:
lines.append("## Anti-patterns (DO NOT introduce these on this profile)")
lines.append("")
for k, v in anti.items():
lines.append(f"- **{k}** — {v}")
lines.append("")
thresholds = top.profile_data.get("success_thresholds", {})
if thresholds:
lines.append("## Verifiable success criteria (Karpathy #4)")
lines.append("")
for k, v in thresholds.items():
lines.append(f"- `{k}` = {v}")
lines.append("")
approvers = top.profile_data.get("named_approver_chain", {})
if approvers:
lines.append("## Named approvers (this tool NEVER auto-approves)")
lines.append("")
for k, v in approvers.items():
lines.append(f"- **{k}**: {v}")
lines.append("")
canon = top.profile_data.get("canon_references", [])
if canon:
lines.append("## Canon")
lines.append("")
for c in canon:
lines.append(f"- {c}")
lines.append("")
lines.append("---")
lines.append("")
lines.append(
"Next step: walk the 7 forcing questions in `references/forcing_questions.md` "
"with the user. Do NOT scaffold until every question has an answer."
)
return "\n".join(lines)
def render_json(inputs: Inputs, matches: list[Match], kills: list[str]) -> str:
out = {
"inputs": asdict(inputs),
"kill_criteria_tripped": kills,
"ranked_matches": [
{
"profile_name": m.profile_name,
"score": round(m.score, 4),
"matched_constraints": m.matched_constraints,
"violated_constraints": m.violated_constraints,
"stack_recommendations": m.profile_data.get("stack_recommendations", {}),
"anti_recommendations": m.profile_data.get("anti_recommendations", {}),
"success_thresholds": m.profile_data.get("success_thresholds", {}),
"named_approver_chain": m.profile_data.get("named_approver_chain", {}),
}
for m in matches
],
}
return json.dumps(out, indent=2)
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
description="Deterministic fullstack-stack picker. Matches inputs against profile JSON files; surfaces tradeoffs + kill criteria + named approvers. Never auto-approves.",
epilog="See ../references/forcing_questions.md for the 7-question grill that must be walked before this tool is run.",
)
p.add_argument("--team-size", type=int, help="Engineers today.")
p.add_argument("--team-size-12mo", type=int, help="Credible engineer count in 12 months.")
p.add_argument(
"--cadence",
choices=["per-pr", "daily", "weekly", "quarterly", "on-demand"],
help="Target deployment cadence.",
)
p.add_argument(
"--user-facing",
choices=["true", "false"],
help="Is the surface customer-facing (true) or internal/marketing (false)?",
)
p.add_argument("--budget", type=int, help="Monthly cloud + SaaS budget ceiling (USD).")
p.add_argument(
"--traffic-p99-rps",
type=int,
default=0,
help="One-year p99 traffic forecast (requests per second).",
)
p.add_argument(
"--data-sensitivity",
choices=["public", "internal", "pii-only", "pii", "phi", "pci", "regulated"],
default="public",
help="Data sensitivity tier.",
)
p.add_argument(
"--read-write-ratio",
type=float,
default=1.0,
help="Reads per write (>= 100 hints marketing-site profile).",
)
p.add_argument("--output", choices=["markdown", "json"], default="markdown")
p.add_argument("--list-profiles", action="store_true", help="List available profile names + exit.")
p.add_argument("--sample", action="store_true", help="Run with sample SaaS-startup inputs.")
return p
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
profiles = load_profiles()
if args.list_profiles:
if not profiles:
print("No profiles found in", PROFILES_DIR, file=sys.stderr)
return 1
for name, data in profiles.items():
print(f"{name}: {data.get('description', '')[:120]}")
return 0
if args.sample:
inputs = Inputs(
team_size=6,
team_size_12mo=12,
cadence="daily",
user_facing=True,
budget_usd_monthly=5000,
traffic_p99_rps=45,
data_sensitivity="pii-only",
read_write_ratio=4.0,
)
else:
required = [
("team_size", args.team_size),
("team_size_12mo", args.team_size_12mo),
("cadence", args.cadence),
("user_facing", args.user_facing),
("budget", args.budget),
]
missing = [name for name, val in required if val is None]
if missing:
print(
"Missing required inputs: " + ", ".join(missing),
file=sys.stderr,
)
print(
"Run with --sample to see a worked example, or --list-profiles to see profile names.",
file=sys.stderr,
)
return 2
inputs = Inputs(
team_size=args.team_size,
team_size_12mo=args.team_size_12mo,
cadence=args.cadence,
user_facing=(args.user_facing == "true"),
budget_usd_monthly=args.budget,
traffic_p99_rps=args.traffic_p99_rps,
data_sensitivity=args.data_sensitivity,
read_write_ratio=args.read_write_ratio,
)
kills = inputs.kill_criteria_check()
matches = rank(profiles, inputs)
if args.output == "json":
print(render_json(inputs, matches, kills))
else:
print(render_markdown(inputs, matches, kills))
return 0
if __name__ == "__main__":
sys.exit(main())
Related skills
How it compares
Use senior-fullstack for greenfield scaffolding and stack decisions; use karpathy-coder or api-design-reviewer for deep review on an existing codebase.
FAQ
Which stack templates does senior-fullstack support?
senior-fullstack project_scaffolder.py generates four templates: nextjs with App Router and Tailwind, fastapi-react with PostgreSQL, mern with TypeScript, and django-react with Django REST Framework plus a React frontend.
How does senior-fullstack pick a stack profile?
senior-fullstack fullstack_decision_engine.py requires team size, cadence, user-facing flag, and budget inputs, then ranks four built-in profiles and returns stack recommendations, SLO floors, and anti-patterns before scaffolding.
Is Senior Fullstack safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.