Security
Password hashing, JWT sessions, webhook signature verification, Zod validation, RBAC, and rate limiting.
Password Hashing
Passwords are hashed with bcryptjs at cost factor 12. Plaintext passwords are never stored or logged.
import bcrypt from "bcryptjs"
const hash = await bcrypt.hash(password, 12)
const isValid = await bcrypt.compare(inputPassword, storedHash)API routes never return passwordHash — always use select to exclude it explicitly.
JWT Sessions
LaunchFst uses NextAuth v5 with the JWT strategy — no database sessions, no PrismaAdapter.
AUTH_SECRETmust be at least 32 bytes:openssl rand -base64 32- All user fields (id, role, plan, subscriptionStatus, etc.) are stored in the JWT token at sign-in
- The session callback reads from the token only — zero database queries on navigation
- Sessions revalidate every 15 minutes to pick up role/plan changes
- A lightweight
updatedAtcheck runs every 60 seconds to detect revoked sessions
Role-Based Access Control
Use session helpers in server components and API routes — never in middleware:
import { getRequiredSession } from "@/lib/auth-utils"
import { getAdminSession } from "@/lib/auth-utils"
import { getSuperAdminSession } from "@/lib/auth-utils"
// Any authenticated user — redirects to /login if not signed in
const session = await getRequiredSession()
// ADMIN role only (SUPERADMIN gets redirected to /dashboard/superadmin)
const session = await getAdminSession()
// SUPERADMIN role only (ADMIN gets redirected to /dashboard/admin)
const session = await getSuperAdminSession()All helpers are wrapped with React.cache() — multiple calls in the same request are deduplicated.
There is no middleware.ts. Route protection is enforced server-side via getRequiredSession() in server components and API routes.
Webhook Signature Verification
All three payment providers verify the HMAC signature before processing any webhook event:
LemonSqueezy — HMAC-SHA256 with LEMONSQUEEZY_WEBHOOK_SECRET:
const hmac = createHmac("sha256", process.env.LEMONSQUEEZY_WEBHOOK_SECRET!)
const digest = hmac.update(rawBody).digest("hex")
if (!timingSafeEqual(Buffer.from(signature), Buffer.from(digest))) {
return new Response("Invalid signature", { status: 401 })
}Polar — uses Webhooks.constructEvent() from @polar-sh/sdk/webhooks:
const webhooks = new Webhooks({ secret: process.env.POLAR_WEBHOOK_SECRET! })
const event = webhooks.constructEvent(payload)Stripe — uses stripe.webhooks.constructEvent():
const event = stripe.webhooks.constructEvent(rawBody, signature, process.env.STRIPE_WEBHOOK_SECRET!)Never process a webhook event without first verifying the signature.
Input Validation with Zod
Every API route validates request bodies with Zod before touching the database:
// Standard pattern: Auth → Zod → logic → response
export async function POST(req: Request) {
try {
const session = await getRequiredSession() // 1. Auth
const body = await req.json()
const data = createSchema.parse(body) // 2. Validate
const result = await db.myModel.create({ ... }) // 3. Logic
return NextResponse.json({ success: true, data: result })
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ success: false, error: error.issues[0].message },
{ status: 400 }
)
}
return NextResponse.json({ success: false, error: "Something went wrong" }, { status: 500 })
}
}SQL Injection Protection
All database queries use Prisma's parameterized query builder. Raw SQL is never used. Prisma's select requirement further limits data exposure.
XSS Protection
React escapes all rendered values by default. dangerouslySetInnerHTML is not used in the codebase. Email templates use @react-email/components which also handles escaping.
Rate Limiting
When the rate-limiting feature is enabled, API routes use Upstash Redis with three tiers:
| Tier | Limit | Use Case |
|---|---|---|
strict | 10/min | Auth endpoints (login, signup, password reset) |
standard | 30/min | General CRUD API routes |
lenient | 60/min | Read-heavy endpoints (search, listings) |
Gracefully degrades to allow-all when UPSTASH_REDIS_REST_URL is not set.
Environment Variable Safety
- Server-only secrets (
DATABASE_URL,AUTH_SECRET,STRIPE_SECRET_KEY, etc.) are never exposed to the client - Client-safe values use the
NEXT_PUBLIC_prefix only when they are safe to expose (provider keys, feature flags) - Never commit
.env.local— it is in.gitignore
CORS
Next.js does not add permissive CORS headers by default. API routes are same-origin unless you explicitly add Access-Control-Allow-Origin headers — which is not done in this codebase.