From saas-standards
USE WHEN: audit my SaaS app, set up auth, add route guards, pick a backend, which ORM should I use, review my onboarding flow, state management best practices. Enforces SaaS standards across auth, onboarding, DB, and state.
How this skill is triggered — by the user, by Claude, or both
Slash command
/saas-standards:saas-standardsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
**Before executing, check for user customizations at:**
Before executing, check for user customizations at:
.claude/saas-standards.local.md in the current project directory.
If this file exists, load and apply any preferences, stack choices, or overrides found there. These override default behavior. If the file does not exist, proceed with skill defaults.
SaaS Application Development Standards — Signup, Onboarding, Auth, Route Guards
Enforces non-negotiable standards for any SaaS application build. Prevents the #1 failure mode: building signup flows with only email/password and zero onboarding.
Activate when you see these patterns:
If you find yourself building a signup form with ONLY email + password and NO onboarding step, STOP IMMEDIATELY. You are violating SaaS standards. Read this skill.
Route to the appropriate workflow based on the request.
When executing a workflow, output this notification directly:
Running the **WorkflowName** workflow in the **SaaSStandards** skill to ACTION...
workflows/audit-signup-flow.mdworkflows/scaffold-auth.mdworkflows/audit-state-management.mdworkflows/select-data-layer.mdSaaS Standards maps to capability modules:
| SaaS Concern | Module | Key Orchestrators |
|---|---|---|
| Authentication | src/modules/auth/ | getAuthSession, requireAuth, requireOrg, requireOnboardedOrg |
| Tenant Profiles | src/modules/offices/ | getOfficeProfile, ensureOfficeProfile, setOnboardingComplete |
| Domain Features | src/modules/<domain>/ | Per-project (e.g., modules/analytics, modules/claims) |
All auth references in this skill mean @/modules/auth orchestrators, not @/lib/auth or direct provider imports.
Provider note: Clerk is the recommended default auth provider. These patterns work with any provider behind
modules/auth.
Route: /signup or /register
Required Fields:
Behaviors:
onboarding_completed: false on user record/onboarding (MANDATORY — never to dashboard)Password Standards (NIST 800-63B Rev 4):
Error Handling:
Done when:
Route: /onboarding (multi-step wizard)
This phase is NON-NEGOTIABLE. Users CANNOT access the dashboard until onboarding is complete.
| Field | Required | Step | Purpose |
|---|---|---|---|
| first_name | YES | 1 | Basic identity |
| last_name | YES | 1 | Basic identity |
| role / position | YES | 1 | Personalization, permissions |
| company_name / org_name | YES | 2 | Multi-tenancy, workspace |
| company_phone | YES | 2 | Contact, verification |
| company_location | YES | 2 | Timezone, compliance, locale |
| team_size | YES | 2 | Plan selection, feature gating |
| use_case / industry | RECOMMENDED | 3 | Personalization, analytics |
| how_did_you_hear | RECOMMENDED | 3 | Marketing attribution |
| Field | Required | Step | Purpose |
|---|---|---|---|
| practice_name | YES | 2 | Organization identity |
| practice_type | YES | 2 | Specialty (general, ortho, pedo, endo, etc.) |
| practice_phone | YES | 2 | Contact, appointment routing |
| practice_address | YES | 2 | Location, multi-site support |
| NPI_number | RECOMMENDED | 2 | Provider identification |
| number_of_providers | YES | 2 | Licensing, plan sizing |
| number_of_staff | YES | 2 | Seat-based licensing |
| primary_insurance_types | RECOMMENDED | 3 | Integration setup |
| practice_management_software | RECOMMENDED | 3 | Integration compatibility |
| HIPAA_BAA_accepted | YES | 3 | Compliance requirement |
User.onboarding_completed === true
REQUIRES:
- first_name IS NOT NULL
- last_name IS NOT NULL
- role IS NOT NULL
- organization IS NOT NULL (company_name OR practice_name)
- organization_phone IS NOT NULL
- organization_location IS NOT NULL
- team_size IS NOT NULL
Behaviors:
onboarding_completed: true, redirects to /dashboardsrc/modules/offices/ (or modules/tenancy/) owns:
Orchestrators:
getOfficeProfile(orgId): OfficeProfile | nullensureOfficeProfile(orgId, data): OfficeProfile (create-if-missing)setOnboardingComplete(orgId): voidAll keyed on orgId, never on provider-specific IDs.
Route guards MUST exist to enforce the signup → onboarding → dashboard flow.
function authMiddleware(request):
session = getAuthSession() // from @/modules/auth
if NOT session:
redirect("/login")
return
if NOT session.email_verified AND route != "/verify-email":
redirect("/verify-email")
return
if NOT session.onboarding_completed AND route != "/onboarding":
redirect("/onboarding")
return
// User is authenticated + onboarded → allow access
next()
| Route | Unauthenticated | Authenticated (no onboarding) | Fully Onboarded |
|---|---|---|---|
/signup | ALLOW | Redirect to /onboarding | Redirect to /dashboard |
/login | ALLOW | Redirect to /onboarding | Redirect to /dashboard |
/onboarding | Redirect to /login | ALLOW | Redirect to /dashboard |
/dashboard | Redirect to /login | Redirect to /onboarding | ALLOW |
/settings | Redirect to /login | Redirect to /onboarding | ALLOW |
/api/* | 401 | 403 | ALLOW |
Next.js (App Router):
// middleware.ts
import { getAuthSession } from '@/modules/auth'
export function middleware(request: NextRequest) {
const session = await getAuthSession()
const path = request.nextUrl.pathname
const publicRoutes = ['/signup', '/login', '/forgot-password']
const onboardingRoute = '/onboarding'
if (!session && !publicRoutes.includes(path)) {
return NextResponse.redirect(new URL('/login', request.url))
}
if (session && !session.onboardingCompleted && path !== onboardingRoute) {
return NextResponse.redirect(new URL('/onboarding', request.url))
}
if (session && session.onboardingCompleted && publicRoutes.includes(path)) {
return NextResponse.redirect(new URL('/dashboard', request.url))
}
}
Module-first note: Use
orgIdas the canonical tenant key in all tables. Never use provider-specific identifiers as column names. All schema examples below use generic IDs that work with any auth provider.
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
email_verified BOOLEAN DEFAULT FALSE,
onboarding_completed BOOLEAN DEFAULT FALSE,
platform_role VARCHAR(50), -- NULL for regular users, 'platform_admin' for app operators
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE profiles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
role VARCHAR(100) NOT NULL,
phone VARCHAR(20),
avatar_url TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE organizations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
phone VARCHAR(20) NOT NULL,
address_line1 VARCHAR(255),
city VARCHAR(100),
state VARCHAR(50),
zip VARCHAR(20),
country VARCHAR(100) DEFAULT 'US',
team_size INTEGER NOT NULL,
industry VARCHAR(100),
-- Healthcare specific (nullable for non-healthcare)
npi_number VARCHAR(20),
practice_type VARCHAR(100),
hipaa_baa_accepted BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE memberships (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
role VARCHAR(50) NOT NULL DEFAULT 'office_staff', -- office_owner, office_manager, office_staff
invited_by UUID REFERENCES users(id),
accepted_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
This phase determines HOW to implement the Phase 4 schema — which backend platform and which data access approach.
| Backend | Type | ORM Required | Auth Built-in |
|---|---|---|---|
| Supabase | Hosted Postgres + services | Yes (Drizzle or Prisma) | Yes |
| Neon | Serverless Postgres | Yes (Drizzle or Prisma) | Via Neon Auth |
| Convex | Reactive document DB | No (own TypeScript SDK) | No (BYO) |
Both Drizzle and Prisma are production-ready for TypeScript + Postgres. Selection depends on project needs:
| Factor | Drizzle | Prisma |
|---|---|---|
| Size | ~12 KB | ~1.6 MB |
| Approach | Code-first, SQL-like | Schema-first + codegen |
| RLS | Native support | Limited |
| Edge/Serverless | Best-in-class | Improved in v7 |
| Ecosystem | Growing, approaching v1.0 | Mature, v7.3 |
Neither is the default. Run the SelectDataLayer workflow to make an informed choice based on deployment target, team familiarity, and feature needs.
Convex is NOT Postgres. It uses TypeScript-native schema definitions (schema.ts) and query/mutation functions. When using Convex, skip ORM selection entirely — use Convex's own SDK. Real-time reactivity is built-in.
data-layer.mdworkflows/select-data-layer.mdDefault stack: Postgres (Supabase/Neon) + Drizzle ORM. Realtime (optional): Convex for incremental updates.
| Concern | Module | Backed By |
|---|---|---|
| Schema & migrations | modules/db | Postgres + Drizzle |
| Dashboard initial state | modules/analytics | Postgres |
| Activity feed (canonical) | modules/activity | Postgres |
| Live updates (optional) | modules/realtime | Convex |
Rules:
modules/db/internal/providers/modules/db/internal/providers/convex/ or modules/realtime/internal/providers/convex/These Playwright tests MUST exist for any SaaS app:
✅ Critical Path Tests (MANDATORY)
├── New user can sign up with email/password
├── After signup, user is redirected to /onboarding (NOT dashboard)
├── User cannot access /dashboard before completing onboarding
├── User cannot access /settings before completing onboarding
├── Onboarding collects all required fields (name, role, org, phone, location, team_size)
├── Onboarding blocks progression if required fields are empty
├── After onboarding completion, user is redirected to /dashboard
├── Returning user login redirects to /dashboard (if onboarded)
├── Returning user login redirects to /onboarding (if NOT onboarded)
├── Direct URL to /dashboard while unauthenticated redirects to /login
├── Direct URL to /dashboard while un-onboarded redirects to /onboarding
├── Duplicate email signup shows inline error
├── Weak password shows inline validation
└── App pages assume required fields exist (no null/undefined fallbacks needed)
State management is not one problem — it's five separate layers. Each needs the right tool.
This phase is DIAGNOSTIC, not prescriptive. It does NOT default to any single library.
| Layer | What It Is | Example Tools |
|---|---|---|
| Server State | Data from database/API (source of truth is server) | TanStack Query, SWR, Apollo, Server Components |
| Client State | UI-only state shared across components | Zustand, Jotai, Redux Toolkit, useState |
| URL State | Shareable, bookmarkable state | nuqs, useSearchParams |
| Form State | Input management and validation | React Hook Form + Zod, Conform, useActionState |
| Real-Time State | Server-pushed live updates | Supabase Realtime, Pusher, Socket.io, SSE |
Server state (database data) needs server state tools. TanStack Query gives you query deduplication, cache invalidation, optimistic mutations, and prefetch hydration. Zustand does NOT — it manages client-only state.
Client state (UI toggles, selections) needs client state tools. Zustand is lightweight and composable. TanStack Query is NOT for this — it manages server data caching.
Don't put everything in one tool. The #1 anti-pattern is using a single global store (Redux/Zustand) for server data, form state, UI toggles, and cached API responses.
Run the AuditStateManagement workflow when:
Full reference: state-management.md
Server Components for reads:
Server Actions for writes:
src/actions/<capability>/<useCase>.action.tsView Models:
RevenueTrendVM, PatientListVMWhen service graph complexity warrants it, consider Effect for dependency injection:
internal/src/runtime/layer.tsKey Topics:
Reference Documents:
signup-onboarding.mdroute-guards.mdhealthcare-saas.mddata-layer.mdstate-management.mdExample 1: Build a SaaS app
User: "Build a dental practice management SaaS"
-> Invokes ScaffoldAuth workflow
-> Creates signup (email+password), mandatory onboarding wizard, route guards
-> User receives auth system with proper user -> profile -> organization schema
Example 2: Audit existing signup flow
User: "Check if my app's signup flow meets standards"
-> Invokes AuditSignupFlow workflow
-> Checks for onboarding gate, required fields, route guards
-> User receives compliance report with specific violations and fixes
Example 3: Add onboarding to existing app
User: "My app goes straight from signup to dashboard, fix it"
-> Invokes ScaffoldAuth workflow
-> Adds onboarding wizard, route guard middleware, database schema updates
-> User gets mandatory onboarding before dashboard access
Example 4: Audit state management architecture
User: "What state management should I use for my SaaS?"
-> Invokes AuditStateManagement workflow
-> Interviews user about framework, data source, pain points, roadmap
-> Discovers: Next.js App Router + Supabase + planned real-time features
-> Recommends: TanStack Query (server state), Zustand (client UI state only), Supabase Realtime
-> Sets up chosen libraries with proper provider wiring
Example 5: State management setup for new project
User: "Set up state management for my app"
-> Invokes AuditStateManagement workflow
-> Interviews to understand which of the 5 state layers need tooling
-> Does NOT default to any library — recommends based on where state lives
-> Scaffolds chosen tools with query key factories, stores, or providers
Example 6: Backend and ORM selection
User: "What database should I use for my SaaS?"
-> Invokes SelectDataLayer workflow
-> Interviews about app type, real-time needs, deployment target, auth strategy
-> Discovers: edge deployment, exhausted Supabase free tier, need real-time
-> Presents backend comparison (Neon vs Convex) with free tier data
-> User selects Convex → provides Convex schema matching Phase 4 tables
-> No ORM selected (Convex uses own SDK — ORM doesn't apply)
Example 7: ORM selection for existing Supabase project
User: "Should I use Drizzle or Prisma with Supabase?"
-> Invokes SelectDataLayer workflow (ORM-focused path)
-> Interviews about deployment target, team familiarity, RLS needs
-> Presents neutral comparison table with selection criteria
-> User decides based on their constraints → provides matching schema + connection setup
npx claudepluginhub aojdevstudio/agentic-utilities --plugin saas-standardsCreates, edits, and verifies skills using a test-driven development approach with pressure scenarios and subagents.