<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: kai gramm</title>
    <description>The latest articles on DEV Community by kai gramm (@kaigrammm).</description>
    <link>https://dev.to/kaigrammm</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3807006%2Ff004011c-df61-41f6-8fcb-d17f2ae7ccce.png</url>
      <title>DEV Community: kai gramm</title>
      <link>https://dev.to/kaigrammm</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kaigrammm"/>
    <language>en</language>
    <item>
      <title>Building the Centralized Identity Hub: Secure API Handlers with Next.js &amp; Prisma (Part 2)</title>
      <dc:creator>kai gramm</dc:creator>
      <pubDate>Fri, 06 Mar 2026 10:01:12 +0000</pubDate>
      <link>https://dev.to/kaigrammm/building-the-centralized-identity-hub-secure-api-handlers-with-nextjs-prisma-part-2-5hml</link>
      <guid>https://dev.to/kaigrammm/building-the-centralized-identity-hub-secure-api-handlers-with-nextjs-prisma-part-2-5hml</guid>
      <description>&lt;p&gt;&lt;strong&gt;Introduction&lt;/strong&gt;&lt;br&gt;
In &lt;a href="https://dev.to/kaigrammm/stop-managing-users-manually-building-a-single-source-of-truth-with-nextjs-6k0"&gt;Part 1&lt;/a&gt;, we discussed the architectural necessity of a Single Source of Truth (SSoT) and designed a robust PostgreSQL schema using Prisma. We established that managing users manually across fragmented services is a liability.&lt;/p&gt;

&lt;p&gt;Today, we move from design to implementation. We will build the Identity Hub's engine using Next.js Route Handlers. Our goal is to create a secure, high-performance API that allows our "Spoke" applications (like Laravel services) to authenticate users and verify permissions without owning the data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The Security Handshake: Service-to-Service Auth&lt;/strong&gt;&lt;br&gt;
Since our Identity Hub is a private internal service, we cannot leave the API endpoints open. We need a way to ensure that only authorized "Spoke" applications can talk to our Next.js Hub.&lt;/p&gt;

&lt;p&gt;For this implementation, we will use a Secret Header-based Authentication (or an API Key).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { headers } from 'next/headers';

export function validateServiceSecret() {
  const headerList = headers();
  const apiKey = headerList.get('x-identity-shared-secret');

  if (!apiKey || apiKey !== process.env.INTERNAL_SERVICE_SECRET) {
    throw new Error('Unauthorized: Service Secret Mismatch');
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Implementing the Authentication Logic&lt;/strong&gt;&lt;br&gt;
The most critical endpoint is the login. It must verify credentials and return a scoped payload. Note how we use Prisma’s include to fetch roles and permissions in a single query—maintaining the performance we promised in Part 1.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
import bcrypt from 'bcryptjs';
import { validateServiceSecret } from '@/lib/auth-guard';

export async function POST(request: Request) {
  try {
    validateServiceSecret();
    const { email, password } = await request.json();

    const user = await prisma.user.findUnique({
      where: { email },
      include: {
        roles: {
          include: {
            role: {
              include: { permissions: { include: { permission: true } } }
            }
          }
        }
      }
    });

    if (!user || !user.isActive) {
      return NextResponse.json({ error: 'User not found or inactive' }, { status: 401 });
    }

    const isPasswordValid = await bcrypt.compare(password, user.passwordHash);
    if (!isPasswordValid) {
      return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
    }

    // Flattening permissions for the Spoke application
    const permissions = Array.from(new Set(
      user.roles.flatMap(ur =&amp;gt; ur.role.permissions.map(rp =&amp;gt; rp.permission.slug))
    ));

    return NextResponse.json({
      user: {
        id: user.id,
        email: user.email,
        fullName: user.fullName,
        permissions: permissions
      }
    });

  } catch (error: any) {
    return NextResponse.json({ error: error.message }, { status: 500 });
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Why This Logic Matters for Scalability&lt;/strong&gt;&lt;br&gt;
In a traditional setup, you’d be tempted to return the whole User object. By flattening the permissions into a simple string array (e.g., ['users.create', 'billing.view']), we:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reduce Payload Size: Important for high-traffic service-to-service calls.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Decouple Spoke Logic: The Laravel app doesn't need to know how the roles are mapped in the Hub; it only needs to know what the user is allowed to do.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Handling Atomicity with Prisma Transactions&lt;br&gt;
When creating a user, you must ensure they are assigned a role immediately. If the user is created but the role assignment fails, your "Single Source of Truth" is now corrupted.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We use Prisma Transactions to ensure an "all-or-nothing" execution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const newUser = await prisma.$transaction(async (tx) =&amp;gt; {
  const user = await tx.user.create({
    data: {
      email: data.email,
      passwordHash: hashedPw,
    }
  });

  await tx.userRole.create({
    data: {
      userId: user.id,
      roleId: defaultRoleId,
    }
  });

  return user;
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Performance: To Cache or Not to Cache?&lt;/strong&gt;&lt;br&gt;
In Part 1, we mentioned Redis. Since the Identity Hub will be hit every time a user logs in or performs a sensitive action in any Spoke app, database latency can become a bottleneck.&lt;/p&gt;

&lt;p&gt;The Strategy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Cache Permissions: Store the flattened permission array in Redis with a TTL (Time to Live).&lt;/li&gt;
&lt;li&gt;Invalidation: Use a Webhook or a simple API call to clear the Redis cache whenever a user's role is updated in the Hub.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What’s Next?&lt;/strong&gt;&lt;br&gt;
We have the Hub, the Security Guard, and the API logic. But how does a legacy service actually consume this?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In Part 3: Connecting Laravel to the Hub&lt;/strong&gt;, we will dive into the "Spoke" side. We will build a custom Guard in Laravel that bypasses its local database and validates everything against our Next.js Identity Hub.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question for the community:&lt;/strong&gt;&lt;br&gt;
How do you handle session invalidation across multiple apps when a user is banned in the Central Hub? Let's discuss in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>prisma</category>
      <category>postgres</category>
    </item>
    <item>
      <title>Stop Managing Users Manually: Building a Single Source of Truth with Next.js.</title>
      <dc:creator>kai gramm</dc:creator>
      <pubDate>Thu, 05 Mar 2026 04:23:50 +0000</pubDate>
      <link>https://dev.to/kaigrammm/stop-managing-users-manually-building-a-single-source-of-truth-with-nextjs-6k0</link>
      <guid>https://dev.to/kaigrammm/stop-managing-users-manually-building-a-single-source-of-truth-with-nextjs-6k0</guid>
      <description>&lt;p&gt;&lt;strong&gt;Series: Centralized User Management (Part 1: Architecture &amp;amp; Database)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the first part of my series: Mastering Centralized User Management. Here is what we will cover:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 1: The Architecture &amp;amp; Database Design (Current)&lt;/strong&gt;&lt;br&gt;
Part 2: Building the Next.js API Hub&lt;br&gt;
Part 3: Connecting Laravel to the Hub&lt;br&gt;
Part 4: Syncing &amp;amp; Advanced Features&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Introduction: The Operational Risk of Fragmented Identity
&lt;/h3&gt;

&lt;p&gt;In the lifecycle of a growing enterprise ecosystem, there is a recurring point of failure that often goes unaddressed until it becomes a critical liability: fragmented identity management. When an organization operates multiple independent applications — such as a suite of legacy Laravel services — the default path is often to allow each service to manage its own user table, authentication logic, and authorization rules.&lt;/p&gt;

&lt;p&gt;This approach introduces significant operational risks. From a security perspective, it creates “access drift,” where a user terminated in one system retains active sessions in three others. From a governance standpoint, it results in duplicated user records, inconsistent role assignments across platforms, and an audit trail that is impossible to reconstruct without manual, error-prone data consolidation.&lt;/p&gt;

&lt;p&gt;To achieve long-term maintainability and security, engineers must shift from managing users manually at the service level to establishing a centralized Single Source of Truth (SSoT).&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Defining the Core Problem: Architectural Anti-Patterns
&lt;/h2&gt;

&lt;p&gt;Fragmented user management is not merely a localized inconvenience; it is a fundamental architectural anti-pattern. Several specific issues arise when identity is decentralized:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Duplicated Logic and Records&lt;/strong&gt;&lt;br&gt;
When Service A and Service B both maintain a users table, the system inevitably drifts. Email updates, password resets, and profile changes must be synchronized via fragile webhooks or, worse, manual intervention. This redundancy increases storage costs and, more importantly, cognitive load for developers who must maintain multiple authentication middlewares.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authorization Governance Failures&lt;/strong&gt;&lt;br&gt;
Authorization is frequently conflated with authentication. When role-based access control (RBAC) is hardcoded into individual service repositories, updating a global permission (e.g., “Editor” can now access “Analytics”) requires a coordinated deployment across every service in the stack. This lack of centralized governance makes it nearly impossible to enforce consistent security policies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit Exposure and Compliance Risks&lt;/strong&gt;&lt;br&gt;
Under regulations such as GDPR or SOC2, the ability to provide a comprehensive audit log of user access is mandatory. In a fragmented system, there is no unified log of when a user logged in, which services they accessed, or who granted them elevated privileges. This creates a massive liability during compliance audits.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Designing a Centralized Architecture
&lt;/h2&gt;

&lt;p&gt;To mitigate these risks, we propose a “Hub and Spoke” architecture. In this model, a centralized Identity Hub handles all identity concerns, while individual applications (the spokes) delegate authentication and authorization to this central authority.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Identity Boundary&lt;/strong&gt;&lt;br&gt;
The Hub defines the boundary for all Identity and Access Management (IAM). No spoke application should ever write directly to the user database. Instead, spokes interact with the Hub via secure API contracts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structural Components&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Authentication Layer: Centralizes credential verification, session management, and JWT issuance.&lt;/li&gt;
&lt;li&gt;Authorization Layer: A unified RBAC/ABAC engine that defines what a user can do across the entire ecosystem.&lt;/li&gt;
&lt;li&gt;The API Hub: Built with Next.js, serving as the gateway for both administrative identity management and external service verification requests.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Arsitektur Program (&lt;a href="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ci1ivgnaionh6dst1lan.png" rel="noopener noreferrer"&gt;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ci1ivgnaionh6dst1lan.png&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;By decoupling identity, we ensure that the Laravel “spokes” remain thin and focused on their specific business domains, while the Next.js “hub” handles the heavy lifting of security and governance.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Why Next.js as the Core Hub?
&lt;/h2&gt;

&lt;p&gt;The selection of Next.js as the engine for a centralized Identity Hub is a strategic architectural choice. While traditional IAM solutions like Keycloak or Auth0 exist, building a custom Hub on Next.js provides the perfect balance between a modular monolith and microservices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full-Stack Synergy&lt;/strong&gt;&lt;br&gt;
Next.js provides a unified environment for building both the administrative UI (for managing users and permissions) and the high-performance API Route Handlers required for service-to-service communication. This reduces the deployment footprint and simplifies the development lifecycle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Serverless and Edge Readiness&lt;/strong&gt;&lt;br&gt;
Modern identity hubs must be highly available. Next.js’s ability to run on edge runtimes or serverless functions allows the authentication layer to be geographically distributed, reducing latency for global service requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Developer Velocity vs. Governance&lt;/strong&gt;&lt;br&gt;
Compared to a pure microservices approach, using Next.js allows us to maintain the identity logic in a single, well-tested repository while providing the necessary interfaces (APIs) for other services to consume. It acts as a “Governance Layer” that can be scaled independently of the consuming applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Database Design Strategy: Normalization and Scale
&lt;/h2&gt;

&lt;p&gt;A centralized system is only as strong as its underlying data model. We require a relational schema that supports fine-grained access control while remaining performant under high read loads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Normalized Relational Schema&lt;/strong&gt;&lt;br&gt;
We will utilize PostgreSQL for its strict schema enforcement and advanced indexing capabilities. The schema focuses on a robust RBAC model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Entity Breakdown:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;users: Core identity data (UUIDs, hashed credentials, MFA status).&lt;/li&gt;
&lt;li&gt;roles: High-level groupings (e.g., Admin, Manager, Auditor).&lt;/li&gt;
&lt;li&gt;permissions: Atomic actions (e.g., inventory.write, finance.view).&lt;/li&gt;
&lt;li&gt;role_permissions: The mapping layer defining the capabilities of each role.&lt;/li&gt;
&lt;li&gt;user_roles: Mapping users to one or more roles.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// schema.prisma

model User {
  id            String     @id @default(uuid())
  email         String     @unique
  passwordHash  String
  fullName      String?
  isActive      Boolean    @default(true)
  roles         UserRole[]
  createdAt     DateTime   @default(now())
  updatedAt     DateTime   @updatedAt

  @@index([email])
}

model Role {
  id          String           @id @default(uuid())
  name        String           @unique
  description String?
  permissions RolePermission[]
  users       UserRole[]
}

model Permission {
  id          String           @id @default(uuid())
  slug        String           @unique // e.g., 'users.create'
  description String?
  roles       RolePermission[]
}

model RolePermission {
  roleId       String
  permissionId String
  role         Role       @relation(fields: [roleId], references: [id])
  permission   Permission @relation(fields: [permissionId], references: [id])

  @@id([roleId, permissionId])
}

model UserRole {
  userId String
  roleId String
  user   User   @relation(fields: [userId], references: [id])
  role   Role   @relation(fields: [roleId], references: [id])

  @@id([userId, roleId])
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Performance and Indexing&lt;/strong&gt;&lt;br&gt;
Since authentication is a read-heavy operation, we prioritize B-tree indexes on the email and slug columns. For authorization checks—which often involve joining multiple tables—we will implement caching strategies (such as Redis) in Part 2 to prevent database bottlenecks during peak traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit Logging Considerations&lt;/strong&gt;&lt;br&gt;
Every change to the user_roles or role_permissions tables must be captured. In an enterprise environment, "who granted this access" is as important as "who has this access." We implement this via an audit_logs table that captures the actor, the action, and the timestamp for every administrative change.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Governance and Scalability Considerations
&lt;/h2&gt;

&lt;p&gt;Horizontal Scaling&lt;br&gt;
The Next.js Hub is stateless by design. Sessions are managed via signed JWTs or short-lived tokens, allowing us to spin up multiple instances of the Hub behind a load balancer without worrying about session affinity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migration Strategy&lt;/strong&gt;&lt;br&gt;
Moving from legacy scattered systems to a Centralized Hub requires a “Parallel Run” strategy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shadow Write: Keep writing to legacy DBs while simultaneously populating the Centralized Hub.&lt;/li&gt;
&lt;li&gt;Lazy Migration: Migrate users to the Hub the next time they log in.&lt;/li&gt;
&lt;li&gt;Deprecation: Once the Hub reaches 100% data parity, flip the switch and point the Laravel spokes to the Hub’s API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Risk Mitigation&lt;/strong&gt;&lt;br&gt;
The SSoT becomes a Single Point of Failure. To mitigate this, we implement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database Replication: Multi-region PostgreSQL deployments.&lt;/li&gt;
&lt;li&gt;Rate Limiting: Protecting the Hub from brute-force attacks or malfunctioning spoke services.&lt;/li&gt;
&lt;li&gt;Circuit Breakers: Ensuring that if the Hub is down, spoke services can fail gracefully (e.g., via cached permissions).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Conclusion
&lt;/h2&gt;

&lt;p&gt;Establishing a Single Source of Truth for user management is not just a technical upgrade; it is a foundational move toward architectural maturity. By centralizing identity in a Next.js and PostgreSQL Hub, we eliminate data redundancy, enforce strict security governance, and create a scalable framework that can support any number of integrated services.&lt;/p&gt;

&lt;p&gt;In this first part, we have laid the theoretical and structural groundwork. However, an architecture is only as good as its implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In Part 2: Building the Next.js API Hub&lt;/strong&gt;, we will move into the implementation phase, focusing on creating secure API Route Handlers, implementing JWT-based authentication, and building the logic that will allow our external Laravel applications to communicate with our new Source of Truth.&lt;/p&gt;

</description>
      <category>softwaredevelopment</category>
      <category>nextjs</category>
      <category>postgressql</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
