SecureStartKit
SecurityFeaturesPricingDocsBlogChangelog
Sign inBuy Now
May 11, 2026·Security·SecureStartKit Team

The Security Architecture Most SaaS Templates Skip [2026]

Five architectural patterns most Next.js SaaS templates skip: backend-only access, Zod everywhere, RLS deny-all, signed webhooks, server-only imports.

Summarize with AI

On this page

  • Table of contents
  • What do "security patterns" mean in a SaaS template?
  • Pattern 1: Backend-only data access
  • Pattern 2: Zod validation on every Server Action
  • Pattern 3: RLS with deny-all defaults
  • Pattern 4: Signed webhook verification with idempotency
  • Pattern 5: The server-only import boundary
  • How do you audit a SaaS template for these 5 patterns?
  • What templates that skip these patterns get away with (for a while)

On this page

  • Table of contents
  • What do "security patterns" mean in a SaaS template?
  • Pattern 1: Backend-only data access
  • Pattern 2: Zod validation on every Server Action
  • Pattern 3: RLS with deny-all defaults
  • Pattern 4: Signed webhook verification with idempotency
  • Pattern 5: The server-only import boundary
  • How do you audit a SaaS template for these 5 patterns?
  • What templates that skip these patterns get away with (for a while)

The five security patterns most SaaS templates skip are: backend-only data access, Zod validation on every Server Action, Row Level Security with deny-all defaults, signed webhook verification with idempotency, and the server-only import boundary. Each prevents a breach class that has hit real Next.js plus Supabase apps in 2026. Most templates ship one or two of these. Almost none ship all five.

The result shows up in the data. A January 2026 scan of 20,052 indie launch URLs found 2,217 domains (11.04%) exposing Supabase credentials in their frontend code, and 2,325 critical exposures involving either a leaked service_role key or tables with no RLS at all [1]. The 170+ Lovable apps disclosed in CVE-2025-48757 fit the same pattern: a template's defaults shipped, and the defaults were wrong [2].

TL;DR:

  • The five patterns: backend-only data access, Zod on every Server Action, RLS deny-all defaults, signed Stripe webhooks with idempotency, server-only import guards.
  • What templates usually have: feature breadth (auth, billing, email, blog, dashboards), opinionated UI, OAuth scaffolding.
  • What templates usually skip: the boundary that keeps secrets out of the browser, the validation that treats every Server Action as a public endpoint, the deny-all RLS posture, signed webhooks, build-time enforcement of the server/client split.
  • Why this matters: ShipFast, Makerkit, Supastarter, and Divjoy each handle some of these. None ships all five with build-time enforcement.
  • How to audit: five greps and a lib/ directory check. Takes ten minutes.

Table of contents

  • What do "security patterns" mean in a SaaS template?
  • Pattern 1: Backend-only data access
  • Pattern 2: Zod validation on every Server Action
  • Pattern 3: RLS with deny-all defaults
  • Pattern 4: Signed webhook verification with idempotency
  • Pattern 5: The server-only import boundary
  • How do you audit a SaaS template for these 5 patterns?
  • What templates that skip these patterns get away with (for a while)

What do "security patterns" mean in a SaaS template?

A security pattern is an architectural commitment the template enforces, not a feature it ships. The distinction matters. "Has authentication" is a feature. "Authentication state is validated against a server-verified JWT before any privileged operation" is a pattern. Features are advertised on landing pages. Patterns are visible only in the code.

Most SaaS templates compete on features: how many OAuth providers, how many billing flows, how many UI components in the kit. Feature breadth is easy to ship. Architectural commitments are harder, because they constrain what developers can do downstream. A template that enforces backend-only data access cannot also offer a one-line client-side query helper. Templates tend to keep both, hedge their bets, and let the developer pick. The default usually ships unsafe.

The five patterns below are the ones that prevent specific breach classes that have already happened. They are not "best practices" in the abstract. Each one ties to a documented incident from the last 12 months. If a template ships without them, the template is moving the floor in the wrong direction, no matter how many UI components it includes.

Pattern 1: Backend-only data access

Backend-only data access means every database query runs on the server, never in a 'use client' file. The browser handles auth flows with the publishable key. The server handles every read and write with the service role key. The service_role key never enters the browser bundle, never appears in a network response, never gets logged client-side.

Most SaaS templates ship the opposite pattern. Open a typical Next.js + Supabase tutorial and you'll find React components calling supabase.from('table').select() directly from a 'use client' file. The argument is convenience: it's fewer files, fewer abstractions, less code to maintain. The trade is real: the browser bundle now contains the URL and anon key, and every table the anon role can read is reachable with curl from anywhere on the internet.

The Hacktron AI disclosure of CVE-2025-48757 ran the full attack chain on 170+ Lovable apps using exactly this pattern: read the bundle, extract credentials, query the tables that ship with permissive RLS [2]. The same scan in January 2026 found 2,325 apps with the equivalent problem: either a service role key leaked into the browser, or tables reachable with no RLS at all [1].

The fix is architectural, not a code review checklist item. Move every query into a Server Action, use a dedicated admin client for privileged reads and writes, and stop importing the browser client from anywhere that touches data. The full pattern, including the three-file setup most projects need, is covered in the backend-only data access guide.

Pattern 2: Zod validation on every Server Action

Server Actions are public HTTP endpoints. Anyone who can read your client bundle can call them, with whatever payload they want. This is not a bug in Next.js. It's the design. Server Actions are RPCs, and RPCs are reachable. The Next.js docs say so directly, and so does the canonical industry guide on Server Actions security: "Treat every 'use server' function as a public API endpoint" [5].

That means every action needs three things, in order: input validation, identity validation, authorization. Skip the first one, and you've shipped an endpoint that trusts whatever JSON the attacker sends. Zod is how the validation happens in practice. A schema at the top of every action file, a safeParse as the first line of every action body, and a hard return on parse failure before any database call.

Here's what the pattern looks like in practice:

'use server'

import { z } from 'zod'
import { createAdminClient, getUser } from '@/lib/supabase/server'

const updateProfileSchema = z.object({
  fullName: z.string().min(1).max(100),
  bio: z.string().max(500).optional(),
})

export async function updateProfile(data: z.infer<typeof updateProfileSchema>) {
  // 1. Validate input first, always
  const parsed = updateProfileSchema.safeParse(data)
  if (!parsed.success) return { error: 'Invalid input' }

  // 2. Validate identity from the cookie session, never from input
  const user = await getUser()
  if (!user) return { error: 'Unauthorized' }

  // 3. Run the actual mutation with the admin client
  const admin = createAdminClient()
  await admin
    .from('profiles')
    .update(parsed.data)
    .eq('id', user.id) // user.id from the session, never from form data
}

Three trust boundaries in 15 lines. The Authgear security guide identifies "failing to validate inputs on the server" as one of the three root causes behind every major Next.js security incident in 2026 [4]. Templates that ship Server Actions without a Zod-first culture leave that root cause wide open. For the full validation pattern, including how to share schemas between client and server forms, see the Server Actions and Zod guide. If you need to turn an existing TypeScript type into a Zod schema, the JSON-to-Zod converter does the scaffolding.

Pattern 3: RLS with deny-all defaults

Supabase enables Row Level Security on dashboard-created tables by default in 2026. That's progress. But "enabled with no policies" and "enabled with permissive policies" look identical from outside. The first denies all access. The second leaks. Most templates ship the second.

The reason: writing RLS policies is genuinely hard, and templates need to ship working demos. So the policies get written permissively. "Authenticated users can read their own profile" sounds correct. Then the policy doesn't filter by auth.uid() properly, or a column gets renamed, or a new feature adds a join the policy didn't anticipate, and now authenticated users can read every profile. The 83% of exposed Supabase databases that stem from RLS misconfigurations all started with a "working" policy that drifted [3].

The deny-all posture sidesteps this. Enable RLS on every table. Write zero policies. The anon and authenticated roles get nothing. The service_role role bypasses RLS regardless, so the Server Action layer still works. If a 'use client' file ever sneaks in a supabase.from('table') query, it returns an empty array instead of leaking data.

-- Enable RLS, write no policies, deny all from the browser
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.customers ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.purchases ENABLE ROW LEVEL SECURITY;

This is more restrictive than typical Supabase setups, where policies allow specific access patterns from authenticated clients. The trade: you give up the ability to query directly from React components. The benefit: an entire class of authorization bugs becomes impossible to introduce, because there is no policy to misconfigure. The Supabase docs are explicit that RLS is one layer of defense, not the only one [6]. The deny-all posture treats it like one.

If you need granular policies later (for example, to support a read-only public feed), the free Supabase RLS policy generator scaffolds them for common patterns. The RLS patterns guide covers the policies that hold up under attack.

Pattern 4: Signed webhook verification with idempotency

Stripe webhooks arrive over HTTPS from a known IP range. They are also trivially spoofable from anywhere if you don't verify the signature. The endpoint at /api/webhooks/stripe is publicly reachable. Anyone with curl can send a fake checkout.session.completed event with whatever metadata they want. If the endpoint marks orders as paid based on the event body without verifying the signature, the attacker just bought your product for free.

Verification is one function call. Stripe's SDK exposes stripe.webhooks.constructEvent(rawBody, signature, secret), which validates the HMAC signature against your endpoint secret and throws if the body has been tampered with or replayed. Templates that skip this either trust the event body directly, or use a higher-level wrapper that does the right thing only if you configure it correctly.

The second half of the pattern is idempotency. Stripe retries failed webhook deliveries. The same event can arrive five times. If your handler doesn't dedupe by event.id, a flaky network turns one purchase into five paid orders. The standard pattern: insert the event ID into a stripe_events table with a unique constraint before processing. If the insert fails with a duplicate key error, the event has already been processed. Return 200 and move on.

const event = stripe.webhooks.constructEvent(
  rawBody,
  signature,
  process.env.STRIPE_WEBHOOK_SECRET!
)

// Dedupe by event ID using a unique constraint
const admin = createAdminClient()
const { error: insertError } = await admin
  .from('stripe_events')
  .insert({ id: event.id, type: event.type })

if (insertError?.code === '23505') {
  return new Response('OK', { status: 200 }) // already processed
}

// Now process the event safely

This is the architecture every payment webhook needs. The Stripe payments guide walks through the full implementation, and the Stripe webhook verifier tests your signature setup against a known event before going live.

Pattern 5: The server-only import boundary

Code organization catches mistakes at code review. The server-only package catches them at build time, which is significantly better, because build-time failures don't depend on a reviewer being thorough at 11pm on a Friday.

The server-only package, maintained by the Next.js team, "poisons" a module so any client component that imports from it triggers a build error [7]. Add one line to the top of any file that should never reach the browser, and the bundler enforces the rule for you:

// lib/supabase/server.ts
import 'server-only'

import { createClient } from '@supabase/supabase-js'
// ... admin client setup using SUPABASE_SERVICE_ROLE_KEY

If a developer accidentally imports createAdminClient from a 'use client' file, Next.js refuses to compile the bundle and prints the offending import path. The mistake never makes it to staging, let alone production. This is the cheapest, highest-leverage piece of enforcement in the entire pattern stack. It costs nothing, requires no runtime overhead, and catches the most dangerous mistake (service role keys in the browser bundle) at the earliest possible point.

Templates that skip this rely on convention: a comment in the file, a section in the docs, a paragraph in the onboarding guide. Conventions get broken. Build errors do not. Of the five patterns, this one is the most striking when you read template source code: almost no commercial template uses it. The package is two years old, exists for exactly this purpose, and ships with Next.js itself. The omission is hard to explain.

How do you audit a SaaS template for these 5 patterns?

Five greps. Ten minutes. You don't need to run the template to find out whether it ships these patterns. The architecture is visible in the source.

Open the template's source in a terminal at the project root and run:

CheckCommandPass condition
Pattern 1: no browser queriesgrep -rn "supabase\.from|\.select(" --include="*.tsx" | grep -l "'use client'"Zero matches (or zero in files marked 'use client')
Pattern 2: Zod on Server Actionsgrep -rn "safeParse|\.parse(" actions/One per action file, near the top
Pattern 3: RLS deny-allOpen supabase/schema.sql or migrationsENABLE ROW LEVEL SECURITY on every table, fewest policies possible
Pattern 4: signed webhooksgrep -rn "stripe\.webhooks\.constructEvent" app/api/One match per webhook route
Pattern 5: server-only guardsgrep -rn "import 'server-only'" lib/At least one match in any module that imports SUPABASE_SERVICE_ROLE_KEY

Then one manual check: search every file for NEXT_PUBLIC_SUPABASE_SERVICE_ROLE. Any match is a critical fail, full stop. The service role key bypasses RLS entirely; if it has the NEXT_PUBLIC_ prefix, it ships in the browser bundle, and the template is giving every visitor full database access. This shouldn't happen. It does, especially in AI-generated code [1].

If you want a more structured pass before deploying your own app, the free SaaS security checklist tool walks through the audit interactively and flags what's missing. The Next.js security hardening checklist covers the surrounding hardening surface beyond these five patterns.

How do the popular templates score on this audit? Briefly: ShipFast defaults to MongoDB and leaves Supabase configuration to the developer, so the question doesn't fully apply, but its 'use client' data access patterns are common in user-shared code. Makerkit ships strong RLS coverage and tested policies, but its default is "RLS-first with permissive policies," not deny-all. Supastarter uses Supabase as a database only and requires manual RLS setup post-migration. Each handles a subset of the five. Side-by-side breakdowns live in the SaaS template comparison and the per-template pages: SecureStartKit vs ShipFast, vs Makerkit, vs Supastarter.

What templates that skip these patterns get away with (for a while)

Skipping these patterns doesn't break the demo. That's why templates skip them.

A template that ships client-side Supabase queries works perfectly until the first attacker reads the bundle. A template without Zod validation works perfectly until the first attacker sends {userId: "someone else"} in the form payload. A template with permissive RLS policies works perfectly until the first policy drift on a column rename. A template without webhook verification works perfectly until the first attacker sends a fake purchase event. A template without server-only works perfectly until the first developer copies the admin client import into a 'use client' file at 11pm.

The window between "works perfectly" and "in the news" varies. The SupaExplorer scan found that 11% of indie apps were already past it [1]. The Hacktron researchers walked into Lovable's office with the API keys of 170 of its customers' apps [2]. The Cognisys team has been seeing the same pattern across "a multitude of client engagements" [3]. The pattern is consistent: the breach class is well-understood, the fixes are well-documented, and templates keep shipping defaults that put new builders directly into the breach window.

This is the security-first foundation frame that SecureStartKit was built around. The five patterns above aren't optional features. They're the architectural decisions that determine whether your template is moving the floor up or down. SecureStartKit ships all five wired in by default: lib/supabase/server.ts uses import 'server-only', every Server Action validates with Zod first, RLS is enabled deny-all in supabase/schema.sql, and the Stripe webhook handler uses constructEvent with idempotency dedup. The site you're reading runs on it. That's the demo.

Frequently Asked Questions

What are the 5 security patterns most SaaS templates skip?
Backend-only data access (every query runs server-side), Zod validation on every Server Action, Row Level Security with deny-all defaults, signed webhook verification with idempotency dedup, and the server-only import boundary as a build-time guard. Each one prevents a known breach class that has hit real Next.js plus Supabase apps in 2026.
Why is backend-only data access more important than RLS policies?
RLS is one enforcement layer; backend-only access is the architectural commitment that makes RLS a safety net instead of a primary defense. A single misconfigured RLS policy can expose every row in a table. If the browser cannot query the database directly in the first place, the policy mistake has no attack surface to exploit.
How do I audit a SaaS template's security before buying?
Open the source and run five greps: search for createClient inside files marked use client (data queries in browser code is a fail), search for NEXT_PUBLIC_SUPABASE_SERVICE_ROLE (any match is a service role leak), search for safeParse and stripe.webhooks.constructEvent (missing means no Zod or no webhook verification), and check for import 'server-only' on admin client modules.
Does using a security-focused template eliminate the need for security review?
No. A template gives you secure defaults and an architecture that makes the wrong thing hard. You still have to review every Server Action you add, validate every input you accept, and audit dependencies as the codebase grows. The template moves the floor up. It does not remove the ceiling.

Built for developers who care about security

SecureStartKit ships with these patterns out of the box.

Backend-only data access, Zod validation on every input, RLS enabled, Stripe webhooks verified. One purchase, lifetime updates.

View PricingSee the template in action

References

  1. Vibe Coding Cybersecurity Insight Report (January 2026)— supaexplorer.com
  2. SupaPwn: Hacking Our Way into Lovable's Office and Helping Secure Supabase— hacktron.ai
  3. Supabase Leaks, What We Found— labs.cognisys.group
  4. Next.js Security Best Practices: Complete 2026 Guide— authgear.com
  5. Next.js Server Actions Security: 5 Vulnerabilities You Must Fix— makerkit.dev
  6. Row Level Security— supabase.com
  7. server-only - npm— npmjs.com

Related Posts

May 9, 2026·Security

Backend-Only Data Access in Next.js + Supabase [2026]

The architectural pattern that prevents Supabase data leaks. Server Actions, admin client, no NEXT_PUBLIC key for queries, ever.

Mar 3, 2026·Security

Vibe Coding Security Checklist: Audit AI Apps [2026]

Vibe coding tools like Cursor and v0 build apps fast, but they often ship vulnerabilities. Here is the technical audit checklist for Next.js and Supabase apps.

May 10, 2026·Security

Vibe Coding Security: The Complete 2026 Guide

AI tools like Lovable, Cursor, Bolt, and Replit ship insecure code. The 2026 breach pattern, bug categories, and the architectural fix.