🚧🚧🚧 Note: this is still very much a WIP
This project is a demo of what I think might be some best practices for an agentic client/server stack that lets users create workflows for other users. My primary use case for this is educators creating lessons for students, but it could be anyone creating workflows for any group.
The agentic part infers AI infused, but it is more than that. Agentic also means flexible workflows, it means many data types, and it means reuseable components. It also means lots of asynchronous data flows. This last part, in particular, reminds me of a multiplayer game, which this design also lends itself to.
To accommodate all these aspects of an agentic client/server stack there are several architectural decisions that I have long wanted to try out. So this demo app is where I will experiement with it:
- We start with a stack similar to MERN (Postgres instead of Mongo) as a great way to manage data types across the frontend and backend.
- We use a websocket and events as the primary way of communicating from client to server.
- We use an event bus to organize data moving through services within the client and within server and on to third party services.
- We make the server be the source of truth for most data. The client may have smart components that maintain internal state, but any data that matters is coming from the server, even if it originates from the client.
- ie. the user types in some data we want persisted, the client model is not updated directly until it makes the round trip to the server and back.
- I wrestle with the fact that this could be limiting offline features but that is a task for another day.
- I also believe that this event structure will ultimately lend itself a future attempt to build off line event queueing and short circuited model updates.
- And at the core of all of this is workflow configuration. The primary means of building a new UI, data persistence, third party data processing is all through a json file. Think of an SPA in json format.
- new workflows can be configured by any combination of developers, non-developers, and AI tools.
- logic can live within the configuration file itself to avoid ai calls
- or logic can live in configured prompts with access to configured tools
- This requires that all UI components, persistence layer modules and third party services respond to events.
- On the client the configuration defines a layout, document model, component props and events, and data transforms triggered by those events.
- Likewise, on the server, the configuration defines the chain of data transforms and named callbacks sent to ai, persistence and other backend services.
- Side benefits to the event bus and configuration include:
- it allows us to easily track and debug complicated workflows as we can read through logs of every step in the process
- it allows us to replay events on the client for debugging purposes
- it allows us to easily incorporate multi-user features in workflows. ie. two users interacting with the same document cause model updates from one user to be broadcast to the client of the other user.
This work is about 30% done at the moment, but the pieces are falling in place (July 2026).
In diagram form, the flow starts in the context of authenticated users, groups and documents. The documents have a "type" and each "type" of document has a workflow config that defines what you can do. The process for creating workflow config JSONs is typically AI assisted but could also be manually written. I have used claude to author config files and now there is a workflow "type" that allows you to create new "type"s by interacting with AI to generate the config file.
Structure of the Config File JSON:

Upon loading a document we either create it's state from the config's initialization block or retrieve the state from the database. The source of truth for this state is always the server, but the client is kept in sync:

After the client loads the state, it also requests a view. There is always a default view and possibly other views into this document's state:

Client components receive props from the client state. They also emit events back to the server and to configured handlers that can transform data, persist data, query data, hit external services like AI, and return data to the client.

And all of these parts are tied together with messages sent from client to server and server to client:

What can you do with it? At the moment, you can login, create a group, add other logged in people to groups, and create documents. And the most interesting part is that you can create workflows (though pretty limited at the moment) that become new document types. For example you can create an activity for students by interacting with an AI chat bot to configure the activity. ie. ask it to tell the student to watch a video and take a quiz. Or to write a poem with the help of an AI assistant.
I will be adding more frontend components and backend services as we go along. And I will be adding more user, group, document and workflow management features.
For now you could also describe a simple new workflow to Claude and see what it does. There is enough precedent that it will easily configure the thing for you and might offer to create new components as needed.
Below, I am letting Claude take over the summary and instructions for using the app.
A full-stack monorepo built with Nx, React, Node.js/Express, and PostgreSQL.
| Layer | Technology |
|---|---|
| Monorepo | Nx 20, pnpm workspaces |
| Frontend | React 18, React Router v6, ESBuild |
| Backend | Node.js, Express, ESBuild |
| Database | PostgreSQL 16, Drizzle ORM |
| Auth | JWT, bcrypt |
| Unit/Integration Tests | Jest, Supertest, embedded-postgres |
| E2E Tests | WebdriverIO 8 |
apps/
api/ # Express gateway: REST API + WebSocket handling (port 3000)
api-e2e/ # Jest + Supertest integration tests
event-processor/ # Workflow engine, persistence, AI calls (port 3001)
event-processor-e2e/ # Jest integration tests for the workflow engine
web/ # React frontend (port 4200)
web-e2e/ # WebdriverIO e2e tests
libs/
shared-types/ # Shared TypeScript interfaces
access-control/ # Access-level types + cache
workflow-configs/ # JSON workflow definitions
db-schema/ # Shared Drizzle schema, migrations, Postgres test helpers
tools/
web-dev-server.mjs # ESBuild dev server for frontend
- Node.js 20+
- Docker (for local PostgreSQL — see setup below)
pnpm install
cp .env.example .envLocal Postgres runs via the repo's docker-compose.yml (Postgres 16, host port 5433 to avoid colliding with a locally-installed Postgres server on 5432). No manual binary install needed.
docker compose up -d # starts Postgres in the background
pnpm run db:migrate # applies migrations from libs/db-schema/drizzle/docker compose up -dnpx nx serve apinpx nx serve event-processornpx nx serve webdocker compose up -d
npx nx serve api &
npx nx serve event-processor &
npx nx serve web &Or, once servers are already running, use the pre-approved restart scripts (pnpm run restart:api / restart:processor / restart:web / restart:all) — they kill the old process, start the new one, and verify it's responding.
Once the first user has registered and logged in through the web app, run this once on the server to create the initial ("root") group and make that user its owner:
pnpm run create-root-group "root group"This only picks an owner automatically when it's the very first user in the system. For any later root group, ownership is inherited from the most recently created root group's owner(s) — it will not guess at an arbitrary user.
docker compose downIf running in the foreground: Ctrl+C in each terminal.
If running in the background:
pkill -f "nodemon"
pkill -f "web-dev-server"
pkill -f "ts-node.*main.ts"npx nx build api # outputs to dist/apps/api
npx nx build web # outputs to dist/apps/web
# or both at once:
npx nx run-many --target=build --all# API integration tests (no local Postgres required — spins up embedded-postgres per test file)
npx nx test api-e2e
# Frontend unit tests
npx nx test web
# WebdriverIO e2e tests (requires both servers running)
npx nx e2e web-e2epsql ships inside the Postgres container — no separate client install needed.
docker compose exec postgres psql -U postgres -d agentic_client_server_base-- List all users
SELECT * FROM users;
-- Find a specific user
SELECT * FROM users WHERE email = 'you@example.com';
-- List a user's group memberships and roles
SELECT g.name, mr.role
FROM memberships m
JOIN groups g ON g.id = m.group_id
JOIN membership_roles mr ON mr.membership_id = m.id
WHERE m.user_id = (SELECT id FROM users WHERE email = 'you@example.com');
-- List all tables
\dt
-- Exit
\qpgAdmin or TablePlus (both have free tiers) can connect directly to postgres://postgres:postgres@localhost:5433/agentic_client_server_base.
The frontend uses shadcn/ui — components are copied directly into apps/web/src/components/ui/ and are fully owned by this repo. Tailwind CSS powers the styling.
npx shadcn@latest add <component>Examples:
npx shadcn@latest add dialog
npx shadcn@latest add input
npx shadcn@latest add dropdown-menuComponents land in apps/web/src/components/ui/. Each component is plain TypeScript/React — edit them freely.
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog';The @/ alias resolves to apps/web/src/.
Use cn() to merge Tailwind classes safely (handles conflicts via tailwind-merge):
import { cn } from '@/lib/utils';
<div className={cn('p-4 rounded', isActive && 'bg-primary text-primary-foreground')} />Some shadcn components depend on @radix-ui/react-* packages not yet installed. Install them at the workspace root:
pnpm add -w @radix-ui/react-dialog
pnpm add -w @radix-ui/react-dropdown-menuColors are defined as CSS variables in apps/web/src/app/styles/global.css and mapped in apps/web/tailwind.config.js. To change the color scheme, update the --primary, --secondary, etc. values in the :root block. Dark mode variables live in the .dark block.
| File | Purpose |
|---|---|
components.json |
shadcn CLI config (component output path, aliases, Tailwind config) |
apps/web/tailwind.config.js |
Tailwind config with shadcn color tokens |
apps/web/src/app/styles/global.css |
CSS variable tokens + Tailwind directives |
apps/web/src/lib/utils.ts |
cn() utility |
apps/web/src/components/ui/ |
All shadcn component source files |
All routes prefixed with /api.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/health |
— | Health check |
POST |
/api/auth/register |
— | Register (email, password, confirmPassword) |
POST |
/api/auth/login |
— | Login (email, password) |
GET |
/api/users |
JWT | List all users |
GET |
/api/users/me |
JWT | Get current user |
PATCH |
/api/users/me |
JWT | Update email or password |
PATCH |
/api/users/:id/roles |
JWT + admin | Set a user's roles |
See .env.example for all options. Key variables:
PORT=3000
DATABASE_URL=postgres://postgres:postgres@localhost:5433/agentic_client_server_base
JWT_SECRET=your-secret-here
JWT_EXPIRES_IN=7d
CORS_ORIGIN=http://localhost:4200