skills/project-scaffold/SKILL.md
Generate project boilerplate from scaffold templates. Use when creating a new project after stack selection. Triggers on: scaffold project, generate boilerplate, create from template.
npx skillsauth add mdmagnuson-creator/yo-go project-scaffoldInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
⛔ CRITICAL: Initial scaffold commit respects user preference
Trigger: Before running
git commitfor initial scaffold.Check: Ask user before committing: "Create initial commit? [Y/n]"
- If user confirms or default: Proceed with
git commit -m "Initial scaffold"- If user declines: Stop after
git add .— do not commitNote: New projects don't have
project.jsonyet, so we ask the user directly. After scaffolding, if the user setsgit.autoCommit: falseinproject.json, all future agent commits will require manual confirmation.
Generate project boilerplate based on selected stack archetype. This skill is invoked by project-bootstrap after stack selection.
This skill expects:
Match the selected stack to available scaffolds:
| Stack Archetype | Scaffold |
|-----------------|----------|
| nextjs-supabase | ~/.config/opencode/scaffolds/nextjs-supabase/ |
| nextjs-prisma | ~/.config/opencode/scaffolds/nextjs-prisma/ |
| go-chi-postgres | ~/.config/opencode/scaffolds/go-chi-postgres/ |
| remix-supabase | ~/.config/opencode/scaffolds/nextjs-supabase/ (adapt) |
| python-fastapi | ~/.config/opencode/scaffolds/python-fastapi/ |
If no exact scaffold match exists, use the closest archetype and adapt.
Read scaffold.yaml from the selected scaffold directory:
cat ~/.config/opencode/scaffolds/<scaffold-name>/scaffold.yaml
The configuration defines:
variables — User prompts for customizationdependencies — npm/go/pip packages to installconditionalDependencies — Feature-based additionsstructure — Directory tree to createfiles — Templates to renderpostScaffold — Commands to run after generationFor each variable in scaffold.yaml, use defaults from context or prompt user:
variables:
- name: projectName
prompt: "Project name"
transform: kebab-case
source: context.projectName # Auto-fill from bootstrap context
- name: description
prompt: "Project description"
source: context.description
- name: supabaseProjectId
prompt: "Supabase project ID (or 'local' for local dev)"
default: local
Auto-fill priority:
Create all directories defined in structure:
mkdir -p <project-path>/src/app
mkdir -p <project-path>/src/components/ui
mkdir -p <project-path>/src/hooks
mkdir -p <project-path>/src/lib/supabase
mkdir -p <project-path>/supabase/migrations
mkdir -p <project-path>/docs
For each file in the scaffold's files/ directory:
| Extension | Processing |
|-----------|------------|
| .hbs | Render with Handlebars, remove .hbs from output |
| .template | Render with Handlebars, remove .template from output |
| (no special extension) | Copy as-is |
const context = {
// From bootstrap/stack-advisor
projectName: 'my-project',
projectNamePascal: 'MyProject',
projectNameCamel: 'myProject',
description: 'A scheduling app',
// From StackDecision
stack: {
frontend: { framework: 'nextjs', version: '15' },
database: { provider: 'supabase', type: 'postgres' },
styling: { framework: 'tailwind', version: '4' },
auth: { provider: 'supabase' }
},
// From RequirementsManifest (if available)
features: {
authentication: true,
multiTenant: true,
payments: true,
email: false,
ai: false
},
// Entities for schema generation
entities: [
{ name: 'Organization', description: 'Tenant/workspace' },
{ name: 'Project', description: 'Work container' },
{ name: 'Task', description: 'Individual work item' }
],
// Computed helpers
hasPayments: capabilities.payments,
hasEmail: capabilities.email,
hasAI: capabilities.ai,
hasMultiTenant: capabilities.multiTenant,
// Date/time
year: '2026',
date: '2026-02-19'
};
for (const file of scaffoldConfig.files) {
const templatePath = `${scaffoldDir}/files/${file.template}`;
const outputPath = `${projectPath}/${file.output}`;
if (file.template.endsWith('.hbs')) {
const template = readFile(templatePath);
const rendered = handlebars.compile(template)(context);
writeFile(outputPath, rendered);
} else {
copyFile(templatePath, outputPath);
}
}
If RequirementsManifest.entities exists, generate initial migration:
Generate supabase/migrations/00001_initial_schema.sql:
-- Generated from spec entities
-- {{date}}
-- Organizations (multi-tenant core)
{{#if hasMultiTenant}}
CREATE TABLE organizations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE organization_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT 'member',
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(organization_id, user_id)
);
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
ALTER TABLE organization_members ENABLE ROW LEVEL SECURITY;
CREATE POLICY "org_member_select" ON organizations
FOR SELECT USING (
id IN (SELECT organization_id FROM organization_members WHERE user_id = auth.uid())
);
CREATE POLICY "org_member_select" ON organization_members
FOR SELECT USING (user_id = auth.uid());
{{/if}}
-- Entity tables
{{#each entities}}
{{#unless (isBuiltIn name)}}
CREATE TABLE {{snakeCase name}}s (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
{{#if ../hasMultiTenant}}
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
{{/if}}
name TEXT NOT NULL,
-- TODO: Add fields for {{description}}
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
ALTER TABLE {{snakeCase name}}s ENABLE ROW LEVEL SECURITY;
{{#if ../hasMultiTenant}}
CREATE POLICY "{{snakeCase name}}_org_access" ON {{snakeCase name}}s
FOR ALL USING (
organization_id IN (
SELECT organization_id FROM organization_members WHERE user_id = auth.uid()
)
);
{{/if}}
{{/unless}}
{{/each}}
Generate prisma/schema.prisma:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
name String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
{{#if hasMultiTenant}}
memberships OrganizationMember[]
{{/if}}
}
{{#if hasMultiTenant}}
model Organization {
id String @id @default(cuid())
name String
slug String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
members OrganizationMember[]
{{#each entities}}
{{#unless (isBuiltIn name)}}
{{camelCase name}}s {{pascalCase name}}[]
{{/unless}}
{{/each}}
}
model OrganizationMember {
id String @id @default(cuid())
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
organizationId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String
role String @default("member")
createdAt DateTime @default(now())
@@unique([organizationId, userId])
}
{{/if}}
{{#each entities}}
{{#unless (isBuiltIn name)}}
model {{pascalCase name}} {
id String @id @default(cuid())
{{#if ../hasMultiTenant}}
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
organizationId String
{{/if}}
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
{{/unless}}
{{/each}}
Combine base dependencies with conditional dependencies:
const allDeps = [...scaffoldConfig.dependencies.production];
const allDevDeps = [...scaffoldConfig.dependencies.development];
for (const cond of scaffoldConfig.conditionalDependencies) {
if (evaluateCondition(cond.if, context)) {
allDeps.push(...cond.add);
}
}
Generate or update package.json:
{
"name": "{{projectName}}",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"typecheck": "tsc --noEmit",
"test": "jest",
"test:e2e": "playwright test"
},
"dependencies": {
{{#each dependencies}}
"{{name}}": "{{version}}"{{#unless @last}},{{/unless}}
{{/each}}
},
"devDependencies": {
{{#each devDependencies}}
"{{name}}": "{{version}}"{{#unless @last}},{{/unless}}
{{/each}}
}
}
Generate go.mod:
module {{projectName}}
go 1.23
require (
github.com/go-chi/chi/v5 v5.0.12
github.com/jackc/pgx/v5 v5.5.5
{{#if hasAuth}}
github.com/golang-jwt/jwt/v5 v5.2.1
{{/if}}
)
.env.example# Database
{{#if (eq stack.database.provider 'supabase')}}
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
{{else}}
DATABASE_URL=postgresql://user:password@localhost:5432/{{projectName}}
{{/if}}
{{#if hasPayments}}
# Stripe
STRIPE_SECRET_KEY=sk_test_xxx
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
{{/if}}
{{#if hasEmail}}
# Email
RESEND_API_KEY=re_xxx
{{/if}}
{{#if hasAI}}
# AI
OPENAI_API_KEY=sk-xxx
{{/if}}
# App
NEXT_PUBLIC_APP_URL=http://localhost:3000
.env.local (gitignored)Copy .env.example to .env.local for immediate local development.
docs/project.jsonThis is handled by project-bootstrap, but scaffold ensures the structure:
{
"$schema": "https://opencode.ai/schemas/project.json",
"name": "{{projectName}}",
"description": "{{description}}",
"stack": { /* from StackDecision */ },
"features": { /* from context */ }
}
AGENTS.md# {{projectNamePascal}}
{{description}}
## Development
\`\`\`bash
npm run dev # Start dev server (port 3000)
npm run build # Production build
npm run test # Run tests
\`\`\`
## Stack
- **Frontend:** Next.js 15 (App Router)
- **Database:** {{stack.database.provider}}
- **Styling:** Tailwind CSS v4
- **Auth:** {{stack.auth.provider}}
## Documentation
- [Architecture](docs/ARCHITECTURE.md)
- [Conventions](docs/CONVENTIONS.md)
Execute commands defined in postScaffold:
postScaffold:
- command: npm install
workdir: .
- command: npx supabase init
condition: stack.database.provider == 'supabase'
- command: git init
- command: git add .
- command: git commit -m "Initial scaffold from {{scaffoldName}}"
requiresConfirmation: true
confirmationPrompt: "Create initial commit? [Y/n]"
Execution:
cd <project-path>
# Install dependencies
npm install
# Initialize Supabase (if applicable)
npx supabase init --workdir .
# Initialize git
git init
git add .
# Ask user before committing (requiresConfirmation: true)
# If user confirms: git commit -m "Initial scaffold from nextjs-supabase"
# If user declines: skip commit, files remain staged
Handling requiresConfirmation:
When a postScaffold command has requiresConfirmation: true:
confirmationPrompt to the userReport what was generated:
═══════════════════════════════════════════════════════════════════════
SCAFFOLD COMPLETE
═══════════════════════════════════════════════════════════════════════
✅ Created project: /Users/dev/code/my-project
📁 Directory structure:
src/
├── app/ Next.js App Router
├── components/ React components
├── hooks/ Custom hooks
└── lib/ Utilities + Supabase clients
supabase/
└── migrations/ Database migrations
docs/ Documentation
📦 Dependencies installed:
• next@15, react@19, react-dom@19
• @supabase/supabase-js, @supabase/ssr
• tailwindcss@4, clsx, tailwind-merge
• stripe, @stripe/stripe-js (payments feature)
🗃️ Database schema generated:
• organizations (multi-tenant)
• organization_members
• projects (from spec)
• tasks (from spec)
📝 Files created:
• package.json
• tsconfig.json
• next.config.ts
• tailwind.config.ts
• src/app/layout.tsx
• src/app/page.tsx
• src/lib/supabase/client.ts
• src/lib/supabase/server.ts
• supabase/migrations/00001_initial_schema.sql
• .env.example
• .env.local
• AGENTS.md
🚀 Next steps:
1. cd /Users/dev/code/my-project
2. Update .env.local with your Supabase credentials
3. Run `npm run dev` to start development
4. Review docs/drafts/prd-mvp.md for your user stories
═══════════════════════════════════════════════════════════════════════
The scaffold system provides these custom helpers:
| Helper | Description | Example |
|--------|-------------|---------|
| kebabCase | Convert to kebab-case | {{kebabCase projectName}} → my-project |
| camelCase | Convert to camelCase | {{camelCase name}} → myProject |
| pascalCase | Convert to PascalCase | {{pascalCase name}} → MyProject |
| snakeCase | Convert to snake_case | {{snakeCase name}} → my_project |
| upperCase | Convert to UPPER_CASE | {{upperCase name}} → MY_PROJECT |
| eq | Equality check | {{#if (eq type 'supabase')}} |
| isBuiltIn | Check if entity is built-in | {{#unless (isBuiltIn name)}} |
Built-in entities (skipped in schema generation):
❌ No scaffold found for archetype: django-postgres
Available scaffolds:
• nextjs-supabase
• nextjs-prisma
• go-chi-postgres
Would you like to:
A. Use closest match (nextjs-prisma) and adapt
B. Generate minimal structure only
C. Cancel
> _
⚠️ Missing required variable: supabaseProjectId
Enter Supabase project ID (or 'local' for local dev):
> _
⚠️ Post-scaffold command failed: npm install
Error: ENOENT: npm not found
The scaffold is complete but dependencies were not installed.
Run manually: cd /path/to/project && npm install
Return the scaffold result to the calling agent:
{
"success": true,
"projectPath": "/Users/dev/code/my-project",
"scaffold": "nextjs-supabase",
"filesCreated": [
"package.json",
"tsconfig.json",
"src/app/layout.tsx",
"..."
],
"dependenciesInstalled": true,
"gitInitialized": true,
"schemaGenerated": true,
"entities": ["Organization", "Project", "Task"]
}
data-ai
Generate verification contracts before delegating tasks to sub-agents, defining how success will be measured. Triggers on: verification contract, delegation contract, task verification, contract-first delegation.
testing
Verify that Vercel environment variables point to the correct Supabase project for each environment to prevent staging/production cross-wiring. Triggers on: vercel supabase check, environment alignment, env var check, supabase environment.
development
Manage codebase and database vectorization for semantic search. Use when initializing, refreshing, or querying the vector index. Triggers on: vectorize init, vectorize refresh, vectorize search, semantic search, vector index, enable vectorization.
testing
Patterns for XCUITest UI tests for native Apple apps (macOS/iOS). Use when writing or reviewing XCUITest tests for Swift apps. Triggers on: XCUITest, xcuitest, native app testing, Apple UI tests, SwiftUI tests, AppKit tests, UIKit tests.