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.jsfor authentication and session accessDrizzle ORMfor persistencesrc/app/api/**/route.tsas the boundary for CRUD or BFF access- app-owned
organization,membership,role, and optionalpermissionsdata
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.tsandsrc/features/users/api/service.tsstill call mock datasrc/app/api/products/**andsrc/app/api/users/**already exist as route-handler shellssrc/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:
- introduces the auth shell
- introduces the shared org and RBAC model
- 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
membershiporrole - 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.tsdefines key factories and query optionsmutations.tsdefines shared mutation options and cache invalidation- components compose on top with
useSuspenseQueryanduseMutation
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 actororganization: active tenant or workspacemembership: relation between user and organizationrole: membership level such asowner,admin, ormemberpermissions: 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.tsfor Auth.js config and exported helpers- route protection in
src/proxy.tsormiddleware.ts - shared server helper for
requireSession,requireOrganization, orrequireRole
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:
useAppFormuseFormFields<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.tsandsrc/constants/mock-api-users.tsare demo-only sourcessrc/features/*/api/service.tsis the intended seam for migrating consumers off mock datasrc/app/api/products/**andsrc/app/api/users/**are good first route-handler migrationssrc/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/route-handlers-drizzle-guide.md for API boundary, Drizzle flow, and CRUD migration
- references/authjs-org-rbac-guide.md for Auth.js, org membership, and RBAC
- references/migration-from-clerk-and-mocks.md when the task is explicitly a migration
- references/forms-guide.md for TanStack Form usage
- references/query-abstractions.md for React Query patterns
- references/theming-guide.md for theme work
- references/charts-guide.md for overview pages and chart slots
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
useSuspenseQuerywith server prefetch - Use
mutationOptionswithgetQueryClient() - Every CRUD mutation must define explicit post-success cache sync for affected queries
- Use
apiClientfor 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