Files
phaichayon 7a390cf0df init
2026-06-11 12:46:57 +07:00

14 KiB

name, description
name description
kiranism-shadcn-dashboard Migration-oriented guide for building and evolving features in this Next.js 16 shadcn dashboard repo. Use this skill whenever the user wants to add dashboard pages, tables, forms, navigation, route handlers, or feature modules in this project, especially when moving from mock APIs to real persistence, replacing Clerk with Auth.js, adding Drizzle-backed CRUD, or implementing organization-aware RBAC. Even if the user only mentions a page, sidebar item, or "connect this to a real backend", use this skill because the repo's feature, auth, and data-layer conventions are tightly coupled.

Dashboard Migration Guide

This skill is for evolving this starter from a demo template into an app-owned architecture.

Default target architecture:

  • Auth.js for authentication and session access
  • Drizzle ORM for persistence
  • src/app/api/**/route.ts as the boundary for CRUD or BFF access
  • app-owned organization, membership, role, and optional permissions data

Do not treat Clerk or src/constants/mock-api*.ts as the happy path. They are legacy template scaffolding that may still exist in the repo and should be migrated deliberately.

Current Repo Reality

Before changing anything, ground the work in the repo's current state:

  • src/features/products/api/service.ts and src/features/users/api/service.ts still call mock data
  • src/app/api/products/** and src/app/api/users/** already exist as route-handler shells
  • src/hooks/use-nav.ts, src/proxy.ts, and multiple pages/components are still coupled to Clerk
  • there is no committed auth.ts, Drizzle schema, or shared app RBAC model yet

Work incrementally. Prefer a migration plan that:

  1. introduces the auth shell
  2. introduces the shared org and RBAC model
  3. migrates one feature at a time from mock data to route handlers plus Drizzle

Quick Reference

Task Default location
Auth.js config src/auth.ts or project root auth.ts
Route protection src/proxy.ts or middleware.ts
Drizzle schema src/db/schema/*.ts or src/db/schema.ts
DB client src/lib/db.ts or src/db/index.ts
Feature types src/features/<name>/api/types.ts
Feature service src/features/<name>/api/service.ts
Query options src/features/<name>/api/queries.ts
Mutation options src/features/<name>/api/mutations.ts
Route handlers src/app/api/<name>/route.ts and [id]/route.ts
Feature components src/features/<name>/components/
Dashboard page src/app/dashboard/<name>/page.tsx
Nav config src/config/nav-config.ts
RBAC types src/types/index.ts plus app auth or membership types

Default Build Order For A Feature

When a user asks for a new feature or a migration from mocks, use this order unless the repo already has a stronger pattern:

1. Define persistence first

Create or extend Drizzle tables for:

  • the feature entity
  • organization ownership if data is tenant-scoped
  • membership or permission tables if access depends on role

Keep entity rules close to the schema so later route handlers are straightforward.

2. Define feature API types

In src/features/<name>/api/types.ts, define:

  • list filters
  • list response shape
  • detail response shape
  • mutation payloads

Do not re-export types from mock files. Types should reflect the route contract or normalized entity shape.

Example:

export interface Order {
  id: number;
  organizationId: string;
  customerName: string;
  status: 'pending' | 'paid' | 'cancelled';
  totalCents: number;
  createdAt: string;
  updatedAt: string;
}

export interface OrderFilters {
  page?: number;
  limit?: number;
  search?: string;
  status?: string;
  sort?: string;
}

export interface OrdersResponse {
  items: Order[];
  totalItems: number;
}

export interface OrderMutationPayload {
  customerName: string;
  status: Order['status'];
  totalCents: number;
}

3. Implement route handlers as the app boundary

Use src/app/api/<feature>/route.ts for list and create, and src/app/api/<feature>/[id]/route.ts for detail, update, and delete.

Route handlers should:

  • read Auth.js session or server auth helper
  • resolve active organization
  • verify membership or role
  • read and write via Drizzle
  • return JSON contracts that match types.ts

Keep them thin. Put reusable query logic in a server-side helper when it grows beyond one route.

Example shape:

import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/auth';
import { db } from '@/lib/db';

export async function GET(request: NextRequest) {
  const session = await auth();
  if (!session?.user) {
    return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
  }

  const { searchParams } = request.nextUrl;
  const page = Number(searchParams.get('page') ?? 1);
  const limit = Number(searchParams.get('limit') ?? 10);

  // Resolve active organization and role before querying.
  const data = await listOrdersForOrganization({
    db,
    userId: session.user.id,
    page,
    limit
  });

  return NextResponse.json(data);
}

4. Implement feature service with apiClient

service.ts should call the local route handlers, not Drizzle directly and not mock stores.

import { apiClient } from '@/lib/api-client';
import type {
  OrderFilters,
  OrdersResponse,
  Order,
  OrderMutationPayload
} from './types';

export async function getOrders(filters: OrderFilters): Promise<OrdersResponse> {
  const searchParams = new URLSearchParams();
  if (filters.page) searchParams.set('page', String(filters.page));
  if (filters.limit) searchParams.set('limit', String(filters.limit));
  if (filters.search) searchParams.set('search', filters.search);
  if (filters.status) searchParams.set('status', filters.status);
  if (filters.sort) searchParams.set('sort', filters.sort);

  return apiClient<OrdersResponse>(`/orders?${searchParams.toString()}`);
}

export async function getOrderById(id: number): Promise<Order> {
  return apiClient<Order>(`/orders/${id}`);
}

export async function createOrder(data: OrderMutationPayload): Promise<Order> {
  return apiClient<Order>('/orders', { method: 'POST', body: JSON.stringify(data) });
}

5. Implement React Query options

Use queryOptions and mutationOptions as the base abstraction.

  • queries.ts defines key factories and query options
  • mutations.ts defines shared mutation options and cache invalidation
  • components compose on top with useSuspenseQuery and useMutation

For every create, update, delete, assign, transfer, or return flow, treat post-mutation freshness as part of the feature contract, not as a nice-to-have. After a successful mutation, make sure all affected list views, detail views, timelines, counters, and dashboard summaries are synchronized immediately via invalidateQueries, setQueryData, optimistic updates, or a deliberate combination of them. Do not leave the user on stale data and do not rely on manual refresh as the normal recovery path.

See references/query-abstractions.md.

6. Build UI and pages

After the contracts are stable:

  • build feature components under src/features/<name>/components/
  • use server-prefetch plus HydrationBoundary
  • add page route under src/app/dashboard/<name>/page.tsx
  • wire navigation in src/config/nav-config.ts

Auth.js And App-Owned RBAC

Use this vocabulary consistently:

  • user: authenticated human actor
  • organization: active tenant or workspace
  • membership: relation between user and organization
  • role: membership level such as owner, admin, or member
  • permissions: optional fine-grained capabilities derived from role or stored explicitly

Do not use Clerk terms as if they still define the architecture.

Auth boundary

The skill should guide agents to centralize auth in:

  • auth.ts for Auth.js config and exported helpers
  • route protection in src/proxy.ts or middleware.ts
  • shared server helper for requireSession, requireOrganization, or requireRole

Example page protection:

import { auth } from '@/auth';
import { redirect } from 'next/navigation';

export default async function Page() {
  const session = await auth();
  if (!session?.user) redirect('/auth/sign-in');

  return <div>Protected dashboard page</div>;
}

Navigation visibility

Navigation should be filtered from app-owned session or membership data, not Clerk hooks.

If a nav item requires access, model it around app semantics:

access: {
  requireOrganization: true,
  role: 'admin'
}

If the repo still uses requireOrg, role, or permission in src/types/index.ts, treat that as a migration seam. Update the types and filtering logic only when the task reaches navigation or RBAC.

Plan and feature gates

Do not promise a drop-in replacement for Clerk Billing.

When a page is gated by plan or feature:

  • describe it as app-owned entitlement data
  • keep the check server-side
  • use placeholders until a billing provider exists

Good language: "read the organization's active plan from Drizzle and gate the page accordingly."

Bad language: "use Clerk Billing" or "use <Protect plan='pro'>".

Feature Page Pattern

Use the repo's existing feature layout, but keep the new data path:

import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
import { Suspense } from 'react';
import { getQueryClient } from '@/lib/query-client';
import { searchParamsCache } from '@/lib/searchparams';
import { ordersQueryOptions } from '../api/queries';
import { OrdersTable, OrdersTableSkeleton } from './orders-table';

export default function OrderListingPage() {
  const page = searchParamsCache.get('page');
  const perPage = searchParamsCache.get('perPage');
  const search = searchParamsCache.get('name');
  const sort = searchParamsCache.get('sort');

  const filters = {
    page,
    limit: perPage,
    ...(search && { search }),
    ...(sort && { sort })
  };

  const queryClient = getQueryClient();
  void queryClient.prefetchQuery(ordersQueryOptions(filters));

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <Suspense fallback={<OrdersTableSkeleton />}>
        <OrdersTable />
      </Suspense>
    </HydrationBoundary>
  );
}

The page flow stays the same. The difference is the backing source:

  • before: component -> service -> mock store
  • target: component -> service -> route handler -> Drizzle

Forms

Forms still use the existing project pattern:

  • useAppForm
  • useFormFields<T>()
  • Zod schema for submit validation
  • useMutation({ ...sharedMutationOptions })

See references/forms-guide.md.

When you show form examples, submit to service functions that hit route handlers. Do not wire forms straight to mock data and do not bypass auth or org checks.

Charts, Themes, And UI

Keep the existing UI conventions unless the task says otherwise:

  • charts use Recharts plus ChartContainer
  • themes use CSS variables and theme.config.ts
  • page headers go through PageContainer
  • icons come only from @/components/icons

For chart work, prefer data fetched through a real server path or a clearly labeled placeholder fetch. Avoid new examples that import delay from mock utilities as the default approach.

Legacy Migration Notes

Use these notes when the user is touching existing template code:

  • src/constants/mock-api.ts and src/constants/mock-api-users.ts are demo-only sources
  • src/features/*/api/service.ts is the intended seam for migrating consumers off mock data
  • src/app/api/products/** and src/app/api/users/** are good first route-handler migrations
  • src/hooks/use-nav.ts, src/proxy.ts, src/components/layout/providers.tsx, and auth-facing pages are likely Clerk hot spots

Recommend migrating feature-by-feature instead of flipping everything at once.

Reference Files

Open only the references needed for the task:

references/mock-api-guide.md is now legacy context only. Do not load it unless the user explicitly asks how the old demo data works.

Code Conventions

  • Use server components by default
  • Use cn() for class merging
  • Keep feature code inside src/features/<name>/
  • Keep the API layer split as types.ts -> service.ts -> queries.ts -> mutations.ts
  • Prefer useSuspenseQuery with server prefetch
  • Use mutationOptions with getQueryClient()
  • Every CRUD mutation must define explicit post-success cache sync for affected queries
  • Use apiClient for local route-handler calls
  • Keep auth and role checks server-side for real protection
  • Avoid direct imports from src/constants/mock-api*
  • Avoid direct use of Clerk helpers or components in new examples

What Good Guidance Looks Like

When this skill is working well, it should guide the agent toward:

  • Auth.js session helpers instead of Clerk auth primitives
  • Drizzle schema and route handlers instead of mock stores
  • organization-aware server checks instead of client-only vendor hooks
  • incremental migration steps that match the repo's real starting point