Eloquent

Documentation

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

LayerLocationResponsibility
LayoutServer ComponentSSR data fetching, passes to context
ContextClient ComponentThin wrapper around hook
HookClientBusiness logic, state, mutations via browser API
PageClient ComponentUI 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:

PackagePurpose
@elqnt/api-clientAPI client for browser & server
@elqnt/typesShared types: ResponseMetadata, JSONSchema, User
@elqnt/entityEntity types and hooks
@elqnt/workflowWorkflow types
@elqnt/kgKnowledge graph types
@elqnt/agentsAgent 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.all for parallel fetching
  • Never use useEffect to fetch data on mount

Hook + Context Pattern

  • Separate business logic into use*.ts hooks
  • 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