Custom Modules
A "module" in CRMBuilder is any new page or feature added to the platform. This doc covers how new pages are scaffolded, where they register in the nav, and the conventions every module must follow.
File structure
New modules live under src/app/(crm)/:
src/app/(crm)/
contacts/
page.tsx ← list view
[id]/
page.tsx ← detail view
deals/
page.tsx
your-module/ ← new module goes here
page.tsx
[id]/
page.tsx
The (crm) route group applies the shared CRM layout — sidebar, topbar, auth check — automatically. Any page placed here inherits all of that with no extra wiring.
Nav registration
The sidebar nav is driven by a central config file:
src/config/nav.ts
Add your module here:
export const navItems: NavItem[] = [
{ label: 'Contacts', href: '/contacts', icon: 'Users' },
{ label: 'Deals', href: '/deals', icon: 'TrendingUp' },
// Add your module:
{ label: 'Reports', href: '/reports', icon: 'BarChart2' },
];
Icons are Lucide icon names (string). Browse available icons at lucide.dev. The icon string must exactly match the export name.
Page component conventions
Every module page must:
- Be a server component by default (no
'use client'at the top) - Check auth at the top using
getServerSession - Scope all DB queries to
id_companyfrom the session - Pass data down to client components for interactivity
Minimal page template:
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { redirect } from 'next/navigation';
import { db } from '@/lib/db';
export default async function YourModulePage() {
const session = await getServerSession(authOptions);
if (!session) redirect('/login');
const { id_company } = session.user;
const rows = await db.query(
`SELECT * FROM your_table
WHERE id_company = ? AND deleted_at IS NULL
ORDER BY created_at DESC`,
[id_company]
);
return <YourModuleClient rows={rows} />;
}
API routes for modules
Module-specific API routes live under src/app/api/:
src/app/api/
your-module/
route.ts ← GET list, POST create
[id]/
route.ts ← GET one, PUT update, DELETE
Every route must:
- Verify session at the top
- Pull
id_companyfromsession.useronly - Return proper HTTP status codes (200, 201, 400, 401, 404, 500)
Database migrations
New tables are added as migration files in:
src/lib/migrations/
Name format: YYYYMMDD_description.sql
Example: 20251103_create_lead_scores.sql
CREATE TABLE IF NOT EXISTS lead_scores (
id SERIAL PRIMARY KEY,
id_company INT NOT NULL,
id_contact INT NOT NULL,
score INT NOT NULL DEFAULT 0,
reason TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_lead_scores_company
ON lead_scores(id_company);
Always:
- Use
CREATE TABLE IF NOT EXISTS(idempotent) - Add an index on
id_companyfor any table that will grow - Include
deleted_atif records should be soft-deleteable - Include
created_aton every table
Checklist for a new module
- Page file at
src/app/(crm)/your-module/page.tsx - Nav entry added in
src/config/nav.ts - API routes at
src/app/api/your-module/route.ts - Migration file in
src/lib/migrations/ - All queries scoped to
id_companyfrom session - All selects include
deleted_at IS NULL -
check_typespasses before PR