Enterprise Resource Planning systems seem straightforward until you hit real-world requirements: multi-tenant data isolation, complex permission hierarchies, real-time dashboards with thousands of concurrent users, and audit trails for compliance. Most tutorials show you how to build a CRUD app — not how to architect a system that survives production.
I've built ERP systems for real estate companies, schools, inventory management, and legal firms. Here's the architecture pattern I now use consistently.
Every ERP I build uses three clear layers:
The key insight: keep business logic in the API layer, never in components or raw SQL. This makes testing straightforward and lets you swap databases if needed.
PostgreSQL's Row Level Security (RLS) is underused in Next.js stacks. Instead of filtering every query manually with WHERE tenant_id = ?, you define policies at the database level:
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON projects
USING (tenant_id = current_setting('app.tenant_id')::uuid);Set app.tenant_id in your API middleware per request and every query is automatically scoped. No accidental data leaks across tenants, even if a query forgets the WHERE clause.
I use a simple but flexible permission model: roles define what a user can do, resources define what they can do it to. A middleware checks both before any API route runs:
Store permissions in PostgreSQL, cache them in JWT claims on login. Re-fetch only on role change. This keeps API calls fast while permissions stay accurate.
For most ERP dashboards, you don't need WebSockets. Next.js 14's server components with revalidate every 30–60 seconds gives "good enough" real-time for operational dashboards. Save WebSockets for truly live data like stock prices or chat.
For report generation (which often involves expensive JOIN queries), I queue them as background jobs and show a loading state. Users get their report in seconds, not a timed-out browser request.