Modular Routing System
TinyKit Pro implements a centralized, modular route configuration system that separates routes by domain (core auth, public pages, admin panel, organizations...
TinyKit Pro implements a centralized, modular route configuration system that separates routes by domain (core auth, public pages, admin panel, organizations). This architecture enables clean separation of concerns and easy customization for forks.
Architecture Overview
The routing system is split into domain-specific configuration files in a centralized location (/src/config/routes/), making it easy to:
- Update specific domains without affecting others (e.g., admin panel updates don't touch public routes)
- Customize public routes in forks while preserving admin updates from upstream
- Maintain clear boundaries between authentication, public, admin, and organization features
Core Components
- Core Routes (
/src/config/routes/core.ts): Authentication routes, rarely customized - Public Routes (
/src/config/routes/public.ts): Public navigation and user dashboard, fork-customizable - Admin Routes (
/src/config/routes/admin.ts): Admin panel routes, frequently updated upstream - Organization Routes (
/src/config/routes/org.ts): Organization workspace routes - Dynamic Registry: Runtime registration system for menu items (admin module)
- Feature Toggles: Environment variable-based feature control
File Structure
src/
├── config/
│ └── routes/
│ ├── index.ts # Main entry point, re-exports all routes
│ ├── core.ts # Auth routes (rarely customized)
│ ├── public.ts # Public navigation + home routes (fork-friendly)
│ ├── admin.ts # Admin panel routes + dynamic registry
│ └── org.ts # Organization workspace routes
├── features/
│ └── navigation/
│ └── components/ # Navigation components
│ ├── root/ # Header, Footer (public)
│ ├── sidebars/ # Admin, User, Org sidebars
│ └── user-menu/ # User dropdown menu
└── app/ # App Router pages only (no component files)Route Configuration Files
Core Routes (core.ts)
Contains authentication routes that rarely need customization:
/**
* Authentication Routes
*
* Core authentication routes - rarely customized in forks.
* These routes handle sign-in, sign-up, password reset, etc.
*/
export const authRoutes = {
signIn: {
title: "Sign In",
href: "/auth/sign-in",
},
signUp: {
title: "Sign Up",
href: "/auth/sign-up",
},
resetPassword: {
title: "Reset Password",
href: "/auth/reset-password",
},
// ... other auth routes
} as const;Public Routes (public.ts)
Contains public navigation and home dashboard routes. Fork-friendly for customization:
/**
* Public Navigation Routes
*
* Routes shown in the public header (logged out state).
* Fork-customizable - change these to match your landing page structure.
*/
export const publicRoutes = {
home: {
title: "Home",
href: "/",
},
pricing: {
title: "Pricing",
href: "/pricing",
},
// ... customize for your fork
} as const;
/**
* User Dashboard Routes
*
* Routes for authenticated user home dashboard.
*/
export const homeRoutes = {
dashboard: {
title: "Home",
href: "/home",
icon: House,
},
account: {
title: "Account",
href: "/home/account",
icon: User,
},
// ... other dashboard routes
} as const;Admin Routes (admin.ts)
Contains admin panel routes and dynamic menu registration. Frequently updated upstream:
/**
* Admin Panel Routes
*
* Structured by sections for organized admin navigation.
* This file is frequently updated upstream - merge carefully in forks.
*/
export const adminRoutes = {
dashboard: {
title: "Dashboard",
href: "/admin",
icon: LayoutDashboard,
},
users: {
title: "Users",
href: "/admin/users",
icon: Users,
},
// ... other admin routes organized by section
} as const;
// Dynamic Registry for Admin Menu Items
export const adminMenuRegistry = {
items: [] as AdminMenuItem[],
};
export function registerAdminMenuItem(item: AdminMenuItem) {
adminMenuRegistry.items.push(item);
}
// Auto-initialization for admin menu
export function initAdminModule() {
registerAdminMenuItem({
type: "route",
title: "Admin Panel",
href: "/admin",
icon: Shield,
allowedUserRoles: ["admin"],
});
}Organization Routes (org.ts)
Contains organization workspace routes with dynamic slug-based routing:
/**
* Organization Workspace Routes
*
* Dynamic routes for organization workspaces using [orgSlug] parameter.
*/
export const orgRoutes = {
dashboard: {
title: "Dashboard",
href: "/orgs/[orgSlug]",
icon: LayoutDashboard,
},
settings: {
title: "Settings",
href: "/orgs/[orgSlug]/settings",
icon: Settings,
},
// ... other org routes
} as const;
// Helper to build org-specific routes
export function buildOrgRoute(
routeKey: keyof typeof orgRoutes,
orgSlug: string,
): string {
return orgRoutes[routeKey].href.replace("[orgSlug]", orgSlug);
}Centralized Configuration Benefits
Fork Management
The centralized structure makes fork management easier:
- Public Routes: Safe to customize heavily in forks (your landing pages, your structure)
- Admin Routes: Merge upstream updates carefully (admin features frequently added)
- Core Routes: Rarely changes upstream (auth is stable)
- Org Routes: Can customize per-fork if needed
// In admin.ts
export const adminMenuRegistry = {
items: [] as AdminMenuItem[],
};
export function registerAdminMenuItem(item: AdminMenuItem) {
adminMenuRegistry.items.push(item);
}
// Auto-initialize when routes module is imported
export function initAdminModule() {
registerAdminMenuItem({
type: "route",
title: "Admin Panel",
href: "/admin",
icon: Shield,
allowedUserRoles: ["admin"],
});
}The registry is auto-initialized on the client side when the routes module is imported:
// In index.ts
if (typeof window !== "undefined") {
void import("./admin").then((mod) => {
mod.initAdminModule();
});
}Navigation Component Integration
User Menu Integration
The user menu in the header dropdown uses routes from the centralized configuration:
// In user-menu.config.ts
import { homeRoutes, adminRoutes } from "@/config/routes";
export const getUserMenuConfig = (userRole?: string) => {
const menuItems = [homeRoutes.dashboard, homeRoutes.account];
// Add admin panel for authorized users
if (userRole === "admin") {
menuItems.push(adminRoutes.dashboard);
}
return menuItems;
};Sidebar Integration
Sidebars (Admin, User, Org) import routes from the centralized configuration:
// In AdminSidebar.tsx
import { adminRoutes, sectionConfig } from "@/config/routes";
export function AdminSidebar() {
return (
<aside>
{Object.entries(sectionConfig).map(([sectionKey, section]) => (
<div key={sectionKey}>
<h3>{section.title}</h3>
{section.routes.map((route) => (
<Link href={route.href}>{route.title}</Link>
))}
</div>
))}
</aside>
);
}Feature Toggles
Features can be conditionally enabled using database-driven feature settings, controlled via the admin panel (Admin > Site Settings > General).
Using Feature Settings
Feature settings are accessed via the useFeatureSettings() hook:
import { useFeatureSettings } from "@/lib/features";
// In components
function OrgSelector() {
const { enableOrganizations, isLoading } = useFeatureSettings();
if (isLoading) return null;
if (!enableOrganizations) return null;
return <OrganizationDropdown />;
}Benefits of Database-Driven Feature Toggles
- Runtime Control: Toggle features without deployment or restart
- Admin UI: Configure via Admin > Site Settings > General
- Real-time Updates: Changes propagate instantly to all connected clients
- Testability: Easy to toggle in tests without environment setup
Role-Based Access Control
Routes can include role restrictions that are enforced in navigation components:
// In admin.ts
export const adminRoutes = {
dashboard: {
title: "Dashboard",
href: "/admin",
icon: LayoutDashboard,
allowedUserRoles: ["admin"],
},
users: {
title: "Users",
href: "/admin/users",
icon: Users,
allowedUserRoles: ["admin"],
},
} as const;
// Helper to filter routes by role
export function getFilteredAdminRoutes(userRole?: string) {
return Object.values(adminRoutes).filter((route) => {
if (!route.allowedUserRoles) return true;
return userRole && route.allowedUserRoles.includes(userRole);
});
}Page-Level Protection
Backend protection is always enforced via Convex access control:
// In Convex backend
export const getAdminData = query({
args: {},
handler: async (ctx) => {
const { userId } = await requireAccess(ctx, {
userRole: ["admin"],
});
// User is authorized
return await ctx.db.query("adminData").collect();
},
});Authentication-Aware Navigation
Navigation components use Better Auth session state for authentication:
// In HeaderNav.tsx
import { publicRoutes, authRoutes } from "@/config/routes";
export function HeaderNav() {
const session = useSession();
return (
<nav>
{/* Public navigation */}
{Object.values(publicRoutes).map((route) => (
<Link key={route.href} href={route.href}>
{route.title}
</Link>
))}
{/* Auth state UI */}
{!session ? (
<>
<Link href={authRoutes.signIn.href}>{authRoutes.signIn.title}</Link>
<Link href={authRoutes.signUp.href}>{authRoutes.signUp.title}</Link>
</>
) : (
<UserMenu />
)}
</nav>
);
}Customization Guide
Customizing Public Routes (Fork-Friendly)
The public.ts file is designed for fork customization:
// In src/config/routes/public.ts
export const publicRoutes = {
home: { title: "Home", href: "/" },
features: { title: "Features", href: "/features" }, // Add your routes
yourCustomPage: { title: "Custom", href: "/custom" }, // Fork-specific
pricing: { title: "Pricing", href: "/pricing" },
} as const;Customize without worrying about upstream conflicts - this file is stable.
Adding Admin Routes (Merge Carefully)
The admin.ts file receives frequent upstream updates. When adding routes:
- Add to appropriate section in
sectionConfig - Use descriptive route keys
- Document custom routes for merge resolution
// In src/config/routes/admin.ts - Custom section
export const sectionConfig = {
// ... existing sections
custom: {
title: "Custom Features",
routes: [
{
key: "customFeature",
title: "My Custom Feature",
href: "/admin/custom-feature",
icon: Sparkles,
},
],
},
} as const;Extending Organization Routes
Add organization-specific routes in org.ts:
// In src/config/routes/org.ts
export const orgRoutes = {
// ... existing routes
customOrgFeature: {
title: "Custom Feature",
href: "/orgs/[orgSlug]/custom",
icon: CustomIcon,
},
} as const;
// Use the helper to build URLs
const url = buildOrgRoute("customOrgFeature", "my-org");
// Returns: "/orgs/my-org/custom"Best Practices
Route Organization
- Domain Separation: Keep auth, public, admin, and org routes in separate files
- Type Safety: Use
as constfor route objects to enable strict typing - Helper Functions: Use
buildOrgRoute()for dynamic slug-based routes - Role Restrictions: Include
allowedUserRolesarrays where needed
Import Patterns
// ✅ Good: Import from centralized index
import { publicRoutes, adminRoutes, authRoutes } from "@/config/routes";
// ❌ Bad: Import from individual files
import { publicRoutes } from "@/config/routes/public";
import { adminRoutes } from "@/config/routes/admin";Fork Management
- Public Routes: Customize freely - designed for forks
- Admin Routes: Merge carefully - frequent upstream updates
- Core Routes: Rarely customize - stable upstream
- Org Routes: Fork-specific customization safe
Navigation Components
- Centralized Imports: Always import routes from
@/config/routes - Role Filtering: Use helper functions like
getFilteredAdminRoutes() - Type Safety: Leverage TypeScript for route autocomplete
Migration Guide
From App Directory Routes to Centralized Config
The new structure centralizes routes in /src/config/routes/ instead of scattering them in app/_nav/ directories.
Migration steps:
- Move route definitions from
app/*/_nav/routes.config.tsto appropriateconfig/routes/*.tsfile - Update imports in navigation components to use
@/config/routes - Remove old
_navdirectories fromapp/after migration - Update component files - move any non-page components from
app/tofeatures/
Example migration:
// Before: app/admin/_nav/routes.config.ts
export const adminRoutes = {
dashboard: { title: "Dashboard", href: "/admin" },
};
// After: src/config/routes/admin.ts (centralized)
export const adminRoutes = {
dashboard: { title: "Dashboard", href: "/admin", icon: LayoutDashboard },
} as const;Troubleshooting
Common Issues
Import errors after refactor:
- Verify imports use
@/config/routesnot old@/app/*/_nav/routes.config - Check that centralized route files export all needed routes
Routes not appearing in navigation:
- Verify role-based filtering in
getFilteredAdminRoutes() - Check that user has correct
userRolefor restricted routes - Ensure routes are properly defined with required fields
TypeScript errors:
- Use
as conston route objects for proper type inference - Import types from
@/config/routesfor route definitions
Debug Tips
// In browser console - check available routes
import { adminRoutes, publicRoutes } from "@/config/routes";
console.log({ adminRoutes, publicRoutes });
// Check user role
const currentUser = useQuery(api.users.private.queries.getMe);
console.log("User role:", currentUser?.userRole);The centralized routing system provides clear domain separation, easier fork management, and better merge strategies for upstream updates while maintaining excellent type safety and developer experience.
Next.js 16 Patterns
TinyKit Pro uses Next.js 16.0.10 with React 19. This guide documents the key patterns and best practices used in the codebase.
MCP Integration (llms.txt)
TinyKit Pro includes a Model Context Protocol (MCP) integration via the /llms.txt endpoint. This endpoint provides site information optimized for consumption...