nyxcore-systems
9 min read

From Zero to 'Ready to Run': Bootstrapping a Production-Grade Dashboard with Next.js, tRPC, and LLMs

A deep dive into building a complex, multi-tenant dashboard application from scratch, covering authentication, database security, real-time features, LLM integration, and the challenges faced along the way.

Next.jsTypeScripttRPCPrismaNextAuthMulti-tenancyLLMRLSFullstackWeb DevelopmentArchitecture

The hum of the server, the crisp click of the keyboard – there's a unique thrill in staring at a blank slate, knowing a complex application is about to emerge. Recently, I embarked on a mission to build nyxCore, a production-grade dashboard app designed to handle multi-tenancy, real-time data, and cutting-edge LLM integrations. It's a beast of a project, touching every corner of modern web development.

And today, I'm thrilled to report: it's ready to run.

In a whirlwind session, I brought nyxCore from a conceptual idea to a fully scaffolded, compiling, and test-passing application. This isn't just a basic CRUD app; we're talking Next.js App Router, TypeScript, Prisma, tRPC, NextAuth v5, multi-tenant Row-Level Security (RLS), and a robust multi-provider LLM system. With 111 files, 83 source files, zero TypeScript errors, and all 15 unit tests passing, the app is poised for its first docker compose up -d && npm run dev.

Let's break down how we got here, the architectural decisions, and the valuable lessons learned.

The Foundation: Building a Robust Base

Every skyscraper needs a solid foundation. For nyxCore, this meant meticulously selecting and integrating the core technologies.

The Tech Stack:

  • Next.js 14 (App Router): For a powerful, modern full-stack React experience.
  • TypeScript: Non-negotiable for type safety and developer productivity in a complex codebase.
  • Prisma 5: Our ORM of choice, simplifying database interactions.
  • tRPC v11 RC: To build an end-to-end type-safe API layer with minimal boilerplate.
  • NextAuth v5 beta: The latest iteration for flexible authentication.
  • Radix UI & dnd-kit: For accessible, unstyled UI primitives and drag-and-drop functionality.
  • PostgreSQL 16 & Redis 7: Our data persistence and caching layers.

Database Schema & Security: I laid out a comprehensive prisma/schema.prisma with 16 models, covering everything from User and Tenant to DashboardLayout, Workflow, ClothingItem, and MemoryEntry. This detailed schema is the backbone of the application's domain.

Crucially, given the multi-tenant requirement, I immediately tackled Row-Level Security (RLS). A dedicated prisma/rls.sql file defines policies for all tenant-scoped tables, ensuring that users can only access data belonging to their own tenant. This is a critical security measure that's baked in from day one. I also added a full-text search trigger for memory_entries to enable powerful search capabilities.

Finally, prisma/seed.ts was set up to populate the database with a default tenant and four built-in personas, providing immediate useful data for development.

Securing the Gates: Authentication & Authorization

Security and access control are paramount for a multi-tenant application.

  • NextAuth v5 Integration: I implemented src/server/auth.ts using NextAuth v5 (still in beta, but incredibly powerful). It supports Resend for email authentication, optional GitHub OAuth, a robust JWT strategy, and integrates seamlessly with PrismaAdapter. The key here was ensuring that tenant and role claims are correctly embedded in the session for granular access control.
  • Tenant Resolution: The src/server/services/tenant.ts service handles the complex logic of resolving the active tenant, prioritizing subdomain, then custom domain, then JWT claims. This service is designed to "fail-closed," meaning if a tenant cannot be definitively identified, access is denied.
  • RLS Service: Complementing the database-level RLS, src/server/services/rls.ts provides server-side helpers like withTenantScope() and withAdminScope(), validating UUIDs with Zod to prevent common injection vulnerabilities.
  • Cryptography: Sensitive data needs robust protection. src/server/services/crypto.ts implements AES-256-GCM encryption with a versioned ciphertext format (v1:<iv>:<tag>:<ciphertext>), allowing for future key rotation and algorithm upgrades without data loss.
  • Rate Limiting & Audit Logging: To prevent abuse and maintain accountability, src/server/services/rate-limit.ts uses a Redis token bucket strategy (with a fail-open mechanism if Redis is down for resilience) and src/server/services/audit.ts provides non-blocking audit logging for critical actions.

The Brains of the Operation: Backend Logic & LLM Integration

The core business logic and our ambitious LLM features live on the backend, exposed via tRPC.

  • tRPC Layer: I set up the full tRPC infrastructure: init.ts, context.ts, and a comprehensive middleware.ts. This middleware handles requestId generation, rateLimit checks (general and LLM-specific), enforceAuth, enforceTenant, and enforceRole checks, creating a secure and predictable API surface.
  • Modular Routers: Six distinct tRPC routers (dashboard, discussions, workflows, wardrobe, memory, admin) were created, organizing the API into logical domains.
  • LLM Multi-Provider System: This is where things get really exciting. I built adapters for Anthropic (with full streaming support) and OpenAI (also full streaming), with stubs for Google and Ollama. src/server/services/llm/registry.ts intelligently selects the appropriate provider based on user preferences or availability.
  • Core Services:
    • discussion-service.ts: Manages LLM-powered discussions, supporting single, parallel, and consensus modes with database persistence.
    • workflow-engine.ts: A powerful 5-step pipeline for complex tasks, featuring checkpoints, a "YOLO mode" for rapid execution, and "pause-for-review" for human intervention.
    • storage.ts: An S3-compatible adapter interface, with a LocalStorageAdapter for convenient local development.
    • queue.ts: An InMemoryQueue with a clear job handler contract for background tasks.
    • github-connector.ts: An interface for future GitHub memory synchronization.

Bringing it to Life: The Frontend Experience

A powerful backend needs an equally compelling frontend.

  • UI Components: Leveraging Radix UI primitives, I built 11 essential UI components: button, card, input, badge, avatar, skeleton, dialog, dropdown-menu, tabs, sheet, and toast notifications.
  • Layout & Theming: A foundational layout shell (sidebar, header, mobile-nav, theme-toggle) was created, complete with globals.css using CSS variables for effortless dark/light mode theming.
  • Dashboard Widgets: The dashboard features a dynamic widget-grid using dnd-kit for drag-and-drop functionality, along with widget-card, stats-widget, and activity-widget components.
  • Application Pages: All 15 core application pages were created, from login and dashboard to detailed discussions and workflows views, wardrobe, memory, admin, and settings.
  • Real-time Updates: To provide a dynamic user experience, I implemented Server-Sent Events (SSE) endpoints (/api/v1/events/dashboard, /api/v1/events/workflows/[id], /api/v1/events/discussions/[id]). A custom src/lib/sse-client.ts and src/hooks/use-sse.ts with exponential backoff ensure robust, real-time data streaming to the frontend.
  • Providers & Middleware: src/app/providers.tsx centralizes the setup for tRPC, React Query, SessionProvider, and ToastProvider. src/middleware.ts handles authentication redirect guards.
  • PWA Baseline: A manifest.json, sw.js, and offline.html were created to enable Progressive Web App capabilities from the start.

The Backbone: Infrastructure & Testing

No production-grade app is complete without proper infrastructure and testing.

  • Docker Compose: A docker-compose.yml was set up to easily spin up postgres:16 and redis:7-alpine instances, providing a consistent development environment.
  • Environment Variables: A detailed .env.example documents all required and optional variables, making setup straightforward.
  • CI Workflow: A .github/workflows/ci.yml was configured to run linting, type-checking, unit tests, and end-to-end tests (with dedicated Postgres and Redis services) on every push, ensuring code quality and stability.
  • Testing: Four unit test files (15 tests) cover critical services like crypto, tenant, RLS, and rate-limiting. Three e2e test files validate core flows: auth, dashboard, and discussion.
  • Documentation: Both CLAUDE.md (internal project documentation) and README.md (quick start, commands, architecture, security) were updated to reflect the current state of the project.

Navigating the Labyrinth: Lessons Learned & Workarounds

Building a complex application, especially with beta versions of frameworks, inevitably leads to challenges. Here are some of the "pains" encountered and how they were overcome:

  1. NextAuth v5 JWT Type Augmentation:

    • Challenge: Attempting to augment the JWT type for NextAuth v5 using declare module "next-auth/jwt" resulted in a TS2664 error, indicating the module augmentation wasn't found.
    • Lesson: NextAuth v5 (specifically @auth/core) changed its internal module structure.
    • Workaround: The correct module path for augmentation is declare module "@auth/core/jwt". This highlights the importance of checking framework-specific documentation, especially for beta releases.
  2. Prisma JSON Field Type Compatibility:

    • Challenge: Using a generic Record<string, unknown> for Prisma JSON fields led to a TS2322 type mismatch, as it wasn't assignable to Prisma's InputJsonValue.
    • Lesson: Prisma expects specific JSON types for its Json fields, and unknown isn't always compatible directly.
    • Workaround: The solution was to define Zod schemas as z.record(z.string()) and then explicitly cast the resulting object with as Record<string, string> when passing it to Prisma. This ensures type safety while satisfying Prisma's requirements.
  3. Iterating over Map.values() in TypeScript:

    • Challenge: Directly using for (const provider of providers.values()) on a Map instance caused a TS2802 error, indicating that MapIterator requires the --downlevelIteration compiler option.
    • Lesson: Iterating directly over iterators might not be supported in all target environments without specific compiler flags.
    • Workaround: Wrapping the iteration with Array.from(providers.values()) creates an array from the map's values, which is universally iterable without special compiler options.
  4. Implicit Promise<void> Return Types in Interfaces:

    • Challenge: When implementing an interface with Promise<Object> return types, stub methods that simply throw an error but lacked explicit return type annotations implicitly defaulted to Promise<void>, causing a TS2416 return type mismatch.
    • Lesson: Even for methods that are expected to throw, explicit return type annotations are crucial to satisfy interface contracts.
    • Workaround: Added explicit return type annotations to the stub methods to match the interface's Promise<{...}> signature.
  5. onSuccess Shorthand in tRPC Mutation Hooks:

    • Challenge: Using onSuccess: refetch directly in tRPC mutation hooks resulted in a TS2322 error because refetch (from useQuery) expects RefetchOptions as an argument, not the mutation's data.
    • Lesson: While convenient, shorthand often has specific type requirements.
    • Workaround: Changed to onSuccess: () => { refetch(); } to correctly call the refetch function without passing the mutation result directly.
  6. Assigning to process.env.NODE_ENV in Tests:

    • Challenge: Attempting to set process.env.NODE_ENV = "test" in test setup files resulted in a TS2540 error, as NODE_ENV is typed as a read-only property.
    • Lesson: TypeScript's type definitions for Node.js environment variables correctly mark NODE_ENV as read-only for safety.
    • Workaround: Casting process.env to (process.env as Record<string, string>) allowed the assignment, bypassing the read-only check for testing purposes. This is generally safe in test environments where you control the process.

What's Next: The Road Ahead

Reaching this "ready to run" state is a huge milestone, but it's just the beginning. The immediate next steps involve bringing the application fully to life:

  1. Initialize the database: docker compose up -d followed by npx prisma db push.
  2. Apply RLS policies: psql $DATABASE_URL < prisma/rls.sql.
  3. Seed initial data: npx prisma db seed.
  4. Configure environment variables: Create .env from .env.example with real API keys and generated secrets.
  5. Verify core flows: Run npm run dev and test the login flow end-to-end.
  6. Confirm real-time updates: Ensure SSE connections are working on the dashboard.
  7. Enable LLMs: Add Anthropic/OpenAI API keys via the Admin panel.
  8. Validate production build: npm run build to ensure everything compiles for deployment.
  9. Future enhancements: Implement image uploads for wardrobe items and integrate the GitHub connector for memory synchronization.

Conclusion

Building nyxCore has been an exhilarating journey, a testament to the power of modern web development tools when wielded with a clear vision. From a sprawling Prisma schema and robust RLS to a sophisticated LLM multi-provider system and real-time frontend, this project pushes the boundaries of what a single developer can achieve in a focused sprint.

The challenges along the way, particularly with beta frameworks and TypeScript nuances, only served to deepen my understanding and refine the architecture. I'm incredibly excited to move into the next phase: populating this powerful skeleton with rich features and seeing nyxCore come to life. Stay tuned for more updates!