nyxcore-systems
6 min read

First Boot Chronicles: Debugging a Next.js 14 Dashboard from Zero to Login

A deep dive into the messy reality of getting a complex Next.js application running for the first time, complete with all the gotchas, workarounds, and hard-won lessons.

nextjsdebuggingauthenticationprismadockeroauth

First Boot Chronicles: Debugging a Next.js 14 Dashboard from Zero to Login

There's something both thrilling and terrifying about running npm run dev on a complex application for the first time. Will it work? Of course not. But how will it break? That's where the real adventure begins.

Today I'm sharing the war stories from getting nyxCore—a multi-tenant dashboard with authentication, database integration, and real-time features—to boot successfully. If you've ever spent hours debugging configuration issues, hydration errors, and authentication flows, this one's for you.

The Goal: From Code to Working Login

The mission was straightforward: take a freshly developed Next.js 14 application with Prisma, PostgreSQL, Redis, NextAuth v5, and get it running end-to-end. That means:

  • ✅ Application boots without errors
  • ✅ Database connections work
  • ✅ Email and OAuth login flows complete successfully
  • ✅ Protected admin routes are accessible
  • ✅ API operations (CRUD) function properly

Spoiler alert: it took way more fixes than expected, but we got there.

The Victory Lap: What Actually Works Now

After a full debugging session, here's what's working:

  • Multi-provider authentication: Both email/password and GitHub OAuth flows complete successfully
  • Role-based access control: Admin routes properly enforce permissions
  • Database operations: API key CRUD operations work through the admin panel
  • Auto-tenant assignment: New users automatically get assigned to the default tenant
  • Flexible encryption: System accepts both hex and base64 encoded encryption keys
  • Proper hydration: No more client/server mismatches on theme toggles

But getting here required navigating quite a few landmines...

Lessons Learned: The Technical Gotchas

1. Next.js 14 Config Files Don't Support TypeScript

The Problem: Started with a next.config.ts file because, hey, everything else is TypeScript, right?

bash
Error: Configuring Next.js via 'next.config.ts' is not supported

The Fix: Rename to next.config.mjs and use JSDoc for type hints:

javascript
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    serverComponentsExternalPackages: ['@prisma/client']
  }
}

export default nextConfig

This one caught me off guard since TypeScript support is so prevalent elsewhere in the Next.js ecosystem.

2. Prisma Column Names vs. SQL Policies

The Problem: Row Level Security policies were failing because I wrote them with snake_case column names:

sql
-- This fails ❌
CREATE POLICY tenant_isolation ON discussions 
  FOR ALL TO authenticated() 
  USING (tenant_id = get_user_tenant_id());

The Reality: Prisma generates camelCase columns, so the SQL needs double-quoted identifiers:

sql
-- This works ✅
CREATE POLICY tenant_isolation ON discussions 
  FOR ALL TO authenticated() 
  USING ("tenantId" = get_user_tenant_id());

Always check your actual database schema, not your Prisma model definitions!

3. The Hydration Error That Wasn't About Data

The Problem: Theme toggle component was causing hydration mismatches:

typescript
// This causes hydration errors ❌
export function ThemeToggle() {
  const { theme, setTheme } = useTheme()
  
  return (
    <Button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
      {theme === 'dark' ? <Sun /> : <Moon />}
    </Button>
  )
}

The Fix: Defer rendering until after hydration:

typescript
// This works ✅
export function ThemeToggle() {
  const [mounted, setMounted] = useState(false)
  const { theme, setTheme } = useTheme()
  
  useEffect(() => setMounted(true), [])
  
  if (!mounted) return <Button><Sun /></Button> // Consistent fallback
  
  return (
    <Button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
      {theme === 'dark' ? <Sun /> : <Moon />}
    </Button>
  )
}

4. GitHub OAuth and the Missing Email Problem

The Problem: GitHub users with private emails were breaking the authentication flow because NextAuth expected an email field.

The Solution: Two-part fix:

  1. Make email optional in the Prisma schema: email String?
  2. Add a profile callback to generate fallback emails:
typescript
GitHubProvider({
  clientId: process.env.GITHUB_CLIENT_ID!,
  clientSecret: process.env.GITHUB_CLIENT_SECRET!,
  profile(profile) {
    return {
      id: profile.id.toString(),
      name: profile.name || profile.login,
      email: profile.email || `${profile.id}+${profile.login}@users.noreply.github.com`,
      image: profile.avatar_url,
    }
  },
})

5. Next.js 14 Route Parameters Aren't Promises

The Problem: Dynamic route pages were throwing errors about unsupported types:

typescript
// This breaks in Next.js 14 ❌
export default async function DiscussionPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = use(params)
  // ...
}

The Fix: Treat params as a plain object:

typescript
// This works ✅
export default async function DiscussionPage({ params }: { params: { id: string } }) {
  const { id } = params
  // ...
}

The use() hook is for actual promises, not objects that might become promises in future Next.js versions.

The Authentication Flow Victory

One of the most satisfying moments was watching the complete authentication flow work:

  1. Email signup → User created with generated tenant membership
  2. GitHub OAuth → Fallback email generated, user auto-assigned to default tenant
  3. Role enforcement → Admin routes properly check permissions
  4. API operations → CRUD operations work with proper tenant isolation

The key insight was that user onboarding needed to happen in the JWT callback, not just during initial signup, to handle edge cases and race conditions.

Infrastructure That Just Works

Getting the supporting infrastructure running was surprisingly smooth:

bash
# One command to rule them all
docker compose up -d

# Database setup
npx prisma db push
psql $DATABASE_URL_WITHOUT_QUERY_PARAMS < prisma/rls.sql
npx prisma db seed

Having PostgreSQL 16 and Redis 7 running in containers with proper networking made the development environment incredibly stable.

What's Next?

With authentication and basic CRUD working, the next phase involves testing the more complex features:

  • Real-time discussions with Server-Sent Events
  • Workflow orchestration with start/pause controls
  • AI-powered wardrobe analysis and gap detection
  • Memory management with vector search capabilities

But having a solid foundation with working auth, database connections, and error-free boot process makes tackling these features much more confident.

Key Takeaways for Fellow Developers

  1. Configuration files matter: Always check what config formats your framework version actually supports
  2. Database naming is critical: Your ORM's conventions might not match your SQL expectations
  3. Hydration errors aren't always about data: Sometimes it's about consistent rendering
  4. OAuth edge cases are real: Plan for users with private/missing profile data
  5. Framework versions have breaking changes: What worked in Next.js 13 might not work in 14

The most important lesson? Don't be afraid of the first boot failures. Each error is a stepping stone to a more robust, well-understood application. And when that login page finally loads and actually works? Pure magic.


Building complex applications is messy, iterative work. If you're fighting similar battles with Next.js, authentication, or database integration, remember: every senior developer has been exactly where you are, staring at error messages and wondering why their config file isn't supported.