artifacts/bundle/skills/product-team/saas-scaffolder/SKILL.md
# SaaS Scaffolder **Tier:** POWERFUL **Category:** Product Team **Domain:** Full-Stack Development / Project Bootstrapping --- ## Input Format ``` Product: [name] Description: [1-3 sentences] Auth: nextauth | clerk | supabase Database: neondb | supabase | planetscale Payments: stripe | lemonsqueezy | none Features: [comma-separated list] ``` --- ## File Tree Output ``` my-saas/ ├── app/ │ ├── (auth)/ │ │ ├── login/page.tsx │ │ ├── register/page.tsx │ │ └── layout.tsx │
npx skillsauth add neekware/ehayeskills artifacts/bundle/skills/product-team/saas-scaffolderInstall 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.
Tier: POWERFUL
Category: Product Team
Domain: Full-Stack Development / Project Bootstrapping
Product: [name]
Description: [1-3 sentences]
Auth: nextauth | clerk | supabase
Database: neondb | supabase | planetscale
Payments: stripe | lemonsqueezy | none
Features: [comma-separated list]
my-saas/
├── app/
│ ├── (auth)/
│ │ ├── login/page.tsx
│ │ ├── register/page.tsx
│ │ └── layout.tsx
│ ├── (dashboard)/
│ │ ├── dashboard/page.tsx
│ │ ├── settings/page.tsx
│ │ ├── billing/page.tsx
│ │ └── layout.tsx
│ ├── (marketing)/
│ │ ├── page.tsx
│ │ ├── pricing/page.tsx
│ │ └── layout.tsx
│ ├── api/
│ │ ├── auth/[...nextauth]/route.ts
│ │ ├── webhooks/stripe/route.ts
│ │ ├── billing/checkout/route.ts
│ │ └── billing/portal/route.ts
│ └── layout.tsx
├── components/
│ ├── ui/
│ ├── auth/
│ │ ├── login-form.tsx
│ │ └── register-form.tsx
│ ├── dashboard/
│ │ ├── sidebar.tsx
│ │ ├── header.tsx
│ │ └── stats-card.tsx
│ ├── marketing/
│ │ ├── hero.tsx
│ │ ├── features.tsx
│ │ ├── pricing.tsx
│ │ └── footer.tsx
│ └── billing/
│ ├── plan-card.tsx
│ └── usage-meter.tsx
├── lib/
│ ├── auth.ts
│ ├── db.ts
│ ├── stripe.ts
│ ├── validations.ts
│ └── utils.ts
├── db/
│ ├── schema.ts
│ └── migrations/
├── hooks/
│ ├── use-subscription.ts
│ └── use-user.ts
├── types/index.ts
├── middleware.ts
├── .env.example
├── drizzle.config.ts
└── next.config.ts
// lib/auth.ts
import { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import { db } from "./db";
export const authOptions: NextAuthOptions = {
adapter: DrizzleAdapter(db),
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
callbacks: {
session: async ({ session, user }) => ({
...session,
user: {
...session.user,
id: user.id,
subscriptionStatus: user.subscriptionStatus,
},
}),
},
pages: { signIn: "/login" },
};
// db/schema.ts
import { pgTable, text, timestamp, integer } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
name: text("name"),
email: text("email").notNull().unique(),
emailVerified: timestamp("emailVerified"),
image: text("image"),
stripeCustomerId: text("stripe_customer_id").unique(),
stripeSubscriptionId: text("stripe_subscription_id"),
stripePriceId: text("stripe_price_id"),
stripeCurrentPeriodEnd: timestamp("stripe_current_period_end"),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
export const accounts = pgTable("accounts", {
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
type: text("type").notNull(),
provider: text("provider").notNull(),
providerAccountId: text("provider_account_id").notNull(),
refresh_token: text("refresh_token"),
access_token: text("access_token"),
expires_at: integer("expires_at"),
});
// app/api/billing/checkout/route.ts
import { NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { stripe } from "@/lib/stripe";
import { db } from "@/lib/db";
import { users } from "@/db/schema";
import { eq } from "drizzle-orm";
export async function POST(req: Request) {
const session = await getServerSession(authOptions);
if (!session?.user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { priceId } = await req.json();
const [user] = await db.select().from(users).where(eq(users.id, session.user.id));
let customerId = user.stripeCustomerId;
if (!customerId) {
const customer = await stripe.customers.create({ email: session.user.email! });
customerId = customer.id;
await db.update(users).set({ stripeCustomerId: customerId }).where(eq(users.id, user.id));
}
const checkoutSession = await stripe.checkout.sessions.create({
customer: customerId,
mode: "subscription",
payment_method_types: ["card"],
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?upgraded=true`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
subscription_data: { trial_period_days: 14 },
});
return NextResponse.json({ url: checkoutSession.url });
}
// middleware.ts
import { withAuth } from "next-auth/middleware";
import { NextResponse } from "next/server";
export default withAuth(
function middleware(req) {
const token = req.nextauth.token;
if (req.nextUrl.pathname.startsWith("/dashboard") && !token) {
return NextResponse.redirect(new URL("/login", req.url));
}
},
{ callbacks: { authorized: ({ token }) => !!token } },
);
export const config = {
matcher: ["/dashboard/:path*", "/settings/:path*", "/billing/:path*"],
};
# .env.example
NEXT_PUBLIC_APP_URL=http://localhost:3000
DATABASE_URL=postgresql://user:[email protected]/neondb?sslmode=require
NEXTAUTH_SECRET=generate-with-openssl-rand-base64-32
NEXTAUTH_URL=http://localhost:3000
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_PRO_PRICE_ID=price_...
The following phases must be completed in order. Validate at the end of each phase before proceeding.
.env.example created with all required variables✅ Validate: Run npm run build — no TypeScript or lint errors should appear.
🔧 If build fails: Check tsconfig.json paths and that all shadcn/ui peer dependencies are installed.
lib/db.ts✅ Validate: Run a simple db.select().from(users) in a test script — it should return an empty array without throwing.
🔧 If DB connection fails: Verify DATABASE_URL format includes ?sslmode=require for NeonDB/Supabase. Check that the migration has been applied with drizzle-kit push (dev) or drizzle-kit migrate (prod).
✅ Validate: Sign in via OAuth, confirm session user has id and subscriptionStatus. Attempt to access /dashboard without a session — you should be redirected to /login.
🔧 If sign-out loops occur in production: Ensure NEXTAUTH_SECRET is set and consistent across deployments. Add declare module "next-auth" to extend session types if TypeScript errors appear.
✅ Validate: Complete a Stripe test checkout using a 4242 4242 4242 4242 card. Confirm stripeSubscriptionId is written to the DB. Replay the checkout.session.completed webhook event and confirm idempotency (no duplicate DB writes).
🔧 If webhook signature fails: Use stripe listen --forward-to localhost:3000/api/webhooks/stripe locally — never hardcode the raw webhook secret. Verify STRIPE_WEBHOOK_SECRET matches the listener output.
✅ Validate: Run npm run build for a final production build check. Navigate all routes manually and confirm no broken layouts, missing session data, or hydration errors.
For additional guidance, generate the following companion reference files alongside the scaffold:
CUSTOMIZATION.md — Auth providers, database options, ORM alternatives, payment providers, UI themes, and billing models (per-seat, flat-rate, usage-based).PITFALLS.md — Common failure modes: missing NEXTAUTH_SECRET, webhook secret mismatches, Edge runtime conflicts with Drizzle, unextended session types, and migration strategy differences between dev and prod.BEST_PRACTICES.md — Stripe singleton pattern, server actions for form mutations, idempotent webhook handlers, Suspense boundaries for async dashboard data, server-side feature gating via stripeCurrentPeriodEnd, and rate limiting on auth routes with Upstash Redis + @upstash/ratelimit.Creator: Product Team License: MIT Source Repo:
neekware/ehaye-skillsSource Bucket:product-teamOriginal Path:product-team/saas-scaffolder
tools
# ehAye Multimedia Use this skill for **video, audio, images, media conversion, previews, transcription, thumbnails, frame extraction, Spotter visual search, or FFmpeg-backed processing**. Core rule: use ehAye native media tools first. Do not reach first for shell `ffmpeg`, `ffprobe`, Python, or `mediainfo` when a native media tool can do the job. Native tools use bundled engines, show proper tool UI, respect cancellation/timeouts, integrate with Preview/Spotter, and avoid cross-platform shell
development
Test-driven development skill for writing unit tests, generating test fixtures and mocks, analyzing coverage gaps, and guiding red-green-refactor workflows across Jest, Pytest, JUnit, Vitest, and Mocha. Use when the user asks to write tests, improve test coverage, practice TDD, generate mocks or stubs, or mentions testing frameworks like Jest, pytest, or JUnit. Handles test generation from source code, coverage report parsing (LCOV/JSON/XML), quality scoring, and framework conversion for TypeScript, JavaScript, Python, and Java projects.
tools
Help a user set up Telegram for ehAye Dojo. Default to Personal private bots (recommended). Group setup is advanced for teams/observers/demos.
development
# Writing Skills ## Overview **Writing skills IS Test-Driven Development applied to process documentation.** **Personal skills live in agent-specific directories (`~/.claude/skills` for Claude Code, `~/.agents/skills/` for Codex)** You write test cases (pressure scenarios with subagents), watch them fail (baseline behavior), write the skill (documentation), watch tests pass (agents comply), and refactor (close loopholes). **Core principle:** If you didn't watch an agent fail without the ski