Skip to content

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

TechnologyPurpose
Next.js 14+App Router, Server Components, file-based routing
TypeScriptStrict mode enabled across both apps
Tailwind CSSUtility-first styling with design tokens
shadcn/uiAccessible component library (Radix UI + Tailwind)
React Hook FormPerformant form state management
ZodSchema-based validation with TypeScript inference
TanStack QueryServer state management with caching
Lucide ReactTree-shakeable icon set
SonnerToast notifications
TinyMCERich 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:

typescript
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:

typescript
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:

typescript
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:

typescript
// 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 files

Admin 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 files

TypeScript Configuration

Both apps enable strict mode and use path aliases for clean imports:

json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "paths": {
      "@/*": ["./*"]
    }
  }
}

This allows importing from any directory using the @/ prefix:

typescript
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:

bash
# 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=v1

TIP

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

Released under the MIT License.