Logo
Back to home

Building a Modern Insurance Management Dashboard

SoftwareEngineering
By Tran Minh Hoang LongMay 22, 2025

Building a Modern Insurance Management Dashboard

Insurance agencies face a constant challenge: managing complex workflows across customer registration, policy tracking, claims processing, and automation monitoring. Traditional systems are often fragmented, requiring multiple platforms and manual processes that slow down operations and increase the risk of errors.

This case study explores how we built a comprehensive insurance management dashboard using Next.js 15, TypeScript, and a carefully curated stack. The result is a unified platform that streamlines operations while providing role-based access for administrators, agencies, and planners.


The Challenge: Fragmented Insurance Workflows

Agencies typically juggle multiple systems---CRM, policy administration, claims processing, and automation tracking. Each often operates in isolation, creating data silos and requiring manual data entry.

Our goal: build a single, cohesive dashboard that handles:

- Customer registration with signature capture and address validation

- Real-time automation tracking and status monitoring

- Claims processing with dynamic PDF generation

- Role-based access control for different user types

- Notice & inquiry management with workflow automation


Architecture: Modern Stack for Complex Requirements

Technology Foundation

We chose Next.js 15 (App Router) with TypeScript for developer experience and runtime reliability.

Core Stack:

- Frontend: Next.js 15 with App Router

- Language: TypeScript

- UI Framework: shadcn/ui (Radix UI)

- Styling: Tailwind CSS

- State Management: Zustand (app state) + TanStack Query (server state)

- Forms: React Hook Form + Yup

- HTTP Client: Axios with automatic token refresh


Component Architecture Strategy

1 src/components/
2 ├── ui/ # Base shadcn/ui components
3 ├── molecules/ # Composite components (DataTable, Pagination)
4 └── organisms/ # Feature components (CustomerForm, Dashboard)

Authentication: Security-First Approach

- Insuary’s dashboard needed to expose dozens of insurance operations while keeping partner APIs and credentials off the client. We adopted Next.js App Router server actions so every network hop stays on the server, then hydrated the UI through React Query hooks that call those actions, giving us a fast, data-rich SPA without shipping secrets.

- Authentication starts in the loginAction, which performs the credential exchange with the upstream API and stores the resulting tokens and user profile in httpOnly cookies.

1export async function loginAction(credentials: LoginRequest) {
2 let shouldRedirectToTempPassword = false;
3
4 try {
5 const response: AxiosResponse<Response> = await axios.post(
6 `${process.env.NEXT_PUBLIC_URL}/api/v1/login`,
7 credentials,
8 {
9 headers: {
10 "Content-Type": "application/json",
11 },
12 }
13 );
14
15 const { data } = response.data;
16
17 // Set auth cookies first
18 await setAuthCookies(data.accessToken, data.refreshToken, data.maxAge);
19
20 // Set user cookie
21 await setUserCookie({
22 id: data.id,
23 name: data.name,
24 role: data.role,
25 agencyId: data.agencyId,
26 agency: data.agency,
27 userId: data.userId,
28 isTempPassword: data.isTempPassword,
29 });
30
31 // Check if temp password redirect is needed
32 shouldRedirectToTempPassword = data.isTempPassword || false;
33
34 return { success: true };
35 } catch (error) {
36 console.error("Login error:", error);
37 return {
38 success: false,
39 error: "아이디 또는 비밀번호를 확인해 주세요.",
40 };
41 } finally {
42 // Handle redirect outside try-catch
43 if (shouldRedirectToTempPassword) {
44 redirect("/temp-password");
45 } else {
46 revalidatePath("/");
47 }
48 }
49}

Cookie helpers enforce secure flags and run only on the server, ensuring identity never reaches the browser.

1import "server-only";
2
3import { cookies } from "next/headers";
4import { redirect } from "next/navigation";
5import type { User } from "@/types/dashboard";
6
7const COOKIE_OPTIONS = {
8 httpOnly: true,
9 secure: process.env.NODE_ENV === "production",
10 sameSite: "lax" as const,
11 path: "/",
12};

- Follow-up actions reuse the same pattern: they fetch the bearer token on the server, call the relevant endpoint, and return structured domain data—e.g. dashboard summaries. Because actions sit in the server runtime, the access token is injected into Axios headers without exposing it to client-side code.

1/**
2 * Server action to fetch dashboard summary data
3 * @returns Dashboard summary data or error object
4 */
5export async function getDashboardSummaryAction() {
6 try {
7 const { accessToken } = await getAuthTokens();
8
9 if (!accessToken) {
10 return {
11 success: false,
12 error: "인증이 필요합니다. 다시 로그인해 주세요.",
13 };
14 }
15
16 const response: AxiosResponse<{
17 data: DashboardSummaryData;
18 message: string;
19 }> = await axios.get(
20 `${process.env.NEXT_PUBLIC_URL}/api/v1/dashboard/summary`,
21 {
22 headers: {
23 Authorization: `Bearer ${accessToken}`,
24 "Content-Type": "application/json",
25 },
26 }
27 );
28
29 return {
30 success: true,
31 data: response.data.data,
32 };
33 } catch (error) {
34 console.error("Dashboard summary error:", error);
35 return {
36 success: false,
37 error: "대시보드 데이터를 불러올 수 없습니다.",
38 };
39 }
40}


- Client components stay “clean”: React Query hooks simply invoke server actions and manage UI state.

1/**
2 * Hook to fetch GA accounts list with filtering and pagination
3 * @param filters - GA account filter parameters
4 * @returns Query result with GA accounts list
5 */
6export const useGAAccounts = (filters: GAAccountFilter = {}) => {
7 return useQuery<GAAccountListResponse>({
8 queryKey: ["agencies", "list", filters],
9 queryFn: async () => {
10 const result = await getGAAccountsAction(filters);
11
12 if (!result.success) {
13 throw new Error(result.error || "Failed to fetch GA accounts list");
14 }
15
16 return result.data!;
17 },
18 staleTime: 5 * 60 * 1000, // 5 minutes
19 });
20};

The AuthProvider fetches the session through an internal API route that reads the secure cookies so the store initializes without ever touching tokens.

1export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
2 const { setUser, setLoading, logout } = useAuthStore();
3
4 useEffect(() => {
5 const initializeAuth = async () => {
6 setLoading(true);
7
8 try {
9 const response = await fetch('/api/user');
10 const result = await response.json();
11
12 if (result.success && result.data) {
13 setUser(result.data);
14 } else {
15 logout();
16 }
17 } catch (error) {
18 console.error('Failed to initialize auth:', error);
19 logout();
20 } finally {
21 setLoading(false);
22 }
23 };
24
25 initializeAuth();
26 }, [setUser, setLoading, logout]);
27
28 return <>{children}</>;
29};

- Defense-in-depth comes from layered refresh and guarding. Axios interceptor logic refreshes tokens when a 401 occurs

1
2// Request interceptor to add access token
3httpClient.interceptors.request.use(
4 async (config) => {
5 const { accessToken } = await getAuthTokens();
6
7 if (accessToken) {
8 config.headers.Authorization = `Bearer ${accessToken}`;
9 }
10
11 return config;
12 },
13 (error) => {
14 return Promise.reject(error);
15 }
16);

, middleware protects routes and enforces temporary-password flows before the request reaches React

1export async function middleware(request: NextRequest) {
2 const accessToken = request.cookies.get("accessToken")?.value;
3 const refreshToken = request.cookies.get("refreshToken")?.value;
4 const userCookie = request.cookies.get("user")?.value;
5 const { pathname } = request.nextUrl;
6
7 // Parse user data from cookie
8 let user: User | null = null;
9 if (userCookie) {
10 try {
11 user = JSON.parse(userCookie);
12 } catch (error) {
13 console.error("Error parsing user cookie:", error);
14 }
15 }
16
17 // Public routes that don't need authentication
18 const publicRoutes = ["/login", "/create-inquiry"];
19 const isPublicRoute = publicRoutes.includes(pathname);
20 const isTempPasswordPage = pathname === "/temp-password";
21
22 if (pathname === "/") {
23 return NextResponse.redirect(new URL("/dashboard", request.url));
24 }
25 // If no access token but we have refresh token, try to refresh
26 if (!accessToken && refreshToken && !isPublicRoute) {
27 const refreshResult = await attemptTokenRefresh(refreshToken);
28
29 if (refreshResult.success && refreshResult.response) {
30 // Token refresh successful, continue with the new tokens
31 return refreshResult.response;
32 } else {
33 // Token refresh failed, clear cookies and redirect to login
34 const response = NextResponse.redirect(new URL("/login", request.url));
35 response.cookies.delete("accessToken");
36 response.cookies.delete("refreshToken");
37 response.cookies.delete("user");
38 return response;
39 }
40 }
41
42 // If no tokens at all and trying to access protected route
43 if (!accessToken && !refreshToken && !isPublicRoute) {
44 return NextResponse.redirect(new URL("/login", request.url));
45 }
46
47 // Temp password logic - highest priority
48 if (user?.isTempPassword) {
49 // If user has temp password but not on temp-password page, redirect to temp-password
50 if (!isTempPasswordPage) {
51 return NextResponse.redirect(new URL("/temp-password", request.url));
52 }
53 } else {
54 // If user doesn't have temp password but is on temp-password page, redirect to home
55 if (isTempPasswordPage && accessToken) {
56 return NextResponse.redirect(new URL("/", request.url));
57 }
58 }
59
60 // If has access token and trying to access auth pages (except temp-password handled above)
61 if (accessToken && isPublicRoute) {
62 return NextResponse.redirect(new URL("/", request.url));
63 }
64
65 return NextResponse.next();
66}

, and server helpers can redirect unauthenticated requests with requireAuth.

1export async function requireAuth(): Promise<User> {
2 const user = await validateSession();
3
4 if (!user) {
5 redirect("/login");
6 }
7
8 return user;
9}

Outcome: all API calls originate from controlled server surfaces, authentication state lives in hardened cookies, and the client keeps the snappy React UX. We can add new features by writing another server action + hook pair without revisiting security scaffolding, and operations are confident that tokens never leak to the browser.


State Management: Three-Tier Strategy

1) Server State --- TanStack Query

1export const useCustomers = (params: CustomerSearchParams) => {
2 return useQuery({
3 queryKey: ['customers', params],
4 queryFn: () => customerApi.getCustomers(params),
5 staleTime: 5 * 60 * 1000,
6 retry: (failureCount, error) => !isAuthError(error) && failureCount < 3,
7 });
8};

2) Global App State --- Zustand

1interface AppStore {
2 isNavigationGuardEnabled: boolean;
3 setNavigationGuard: (enabled: boolean) => void;
4 modals: {
5 customerDetail: { open: boolean; customerId?: string };
6 };
7 openCustomerDetail: (customerId: string) => void;
8 closeCustomerDetail: () => void;
9}

3) Form State --- React Hook Form + Yup

1const customerSchema = yup.object({
2 name: yup.string().required('이름을 입력해주세요'),
3 phone: yup.string().required('전화번호를 입력해주세요'),
4 email: yup.string().email('올바른 이메일을 입력해주세요'),
5 birthDate: yup.date().required('생년월일을 입력해주세요'),
6});
7

-----------

Challenges & Solutions

1) Complex Form Validation

1const insuranceSchema = yup.object().shape({
2 insuranceType: yup.string().required(),
3 coverage: yup.number().when('insuranceType', {
4 is: 'life',
5 then: yup.number().min(10_000_000, '최소 1천만원'),
6 otherwise: yup.number().min(1_000_000, '최소 100만원'),
7 }),
8});

2) Role-Based UI Rendering

1export const useAuthStore = create<AuthStore>()(
2 devtools(
3 persist(
4 (set, get) => ({
5 // State
6 user: null,
7 isAuthenticated: false,
8 isLoading: false,
9
10 // Actions
11 setUser: (user: User) =>
12 set(
13 {
14 user,
15 isAuthenticated: true,
16 isLoading: false,
17 },
18 false,
19 "setUser"
20 ),
21
22 logout: () =>
23 set(
24 {
25 user: null,
26 isAuthenticated: false,
27 isLoading: false,
28 },
29 false,
30 "logout"
31 ),
32
33 setLoading: (loading: boolean) =>
34 set({ isLoading: loading }, false, "setLoading"),
35
36 // Role checking utilities
37 hasRole: (role: UserRole) => {
38 const { user } = get();
39 return user?.role === role;
40 },
41
42 hasAnyRole: (roles: UserRole[]) => {
43 const { user } = get();
44 return user ? roles.includes(user.role) : false;
45 },
46
47 isAdmin: () => {
48 const { user } = get();
49 return user?.role === "ADMIN";
50 },
51
52 isAgency: () => {
53 const { user } = get();
54 return user?.role === "AGENCY";
55 },
56
57 isPlanner: () => {
58 const { user } = get();
59 return user?.role === "PLANNER";
60 },
61 }),
62 {
63 name: "auth-storage",
64 partialize: (state) => ({
65 user: state.user,
66 isAuthenticated: state.isAuthenticated,
67 }),
68 }
69 ),
70 {
71 name: "auth-store",
72 }
73 )
74);
75
76// Selectors for better performance
77export const useUser = () => useAuthStore((state) => state.user);
78export const useIsAuthenticated = () =>
79 useAuthStore((state) => state.isAuthenticated);
80export const useUserRole = () => useAuthStore((state) => state.user?.role);
81export const useIsAdmin = () => useAuthStore((state) => state.isAdmin());
82export const useIsAgency = () => useAuthStore((state) => state.isAgency());
83export const useIsPlanner = () => useAuthStore((state) => state.isPlanner());
84

Performance Optimizations

1. Code Splitting: Lazy-load major features with dynamic imports

2. Query Tuning: Strategic `staleTime` and selective field fetching

3. Bundle Diet: Tree-shaking + dynamic imports → ~40% smaller bundle

4. Caching: Browser + HTTP caching for static assets and API responses


Development Best Practices

- Type Safety: 100% TypeScript coverage

- Code Quality: ESLint + Prettier with domain-specific rules

- Consistency: Standardized API hooks, composition patterns, and error handling

- Docs: JSDoc and ADRs for architectural decisions


Lessons Learned

1. Architecture Matters: three-tier state scaled cleanly

2. Type Safety Pays Off: `strict` mode prevented runtime errors

3. UX is Critical: smooth auth and loading states improved adoption

4. Performance Early: virtualization + caching avoided debt


Looking Forward

- Advanced analytics

- External insurance API integrations

- Microservices for scalability


Conclusion

Modern web tech can transform traditional insurance operations. By aligning architecture, developer ergonomics, and user experience, we built a platform that meets current needs and lays a foundation for future growth. The key is **balancing technical sophistication with practical usability**---every architectural decision should make users more productive in their daily work.

Live Demo (desktop only)

Username: a d m i n _ l o m e n (no space)

Password: a d m i n 1 2 3 4 (no space)