Skip to content
Norcel
Back to blog
Architecture · · 7 min read · Norcel

Norcel OS: Treating Your SaaS Starter as an Operating System

A deep dive into the Norcel architecture philosophy — how treating your Next.js SaaS starter as an OS-level foundation changes the way you ship products.

Every SaaS founder reinvents the same foundation. Auth, sessions, roles, email, billing, database schema — the same five hundred lines of boilerplate, copy-pasted from a previous project or assembled from a dozen Stack Overflow answers. Norcel exists to end that cycle.

But Norcel isn’t just a “starter kit”. It’s better understood as an operating system for your SaaS product — a stable, well-defined platform that your application logic runs on top of, the same way an OS provides a kernel that apps don’t have to think about.

This post explains the philosophy and what it means for the code you write.

What an OS Actually Does

An operating system provides:

  1. A kernel — the non-negotiable, always-running foundation (process scheduling, memory management).
  2. Standard interfaces — system calls that application code uses without knowing the implementation.
  3. Security boundaries — the OS enforces who can access what. Applications don’t implement their own memory isolation.
  4. Extensibility — you build on top of it, you don’t fork it.

Now map that to a SaaS product:

OS ConceptSaaS Equivalent
KernelAuth, sessions, roles, database schema
System callsrequireAuth(), requireAdmin(), hasRole()
Security boundariesMiddleware, RBAC, HttpOnly cookies, CSRF protection
ExtensibilityYour product features, built on top

That’s Norcel OS. The auth layer is the kernel. The helper functions are the system calls. You build your product on top — you don’t rewrite the kernel.

The Kernel: Auth as Infrastructure

In Norcel, authentication is not a feature. It’s infrastructure. It runs on every request, it’s invisible when working, and it’s catastrophic when broken.

The auth kernel has four responsibilities:

1. Identity

// lib/auth/session.ts
export async function getCurrentUser(): Promise<User | null> {
  const session = await auth();
  if (!session?.user?.id) return null;

  return prisma.user.findUnique({
    where: { id: session.user.id },
    select: {
      id: true,
      email: true,
      role: true,
      emailVerified: true,
    },
  });
}

One function. Returns a typed User or null. Every server component and route handler that needs the current user calls this. No JWT parsing, no cookie decoding, no Prisma queries scattered across the codebase — all of that lives in the kernel.

2. Session Integrity

Sessions are stored in Postgres, not just in the JWT. This matters because:

  • A stolen JWT cannot be used after the user revokes it from another device.
  • Session rows have an expiresAt column — expiry is enforced at the database level, not just by JWT exp.
  • The middleware validates the session row on every protected request.
model UserSession {
  id        String   @id @default(cuid())
  userId    String
  expiresAt DateTime
  lastSeen  DateTime @default(now())
  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

Cascade delete means that deleting a user row automatically deletes all their sessions. No orphaned data, no stale sessions.

3. Authorization

// lib/auth/guards.ts
export async function requireAuth(): Promise<User> {
  const user = await getCurrentUser();
  if (!user) redirect('/login');
  return user;
}

export async function requireAdmin(): Promise<User> {
  const user = await requireAuth();
  if (user.role !== 'ADMIN') redirect('/403');
  return user;
}

export async function hasRole(role: UserRole): Promise<boolean> {
  const user = await getCurrentUser();
  return user?.role === role;
}

These are the system calls. A route handler or server component calls requireAdmin() and either gets a typed User back or the user is redirected. No if (!session) redirect() scattered across fifty files.

4. Middleware Security

// middleware.ts
export function middleware(req: NextRequest) {
  const sessionToken = req.cookies.get('__Secure-authjs.session-token');
  const isProtected = protectedPaths.some(path =>
    req.nextUrl.pathname.startsWith(path)
  );

  if (isProtected && !sessionToken) {
    const url = new URL('/login', req.url);
    url.searchParams.set('callbackUrl', req.nextUrl.pathname);
    return NextResponse.redirect(url);
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/settings/:path*', '/admin/:path*'],
};

The edge middleware is the kernel’s border guard. It checks the session cookie before the request ever reaches a page or route handler. Cheap, fast, and impossible to bypass.

Standard Interfaces: The System Call Layer

The most important architectural decision in Norcel is that the rest of your application never touches auth internals directly. No raw auth() calls scattered in components. No manual cookie parsing. No direct Prisma queries for user lookup.

You call the system calls:

// ✅ Correct — using the system call
export default async function DashboardPage() {
  const user = await requireAuth();
  // user is typed: { id, email, role, emailVerified }
  return <Dashboard user={user} />;
}

// ❌ Wrong — reaching into the kernel directly
export default async function DashboardPage() {
  const session = await auth();
  if (!session?.user) redirect('/login');
  const user = await prisma.user.findUnique({ where: { id: session.user.id } });
  // Now you have a different type shape, maybe missing fields...
}

The first pattern means that if the underlying auth implementation changes — say, switching from Auth.js to a custom JWT system — you change the kernel. The system call signatures stay the same. Your product code doesn’t change.

This is exactly how operating systems achieve longevity. macOS apps don’t break when Apple replaces the scheduler internals, because the system call interface is stable.

Security Boundaries

A real OS enforces security at the kernel level, not the application level. Apps can’t access each other’s memory — the OS makes that impossible, not just convention.

Norcel enforces the same principle:

  • HttpOnly cookies — JavaScript cannot read the session token. Not even your own JavaScript. This makes XSS attacks unable to steal sessions.
  • SameSite=Lax — Cross-site requests don’t automatically carry the session cookie. CSRF is structurally prevented.
  • CSRF tokens on mutations — For endpoints that change state, a CSRF token in the request body is validated server-side.
  • Role checks in the kernel — requireAdmin() performs the role check inside the guard. A page can’t accidentally skip it because the guard is in the call stack, not in the page component.
// This is impossible to call without triggering the role check:
export default async function AdminPage() {
  const user = await requireAdmin(); // Role enforced here, always
  return <AdminPanel />;
}

Contrast this with the common pattern of checking if (user.role !== 'ADMIN') redirect() in each admin page component — a pattern that guarantees someone will forget to add it to a new page someday.

Extensibility: Your Product Lives Above the Kernel

The OS metaphor breaks down if the kernel is too opinionated. Norcel’s kernel handles identity, sessions, authorization, and the database schema for auth entities. It does not dictate:

  • Your subscription model
  • Your billing tables
  • Your product’s data schema
  • Your UI design beyond auth pages
  • Your email templates beyond verification/reset

The Prisma schema has exactly the tables the kernel needs:

// Kernel tables — don't modify these
model User               { ... }
model UserSession        { ... }
model Account            { ... }       // OAuth providers
model VerificationToken  { ... }

// Your product tables — add them here
model Subscription { ... }
model Project      { ... }
model Usage        { ... }

Add your tables. Add your models. Extend the User with relations. The kernel stays stable.

The Upgrade Path

A real OS can be updated without reinstalling every application. Norcel aims for the same property.

When a security patch is needed — say, a new session invalidation strategy — you update the kernel files (lib/auth/session.ts, middleware.ts, lib/auth/guards.ts). Your product code, which only called requireAuth(), doesn’t change.

This is possible because the system call interface is stable. The implementation can change. The contract doesn’t.

Why This Matters for Shipping Speed

The typical SaaS authentication story:

  1. Week 1: Set up auth from scratch
  2. Week 2: Debug OAuth redirect loops
  3. Week 3: Implement email verification
  4. Week 4: Realize sessions aren’t revocable, rebuild
  5. Week 5: Add password reset
  6. Week 6: Add RBAC
  7. Week 7: Notice the CSRF hole, patch it
  8. Week 8: Finally start building the actual product

With Norcel OS, you clone the repo on Monday. By Tuesday you’re writing product code. The kernel was built correctly once — you don’t rebuild it per project.

Ship the boring parts right so you can ship the interesting parts fast.

Getting Started

The kernel is available in the Norcel starter. Clone it, run npm install, configure your environment, and start building your product above the kernel line.

git clone https://github.com/norcel/starter your-app
cd your-app
npm install
cp .env.example .env.local
# Configure DATABASE_URL, AUTH_SECRET, and OAuth credentials
npm run dev

Open http://localhost:3000/dashboard — you’ll be redirected to login. That’s the kernel working. From here, every page you add above /dashboard is automatically protected.

The OS is running. Build your product.

Keep reading

Next.js ·

How to Build a Production-Ready SaaS with Next.js

A practical walkthrough of the architecture, security, and infrastructure decisions that turn a Next.js prototype into a SaaS you can charge for.

Demo ·

Demo Blog: A Walkthrough of the Norcel Stack

A hands-on demo showing how Norcel's Next.js SaaS starter handles auth, database, email, and billing — from zero to production in a weekend.

Demo ·

Demo Blog 4: Scaling Edge Middleware & Background Jobs

A deep dive into building low-latency edge middlewares, handling async workloads, and keeping background workers rock solid in Next.js.