← Back to blog

Next.js from Scratch: Fundamentals Applied in a Real SaaS Project

This post covers Next.js from first principles, then shows exactly how each concept is applied in Senquel. If you've seen the framework name but never built with it, this is the grounded introduction. If you already know React, it explains what Next.js adds and why those additions matter for a production SaaS app.

1. What is Next.js?

React is a UI library. It gives you components, state, and rendering — but nothing else. To build a real app you also need routing, a server, API endpoints, and a way to ship optimised HTML to the browser. Next.js provides all of that, assembled into a single framework with sensible defaults.

What Next.js adds on top of React

React alone

  • Components and props
  • State and hooks
  • Virtual DOM rendering
  • JSX syntax

Next.js adds

  • File-based routing
  • Server-side rendering
  • API routes (built-in backend)
  • Middleware (runs before routes)
  • Image + font optimisation
  • TypeScript out of the box

2. Creating a Next.js app from scratch

One command bootstraps a complete project:

npx create-next-app@latest my-project
# Options you'll be asked:
# ✓ TypeScript? Yes
# ✓ ESLint? Yes
# ✓ Tailwind CSS? Yes
# ✓ App Router? Yes   ← always yes for new projects
# ✓ src/ directory? No (project root is cleaner)
# ✓ import alias @/*? Yes

This gives you a working dev server on localhost:3000. Run it with:

npm run dev

That's it. No Webpack config, no Babel setup, no manual routing library — the framework handles all of it.

3. The project structure

After scaffolding, your project looks like this. Senquel follows this structure exactly.

Project layout

my-project/
├── app/                  ← every page and API route lives here
│   ├── layout.tsx        ← root HTML shell (wraps every page)
│   ├── page.tsx          ← the / route
│   ├── blog/
│   │   ├── page.tsx      ← /blog
│   │   └── [slug]/
│   │       └── page.tsx  ← /blog/any-slug
│   ├── features/
│   │   └── project-creation/
│   │       └── page.tsx  ← /features/project-creation
│   └── api/
│       └── whatsapp/
│           └── webhook/
│               └── route.ts  ← POST /api/whatsapp/webhook
├── lib/                  ← shared utilities, Supabase helpers, types
├── public/               ← static files served as-is (images, videos)
├── middleware.ts          ← runs before every request
└── next.config.ts        ← framework configuration

The key rule: the folder path inside app/ is the URL. app/blog/[slug]/page.tsx becomes /blog/:slug. No separate router configuration — the file system is the router.

4. File-based routing in depth

Next.js App Router uses special filenames inside each folder. Understanding what each file does is the foundation of everything else.

Special filenames in App Router

File Purpose In Senquel
page.tsx Renders the UI for this route All feature pages, blog posts
layout.tsx Wraps child pages — persists across navigations Root HTML + font loading
route.ts API endpoint — exports GET, POST, etc. WhatsApp webhook, Xero OAuth, firm invite
loading.tsx Shown while the page is streaming Not used yet
[slug] Dynamic segment — value passed as a param app/blog/[slug]/page.tsx

5. Server vs Client components — the most important concept

Every component in Next.js App Router is a Server Component by default. Server Components run exclusively on the server — they never ship JavaScript to the browser, they can directly access databases, and they can be async functions. They cannot use hooks or browser APIs.

Add 'use client' at the top of a file to make it a Client Component. Client Components are sent to the browser as JavaScript — they support useState, useEffect, event listeners, and all of React's interactive APIs.

Server vs Client components

SERVER app/blog/[slug]/page.tsx async function BlogPost() // runs on server — no bundle cost const post = getPostBySlug(slug) // can be async, can query DB No JavaScript shipped to browser Can use async/await directly Can access env secrets safely Cannot use useState / useEffect BROWSER app/features/project-creation/page.tsx 'use client' // shipped as JS to the browser const [step, setStep] = useState(1) // interactive — responds to events useState, useEffect, event handlers Accesses browser APIs (window, etc) Cannot be async at top level Cannot hold secret env vars HTML

In Senquel: blog posts are Server Components (no JS cost). Feature pages are Client Components (interactive forms, state).

The practical rule: default to Server Components. Only add 'use client' when you need interactivity, hooks, or browser APIs. In Senquel, every page under app/features/ starts with 'use client' because they manage form state, show loading indicators, and respond to user events.

6. API Routes — the built-in backend

Any file named route.ts inside app/api/ becomes an HTTP endpoint. You export functions named after the HTTP methods you want to handle:

// app/api/whatsapp/webhook/route.ts

export async function GET(request: Request) {
  // Handles GET /api/whatsapp/webhook
  // Used by Meta to verify the webhook on setup
}

export async function POST(request: Request) {
  // Handles POST /api/whatsapp/webhook
  // Called by Meta/Twilio when a WhatsApp message arrives
}

These run on the server only — no browser JavaScript is involved. They can access secrets, call external APIs, write to the database. In Senquel, /api/whatsapp/webhook receives incoming WhatsApp messages, looks up the project by phone number, parses the command, and calls the appropriate Supabase helper.

How a WhatsApp message becomes a database write

WhatsApp Client sends /approve Meta / Twilio POSTs payload to webhook URL route.ts 1. Validate signature 2. Resolve project 3. Parse command 4. Write to DB Supabase Milestone status updated Reply sent back via API

7. Middleware — the request interceptor

The file middleware.ts at the project root runs before every matched request. It receives the request and returns either the original response (passing through), a redirect, or a rewrite. It runs at the edge — before any page component or API route handler executes.

// middleware.ts
export async function middleware(request: NextRequest) {
  // Redirect senquel.com/about → /demo/product-2
  const host = request.headers.get('host') ?? '';
  if (host.includes('senquel.com') && request.nextUrl.pathname === '/about') {
    return NextResponse.redirect(new URL('/demo/product-2', request.url));
  }

  // Auth guard — /features/* requires a signed-in user
  const { data: { user } } = await supabase.auth.getUser();
  if (path.startsWith('/features') && !user) {
    return NextResponse.redirect(new URL('/launch', request.url));
  }

  return supabaseResponse; // pass through
}

Request lifecycle with middleware

Browser GET /features/ project-pipeline middleware.ts Is user signed in? Yes → pass through No → redirect page.tsx Renders pipeline UI → /launch (login) signed in not signed in

The matcher config controls which paths trigger middleware. In Senquel, API routes are excluded so webhook handlers never get intercepted by the auth logic:

export const config = {
  matcher: [
    // Skip: static files, images, API routes
    '/((?!_next/static|_next/image|favicon.ico|api/|.*\.(?:svg|png|mp4)$).*)',
  ],
};

8. Data fetching — three patterns in Senquel

Next.js supports multiple data fetching strategies. Senquel uses three, each for a different context:

The three Supabase clients in Senquel

lib/supabase/client.ts createBrowserClient() Used in: 'use client' pages ✓ Respects user's auth session ✓ RLS enforced by user role ✗ Cannot access server secrets lib/supabase/server.ts createServerClient() Used in: middleware, Server Components ✓ Reads cookies from request ✓ Can refresh session server-side ✗ Still RLS-scoped to user lib/supabase/service.ts createServiceClient() Used in: API routes, webhooks ✓ Bypasses RLS entirely ✓ Can read any firm's data ✗ NEVER use in client code Supabase Postgres Row Level Security enforced except for service_role key

The critical security rule: the service client uses SUPABASE_SERVICE_ROLE_KEY — a secret that bypasses all Row Level Security. It must never appear in client-side code or be prefixed with NEXT_PUBLIC_. Senquel uses it only inside API route handlers where no browser can reach it.

9. Environment variables — public vs secret

Next.js has a simple but strict rule for environment variables:

Prefix Available in Senquel example
NEXT_PUBLIC_ Browser + server NEXT_PUBLIC_SUPABASE_URL
(no prefix) Server only — never reaches browser SUPABASE_SERVICE_ROLE_KEY
TWILIO_AUTH_TOKEN
ANTHROPIC_API_KEY

If you accidentally add NEXT_PUBLIC_ to a secret key, Next.js embeds it in the JavaScript bundle shipped to every visitor. This is a real security incident — check your prefixes carefully.

10. TypeScript and path aliases

Next.js ships with TypeScript support configured automatically. The @/ path alias (set in tsconfig.json) lets you import from the project root without long relative paths:

// Without alias — fragile, breaks when you move files
import { createProject } from '../../../lib/supabase/projects';

// With @/ alias — always resolves from project root
import { createProject } from '@/lib/supabase/projects';

Every file in Senquel uses the @/ alias. It's defined once in tsconfig.json:

// tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./*"]
    }
  }
}

11. The full Senquel request flow

Putting it all together: here's what happens when a designer opens the Milestone Tracker page.

GET /features/milestone-tracker — full flow

1. Browser Navigates to /features/milestone-tracker 2. middleware.ts getUser() → session found ✓ Pass through 3. Server Component (layout) Renders HTML shell — no JavaScript shipped Injects <Nav/>, global styles 4. Client Component ('use client') milestone-tracker/page.tsx hydrates in browser useEffect → createBrowserClient() → getProjectById() 5. Supabase Postgres RLS: returns only user's firm's projects Milestones, tasks, notes — single query

What to build next

If you're starting a new Next.js project from scratch, here's the sequence that works:

  1. Scaffold with create-next-app — TypeScript, App Router, Tailwind, yes to all.
  2. Add your database — Supabase gives you Postgres, auth, and real-time out of the box with Next.js SSR support via @supabase/ssr.
  3. Wire middleware — protect your auth-required routes immediately, before building features. It's two dozen lines and saves hours of debugging later.
  4. Build features as Client Components'use client' pages with useEffect for data fetching is the fastest path to a working UI. Optimise to Server Components later where it matters for performance.
  5. Add API routes for external integrations — webhooks, OAuth callbacks, third-party APIs. Keep secrets out of client code from day one.

The Senquel codebase follows this sequence exactly — the migrations folder tells the story of what was added in what order. If you want to read it from the beginning, start at supabase/migrations/001_multi_tenant.sql and work forward.

Comments

Sign in to leave a comment.