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:

  1. Be a server component by default (no 'use client' at the top)
  2. Check auth at the top using getServerSession
  3. Scope all DB queries to id_company from the session
  4. 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_company from session.user only
  • 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_company for any table that will grow
  • Include deleted_at if records should be soft-deleteable
  • Include created_at on 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_company from session
  • All selects include deleted_at IS NULL
  • check_types passes before PR
Custom Modules — PushButtonCRM Dev