Building nyxCore: A Production-Grade Dashboard with Next.js, tRPC, and Multi-Tenant Architecture
A deep dive into building a complete production-ready dashboard application from scratch, featuring Next.js 14, tRPC v11, multi-tenant RLS, and an LLM-powered discussion system.
Building nyxCore: A Production-Grade Dashboard with Next.js, tRPC, and Multi-Tenant Architecture
Last week, I embarked on an ambitious project: building a complete production-grade dashboard application from the ground up. The goal was to create nyxCore — a multi-tenant platform combining modern web technologies with AI-powered features. After an intensive development session, I'm excited to share the journey, challenges, and architectural decisions that went into creating this 111-file, fully-tested application.
The Vision: More Than Just Another Dashboard
nyxCore isn't your typical admin panel. It's designed as a comprehensive platform featuring:
- Multi-tenant architecture with Row-Level Security (RLS)
- AI-powered discussions with multiple LLM providers
- Workflow automation with visual pipeline management
- Real-time updates via Server-Sent Events
- Drag-and-drop dashboard customization
- Enterprise-grade security with audit logging and rate limiting
The Tech Stack: Modern, Robust, Production-Ready
Here's what powers nyxCore:
{
"frontend": ["Next.js 14", "TypeScript", "Tailwind CSS", "Radix UI"],
"backend": ["tRPC v11", "Prisma 5", "PostgreSQL", "Redis"],
"auth": ["NextAuth v5", "JWT", "Multi-provider OAuth"],
"ai": ["OpenAI", "Anthropic", "Google AI", "Ollama"],
"infrastructure": ["Docker", "S3-compatible storage", "PWA support"]
}
The combination of Next.js App Router with tRPC provides incredible type safety — every API call is fully typed from server to client, eliminating an entire class of runtime errors.
Database Design: 16 Models, Bulletproof Security
The heart of any multi-tenant application is its data model. nyxCore features 16 carefully designed Prisma models:
model Tenant {
id String @id @default(cuid())
name String
subdomain String @unique
customDomain String? @unique
// Relations to all tenant-scoped resources
members TenantMember[]
dashboards DashboardLayout[]
discussions Discussion[]
workflows Workflow[]
// ... and more
}
What makes this special is the PostgreSQL Row-Level Security (RLS) implementation. Every tenant-scoped table has policies that automatically filter data based on the current user's tenant context:
-- Example RLS policy for discussions
CREATE POLICY tenant_isolation_policy ON discussions
FOR ALL TO authenticated
USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
This approach provides defense in depth — even if application-level tenant filtering fails, the database won't leak data across tenants.
The LLM Integration: Multi-Provider AI Discussions
One of nyxCore's standout features is its flexible LLM integration. The system supports multiple AI providers through a unified interface:
export interface LLMProvider {
name: string;
generateResponse(params: GenerateParams): AsyncIterable<string>;
isAvailable(): Promise<boolean>;
}
The discussion service offers three interaction modes:
- Single Mode: One AI persona responds
- Parallel Mode: Multiple personas respond simultaneously
- Consensus Mode: Personas collaborate to reach agreement
All conversations are streamed in real-time and persisted to the database, creating a rich history of AI interactions.
Real-Time Architecture: SSE + React Query
Rather than WebSockets, nyxCore uses Server-Sent Events (SSE) for real-time updates. This choice provides several advantages:
- Simpler implementation and debugging
- Better handling of network interruptions
- Native browser reconnection support
- Lower server resource usage
// Custom SSE hook with exponential backoff
export function useSSE(endpoint: string) {
const [data, setData] = useState(null);
useEffect(() => {
const eventSource = new EventSource(endpoint);
eventSource.onmessage = (event) => {
setData(JSON.parse(event.data));
};
// Exponential backoff reconnection logic
eventSource.onerror = () => {
setTimeout(() => {
eventSource.close();
// Reconnect with backoff
}, backoffDelay);
};
return () => eventSource.close();
}, [endpoint]);
return data;
}
Lessons Learned: TypeScript Gotchas and Workarounds
Building a complex TypeScript application always comes with challenges. Here are the key issues I encountered and how I solved them:
NextAuth v5 Module Augmentation
The Problem: TypeScript couldn't find the JWT module for type augmentation.
// ❌ This didn't work
declare module "next-auth/jwt" {
interface JWT {
tenantId?: string;
role?: string;
}
}
The Solution: NextAuth v5 changed its internal structure.
// ✅ This works
declare module "@auth/core/jwt" {
interface JWT {
tenantId?: string;
role?: string;
}
}
Prisma JSON Field Types
The Problem: Prisma's InputJsonValue type is stricter than expected.
// ❌ TypeScript error
const data: Record<string, unknown> = { key: "value" };
await prisma.model.create({ data: { jsonField: data } });
The Solution: Use more specific types and explicit casting.
// ✅ Works perfectly
const schema = z.record(z.string());
const data = schema.parse(input) as Record<string, string>;
await prisma.model.create({ data: { jsonField: data } });
Iterator Protocol Issues
The Problem: Map iterators require downlevel iteration support.
// ❌ Compilation error
for (const provider of providers.values()) {
// ...
}
The Solution: Convert to array first.
// ✅ Clean and compatible
for (const provider of Array.from(providers.values())) {
// ...
}
Testing Strategy: Comprehensive Coverage
nyxCore includes both unit and end-to-end tests:
- 15 unit tests covering critical services (crypto, tenant resolution, rate limiting)
- E2E tests using Playwright with real PostgreSQL and Redis instances
- GitHub Actions CI running the full test suite on every push
# CI pipeline with services
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
Security First: Enterprise-Grade Protection
Security isn't an afterthought in nyxCore:
- AES-256-GCM encryption for sensitive data with versioned ciphertext
- Rate limiting with Redis token buckets
- Audit logging for all critical operations
- JWT-based authentication with secure session management
- Row-Level Security preventing data leaks
- Input validation with Zod schemas throughout
// Example: Encrypted API key storage
export async function encryptApiKey(plaintext: string): Promise<string> {
const key = await getEncryptionKey();
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipher('aes-256-gcm', key);
cipher.setAAD(Buffer.from('api-key'));
const encrypted = Buffer.concat([
cipher.update(plaintext, 'utf8'),
cipher.final()
]);
const tag = cipher.getAuthTag();
// Versioned format for future key rotation
return `v1:${iv.toString('hex')}:${tag.toString('hex')}:${encrypted.toString('hex')}`;
}
Performance Optimizations: Built for Scale
From day one, nyxCore was designed with performance in mind:
- React Query for intelligent data fetching and caching
- Incremental Static Regeneration for dashboard layouts
- Connection pooling with Prisma
- Redis caching for frequently accessed data
- Lazy loading for heavy components
- PWA capabilities for offline functionality
What's Next: The Roadmap
With the foundation complete, here's what's coming next:
- Production deployment with proper environment configuration
- Image upload system for wardrobe management
- GitHub integration for memory synchronization
- Advanced workflow automation features
- Mobile app using React Native
- Analytics dashboard with custom metrics
Key Takeaways
Building nyxCore taught me several valuable lessons:
- Type safety is worth the upfront investment — tRPC's end-to-end typing caught dozens of potential bugs
- Security should be layered — RLS + application-level checks provide robust protection
- Real-time doesn't always need WebSockets — SSE is simpler and often sufficient
- Testing complex integrations requires real services — mocking databases and Redis hides important edge cases
- Modern TypeScript is incredibly powerful — but you need to understand its nuances
Try It Yourself
The complete nyxCore codebase is production-ready and fully documented. To get started:
# Clone and setup
git clone [repository-url]
cd nyxcore
npm install
# Start services
docker compose up -d
# Initialize database
npx prisma db push
psql $DATABASE_URL < prisma/rls.sql
npx prisma db seed
# Launch the app
npm run dev
Building nyxCore was an incredible journey through modern web development. The combination of Next.js, tRPC, and thoughtful architecture decisions resulted in a platform that's both powerful and maintainable.
What would you build with this foundation? I'd love to hear your thoughts and see what you create!
Have questions about the implementation details? Found this helpful? Let me know in the comments below or reach out on Twitter. Happy coding! 🚀