Frontend Apps
This guide explains the standardized patterns for building React applications within the Eloquent platform. The primary architecture uses Server-Side Rendering (SSR) with Server Actions for initial data fetching, and browser-side API calls for mutations.
Architecture Overview
INITIAL LOAD (SSR)
Layout (Server) → Server Action → API Gateway → Services
MUTATIONS (Browser)
Page (Client) → Hook → API Client → API Gateway → Services
Pattern: Layout → Context → Hook → Page
| Layer | Location | Responsibility |
|---|---|---|
| Layout | Server Component | SSR data fetching, passes to context |
| Context | Client Component | Thin wrapper around hook |
| Hook | Client | Business logic, state, mutations via browser API |
| Page | Client Component | UI rendering using context |
Key Point: We do NOT use NATS or WebSocket from the frontend. All communication is HTTP-based.
@elqnt/* Packages
All frontend apps use shared packages from the @elqnt/* namespace:
| Package | Purpose |
|---|---|
@elqnt/api-client | API client for browser & server |
@elqnt/types | Shared types: ResponseMetadata, JSONSchema, User |
@elqnt/entity | Entity types and hooks |
@elqnt/workflow | Workflow types |
@elqnt/kg | Knowledge graph types |
@elqnt/agents | Agent types |
Server Actions Pattern
Server actions call the API Gateway using @elqnt/api-client/server:
// actions/customer-actions.ts
"use server";
import { createServerClient } from "@elqnt/api-client/server";
const client = createServerClient({
gatewayUrl: process.env.API_GATEWAY_URL!,
jwtSecret: process.env.JWT_SECRET!,
});
export async function getCustomersSSR(orgId: string) {
const result = await client.get<{ records: Customer[] }>(
"/api/v1/entities/customer/records",
{ orgId }
);
return { customers: result.data?.records || [] };
}
Async Server Layouts
CRITICAL: Layouts MUST be async server components that fetch initial data:
// app/(projects)/layout.tsx
import { getServerAuth } from "@/lib/server-auth";
import { getProjects } from "@/actions/projects-actions";
import { ProjectsContextProvider } from "./contexts/projects-context";
export default async function ProjectsLayout({
children,
}: {
children: React.ReactNode;
}) {
const { session, selectedOrgId } = await getServerAuth();
if (!session || !selectedOrgId) {
return (
<ProjectsContextProvider initialProjects={[]} orgId="">
{children}
</ProjectsContextProvider>
);
}
const projectsResult = await getProjects(selectedOrgId);
return (
<ProjectsContextProvider
initialProjects={projectsResult.projects || []}
orgId={selectedOrgId}
>
{children}
</ProjectsContextProvider>
);
}
Hook + Context Pattern
The Logic Hook (with useEntities)
// hooks/use-customers.ts
"use client";
import { useEntities } from "@elqnt/entity/hooks";
import type { EntityRecord } from "@elqnt/entity/models";
import { useAppConfig } from "@/app/providers";
interface Customer {
id: string;
name: string;
email: string;
status: string;
}
function toCustomer(record: EntityRecord): Customer {
return {
id: record.id,
name: record.fields.name,
email: record.fields.email,
status: record.fields.status,
};
}
export function useCustomers() {
const config = useAppConfig();
const { queryRecords, createRecord, updateRecord, deleteRecord, loading, error } = useEntities({
baseUrl: config.apiGatewayUrl,
orgId: config.orgId,
});
const fetchCustomers = async (filters?: Record<string, unknown>) => {
const result = await queryRecords("customer", { filters });
return result?.records?.map(toCustomer) ?? [];
};
const createCustomer = async (data: Omit<Customer, "id">) => {
const record = await createRecord("customer", { fields: data });
return record ? toCustomer(record) : null;
};
return { fetchCustomers, createCustomer, loading, error };
}
The Context Provider
// contexts/projects-context.tsx
"use client";
import { createContext, useContext, ReactNode } from "react";
import { useProjects, type UseProjectsReturn } from "../hooks/use-projects";
const ProjectsContext = createContext<UseProjectsReturn | null>(null);
export function ProjectsContextProvider({
children,
initialProjects,
orgId,
}: ProjectsContextProviderProps) {
const value = useProjects({ initialProjects, orgId });
return (
<ProjectsContext.Provider value={value}>
{children}
</ProjectsContext.Provider>
);
}
export function useProjectsContext() {
const context = useContext(ProjectsContext);
if (!context) {
throw new Error("useProjectsContext must be used within ProjectsContextProvider");
}
return context;
}
Data Flow
Initial Page Load (Fast, No Loader)
1. User navigates to /admin/customers
2. Server renders layout.tsx
3. layout.tsx calls getCustomersSSR()
4. Server action calls API Gateway (HTTP)
5. Gateway proxies to Entities Service
6. Data returned, passed to CustomersProvider
7. Page renders with data already available
Mutations (Create/Update/Delete)
1. User clicks "Create Customer"
2. Page calls createCustomer() from custom hook
3. Hook calls createRecord() from useEntities
4. useEntities fetches JWT from /api/gateway-token
5. Browser calls API Gateway with JWT
6. Gateway proxies to Entities Service
7. Hook updates local state optimistically
Complete Mini-App Structure
app/(projects)/
├── layout.tsx # Async server layout
├── page.tsx # Server/client component
├── loading.tsx # Loading UI
├── error.tsx # Error boundary
├── [id]/
│ └── layout.tsx # Nested async layout
├── actions/
│ └── projects-actions.ts # Server actions
├── hooks/
│ └── use-projects.ts # Business logic
├── contexts/
│ └── projects-context.tsx # Thin wrapper
└── components/
└── projects-list.tsx # Client component
Best Practices
SSR Data Loading
- Layouts MUST be async server components
- Pass initial data to
*ContextProvider - Use
Promise.allfor parallel fetching - Never use
useEffectto fetch data on mount
Hook + Context Pattern
- Separate business logic into
use*.tshooks - Keep context providers as thin wrappers
- Memoize hook return values with
useMemo
Type Safety
- Use generated types from backend (tygo)
- Use Zod for form validation
- Don't create duplicate interfaces
Next Steps
- API Gateway - Gateway configuration
- Type System - TypeScript generation from Go
- Entities Management - Entity CRUD patterns