---
overlay: Supastarter Specialization
parent_agent: Super Coder
description: "Supastarter boilerplate conventions"
---

## SUPASTARTER CODING GUIDELINES

You are working in a **Supastarter** monorepo (Next.js App Router). Follow these conventions exactly.

---

### MONOREPO STRUCTURE

```
apps/
  web/                    # Next.js SaaS app (App Router)
  mail-preview/           # Email template previewer
  docs/                   # Documentation site
packages/
  api/                    # oRPC procedures & Hono handlers
  auth/                   # Better Auth config
  database/               # Prisma + Drizzle schema & queries
  ui/                     # Shadcn UI components
  mail/                   # React Email templates
  payments/               # Stripe/Polar/other providers
  ai/                     # Vercel AI SDK wrappers
  i18n/                   # next-intl config & translations
  logs/                   # Logger utilities
  storage/                # S3/file storage
  utils/                  # Shared utilities
config/                   # Workspace-level config
tooling/                  # Build tooling & shared configs
```

---

### IMPORTS & PATH ALIASES

Always use path aliases. Never use deep relative imports.

```typescript
// Good
import { Button } from "@repo/ui/components/button";
import { cn } from "@repo/ui";
import { getSession } from "@saas/auth/lib/server";
import { db } from "@repo/database";

// Bad
import { Button } from "../../../packages/ui/components/button.tsx";
```

| Alias | Maps To |
|-------|---------|
| `@/*` | `apps/web/*` |
| `@marketing/*` | `apps/web/modules/marketing/*` |
| `@saas/*` | `apps/web/modules/saas/*` |
| `@shared/*` | `apps/web/modules/shared/*` |
| `@repo/*` | `packages/*` |

---

### REACT SERVER COMPONENTS

Default to Server Components. Only add `"use client"` for interactivity, browser APIs, hooks (`useQuery`, `useMutation`), or forms.

```typescript
// Server Component (default)
import { getSession } from "@saas/auth/lib/server";
import { redirect } from "next/navigation";

export default async function ProtectedPage() {
  const session = await getSession();
  if (!session) redirect("/auth/login");
  return <div>Welcome, {session.user.name}</div>;
}

// Client Component (only when necessary)
"use client";
import { useState } from "react";
export function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
```

Keep client components small and focused.

---

### MODULE ORGANIZATION

```
apps/web/modules/
  saas/
    auth/           # components/, hooks/, lib/, constants/, config.ts
    organizations/  # components/, hooks/, lib/
    payments/
    settings/
    admin/
    ai/
  shared/           # Cross-cutting components, hooks, lib
  marketing/        # Marketing-only components, lib
```

---

### API LAYER (oRPC)

Procedures live at `packages/api/modules/[feature]/procedures/[action].ts`:

```typescript
import { protectedProcedure } from "../../../orpc/procedures";
import { z } from "zod";
import { ORPCError } from "@orpc/client";

export const submitContactForm = publicProcedure
  .route({ method: "POST", path: "/contact", tags: ["Contact"] })
  .input(z.object({
    name: z.string().min(3),
    email: z.string().email(),
    message: z.string().min(10),
  }))
  .use(localeMiddleware)
  .handler(async ({ input, context }) => {
    // Implementation
  });
```

**Procedure types:** `publicProcedure` (no auth), `protectedProcedure` (requires session), `adminProcedure` (requires admin role).

**Client-side data fetching:**
```typescript
"use client";
import { useQuery } from "@tanstack/react-query";
import { orpc } from "@shared/lib/orpc-query-utils";

export function UsersList() {
  const { data, isLoading } = useQuery(orpc.users.list.queryOptions());
  if (isLoading) return <Skeleton />;
  return <ul>{data?.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
```

---

### DATABASE (Drizzle)

```typescript
import { eq } from "drizzle-orm";
import { db } from "../client";

export async function getUserById(id: string) {
  return await db.query.user.findFirst({
    where: (user, { eq }) => eq(user.id, id),
  });
}

export async function createUser(data: UserCreateInput) {
  const [{ id }] = await db
    .insert(user)
    .values({ ...data, createdAt: new Date(), updatedAt: new Date() })
    .returning({ id: user.id });
  return await getUserById(id);
}
```

Import from `@repo/database` exports — never raw Prisma imports.

---

### AUTH (Better Auth)

```typescript
// Server-side (ALWAYS use getSession from @saas/auth/lib/server)
const session = await getSession();
if (!session) redirect("/auth/login");

// Client-side
"use client";
import { useSession } from "@saas/auth/hooks/use-session";
const { user, loaded } = useSession();
```

Features: email/password, OAuth (Google, GitHub), magic link, passkey, 2FA, organization-scoped.

---

### UI COMPONENTS (Shadcn + CVA)

```typescript
import { cva } from "class-variance-authority";
import { cn } from "../lib";

const buttonVariants = cva(
  "flex items-center justify-center font-medium transition-colors",
  {
    variants: {
      variant: { primary: "bg-primary text-primary-foreground", secondary: "bg-secondary" },
      size: { sm: "h-6 px-3 text-xs", md: "h-9 px-4 text-sm", lg: "h-12 px-6 text-base" },
    },
    defaultVariants: { variant: "secondary", size: "md" },
  },
);
```

Use `cn()` for class merging. Use `<Button loading={...}>` prop for loading states.

---

### FORMS (React Hook Form + Zod)

```typescript
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@repo/ui/components/form";

const formSchema = z.object({ email: z.string().email(), name: z.string().min(3) });

export function ContactForm() {
  const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { email: "", name: "" } });
  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <FormField control={form.control} name="email"
          render={({ field }) => (
            <FormItem><FormLabel>Email</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
          )} />
        <Button type="submit" loading={form.formState.isSubmitting}>Submit</Button>
      </form>
    </Form>
  );
}
```

---

### I18N (next-intl)

```typescript
// Server Component
import { getTranslations, setRequestLocale } from "next-intl/server";
const t = await getTranslations();
return <h1>{t("home.welcome.title")}</h1>;

// Client Component
"use client";
import { useTranslations } from "next-intl";
const t = useTranslations();
```

---

### NAMING CONVENTIONS

| Type | Convention | Example |
|------|-----------|---------|
| Directories | kebab-case | `components/auth-form/` |
| Components | PascalCase.tsx | `LoginForm.tsx` |
| Functions | camelCase | `getUser()`, `handleSubmit()` |
| Booleans | is/has/can prefix | `isLoading`, `hasError` |
| Constants | SCREAMING_SNAKE | `MAX_RETRIES` |
| Types | PascalCase | `UserProps`, `AuthConfig` |

---

### TYPESCRIPT RULES

- Named exports only — never default exports
- `as const` assertions — never enums
- Strict typing — no `any` without justification
- Client env vars: prefix with `NEXT_PUBLIC_`
- Use `@repo/logs` logger — never `console.log` in production

---

### CONFIGURATION

```typescript
// apps/web/config.ts
export const config = {
  appName: "supastarter",
  saas: { enabled: true, redirectAfterSignIn: "/app" },
  marketing: { enabled: true },
} as const;

// Import: import { config } from "@/config";
```

Each package has scoped config. Import via `@repo/[package]/config`.

---

### BUILD & TOOLING

```bash
pnpm dev          # Start dev (Turbo)
pnpm build        # Build all packages
pnpm type-check   # TypeScript check
pnpm lint         # Biome lint
pnpm format       # Biome format
pnpm e2e          # Playwright E2E tests
```

---

### ANTI-PATTERNS

- Deep relative imports — use path aliases
- Default exports — use named exports
- Enums — use `as const` + type extraction
- `select('*')` — select only needed columns
- Raw Prisma imports — use `@repo/database`
- `console.log` — use `@repo/logs` logger
- Hardcoded strings — use i18n translations
- Missing `NEXT_PUBLIC_` prefix — server vars won't reach the client
- Singleton server Supabase clients — server clients are request-scoped
