Frontend Overview
FORGE generates two independent Next.js 14+ applications that together form a complete frontend layer: a public-facing web application (apps/web) and an admin dashboard (apps/admin). Both applications share the same architectural patterns and technology stack but serve fundamentally different audiences.
Architecture
┌─────────────────────────────────────────────────────────────┐
│ Caddy Reverse Proxy │
│ (Automatic HTTPS, routing) │
├─────────────────────┬───────────────────────────────────────┤
│ │ │
│ myapp.test │ admin.myapp.test │
│ ┌───────────────┐ │ ┌───────────────────────────────┐ │
│ │ Web App │ │ │ Admin Dashboard │ │
│ │ (Next.js) │ │ │ (Next.js) │ │
│ │ │ │ │ │ │
│ │ Public pages │ │ │ User/Role management │ │
│ │ Auth flows │ │ │ Content editor │ │
│ │ User profile │ │ │ Menu builder │ │
│ │ Dynamic nav │ │ │ Translation management │ │
│ └───────┬───────┘ │ └──────────────┬────────────────┘ │
│ │ │ │ │
├───────────┴─────────┴──────────────────┴────────────────────┤
│ REST API (Rust/Axum) │
│ api.myapp.test │
└─────────────────────────────────────────────────────────────┘Both apps communicate with the same backend API over HTTPS. They share no runtime code, but follow identical conventions for API communication, authentication, internationalization, and component structure.
Technology Stack
| Technology | Purpose |
|---|---|
| Next.js 14+ | App Router, Server Components, file-based routing |
| TypeScript | Strict mode enabled across both apps |
| Tailwind CSS | Utility-first styling with design tokens |
| shadcn/ui | Accessible component library (Radix UI + Tailwind) |
| React Hook Form | Performant form state management |
| Zod | Schema-based validation with TypeScript inference |
| TanStack Query | Server state management with caching |
| Lucide React | Tree-shakeable icon set |
| Sonner | Toast notifications |
| TinyMCE | Rich text editing for content management |
Shared Patterns
Both applications implement four foundational patterns that ensure consistency across the frontend layer.
API Client
A typed fetch wrapper that handles JWT token injection, error responses, 401 redirects, and file uploads. Every API call flows through this singleton:
import { api } from "@/lib/api";
// GET request with query params
const users = await api.get<PaginatedResponse<User>>("/admin/users", {
params: { page: 1, per_page: 20 },
});
// POST request with body
const newUser = await api.post<ApiResponse<User>>("/admin/users", {
name: "Jane Doe",
email: "jane@example.com",
});
// File upload with multipart/form-data
const media = await api.upload<ApiResponse<Media>>("/media", file, "file");Auth Provider
A React context that manages authentication state, protects routes, and exposes permission checking:
import { useAuth, usePermission } from "@/components/providers/auth-provider";
function MyComponent() {
const { user, isAuthenticated, logout } = useAuth();
const canEditUsers = usePermission("users.edit");
if (!canEditUsers) return null;
return <div>Welcome, {user?.name}</div>;
}I18n Provider
Loads translations from the backend, provides a t() function with interpolation, and manages RTL direction:
import { useTranslation } from "@/components/providers/i18n-provider";
function MyComponent() {
const { t, locale, direction } = useTranslation();
return <h1>{t("auth.login_title")}</h1>;
}Protected Routes
Both apps use layout-level authentication checks. Unauthenticated users are redirected to the login page with a return URL:
// Redirect preserves the intended destination
router.push(`/login?redirect=${encodeURIComponent(pathname)}`);Project Structure
Both applications follow the same directory layout. Here is the canonical structure:
Web Application (apps/web)
apps/web/
├── app/
│ ├── layout.tsx # Root layout (providers, html dir/lang)
│ ├── page.tsx # Home page with i18n hero
│ ├── globals.css # Tailwind base styles
│ ├── (auth)/
│ │ ├── layout.tsx # Auth pages layout
│ │ ├── login/page.tsx # Login (email/password or mobile/OTP)
│ │ ├── register/page.tsx # Registration
│ │ └── password/
│ │ ├── forgot/page.tsx # Forgot password
│ │ └── reset/page.tsx # Reset password
│ ├── (protected)/
│ │ ├── layout.tsx # Auth guard layout
│ │ └── profile/
│ │ ├── page.tsx # View profile
│ │ ├── edit/page.tsx # Edit profile
│ │ └── password/page.tsx # Change password
│ └── [slug]/
│ ├── page.tsx # Dynamic content pages
│ └── not-found.tsx # 404 for missing content
├── components/
│ ├── site-header.tsx # Header with dynamic navigation
│ ├── site-footer.tsx # Footer component
│ ├── dynamic-nav.tsx # Menu API-driven navigation
│ ├── language-switcher.tsx # Locale dropdown
│ ├── providers/
│ │ └── i18n-provider.tsx # Internationalization context
│ └── ui/ # shadcn/ui primitives
├── lib/
│ ├── api.ts # API client singleton
│ ├── types.ts # TypeScript type definitions
│ ├── utils.ts # Utility functions (cn, etc.)
│ └── validations.ts # Zod schemas for forms
└── public/
└── messages/ # Static translation JSON filesAdmin Dashboard (apps/admin)
apps/admin/
├── app/
│ ├── layout.tsx # Root layout (providers)
│ ├── globals.css # Tailwind + shadcn theme
│ ├── (auth)/
│ │ └── login/page.tsx # Admin login
│ └── (dashboard)/
│ ├── layout.tsx # Sidebar + header layout
│ ├── page.tsx # Dashboard with stats
│ ├── users/ # User CRUD pages
│ ├── roles/ # Role management
│ ├── permissions/ # Permission viewer
│ ├── contents/ # Content editor (create, edit, list)
│ ├── menus/ # Navigation menu builder
│ ├── lookups/ # Lookup/reference data
│ ├── translations/ # Translation management
│ ├── settings/ # Application settings
│ ├── api-keys/ # API key management
│ ├── audit/ # Audit log viewer
│ └── profile/ # Admin profile
├── components/
│ ├── layout/
│ │ ├── sidebar.tsx # Collapsible sidebar navigation
│ │ └── header.tsx # Top header bar
│ ├── dashboard/
│ │ ├── stats.tsx # Dashboard statistics cards
│ │ └── recent-activity.tsx # Recent activity feed
│ ├── providers/
│ │ ├── auth-provider.tsx # Authentication context
│ │ ├── i18n-provider.tsx # Internationalization context
│ │ └── theme-provider.tsx # Dark/light mode provider
│ └── ui/ # shadcn/ui + custom components
│ ├── data-table.tsx # Sortable, filterable table
│ ├── rich-text-editor.tsx # TinyMCE wrapper
│ ├── language-switcher.tsx
│ ├── confirm-dialog.tsx
│ ├── translations-input.tsx
│ ├── content-translations-input.tsx
│ ├── permission-selector.tsx
│ └── ... # shadcn/ui primitives
├── lib/
│ ├── api.ts # API client singleton
│ ├── types.ts # TypeScript type definitions
│ ├── utils.ts # Utility functions
│ └── validations.ts # Zod schemas for all forms
└── public/
└── messages/ # Static translation JSON filesTypeScript Configuration
Both apps enable strict mode and use path aliases for clean imports:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"paths": {
"@/*": ["./*"]
}
}
}This allows importing from any directory using the @/ prefix:
import { api } from "@/lib/api";
import { Button } from "@/components/ui/button";
import type { User } from "@/lib/types";Environment Variables
Both applications use the same environment variable pattern:
# API URL for client-side requests
NEXT_PUBLIC_API_URL=https://api.myapp.test
# API URL for server-side requests (Docker internal network)
INTERNAL_API_URL=http://api:8080
# API version prefix
NEXT_PUBLIC_API_VERSION=v1TIP
INTERNAL_API_URL is used for server-side data fetching within Docker Compose. This avoids routing through the reverse proxy and provides faster, more reliable server-to-server communication.
What's Next
- Admin Dashboard -- Admin panel structure and CRUD pages
- Web Application -- Public-facing app, auth flows, and content pages
- Components -- Reusable UI components and their usage
- Forms & Validation -- React Hook Form + Zod patterns
- Data Fetching -- API client and data loading patterns
- Internationalization -- Multi-language support and RTL handling