A self-hosted enterprise AI-assistant platform — realtime chat where everyone gets a personal AI that acts for them via governed third-party connectors (MCP).
A high-performance monorepo with four microservices, a Flutter mobile client, and a Next.js web client — delivering secure, low-latency 1-on-1 and group chat, WebRTC calls, a RAG-backed AI assistant with long-term memory, and an enterprise foundation (workspace, departments, role-based access control) where the assistant can take real actions through governed MCP connectors (Notion, Google Workspace, custom servers).
New here? To evaluate or self-host PON, use
main— see Getting Started. To contribute, ONBOARDING.md explains the development branch and workflow. Architecture diagrams live in docs/architecture.md; how environments are kept apart, in docs/environments.md.Product direction: PON is evolving from a chat app into a self-hosted, single-tenant-per-deployment B2B AI-assistant platform (one deployment = one company). The vision, revised roadmap, and current build state live in docs/roadmap.md and CHANGELOG.md.
PON is a modular, full-stack platform composed of four backend microservices and two clients interacting asynchronously:
| Layer | Technology | Responsibility |
|---|---|---|
auth-service |
NestJS · TypeScript | Identity & tokens, OTP/OAuth, enterprise RBAC (workspace, departments, roles, permission matrix embedded in the JWT) |
chat-service |
Spring Boot 3 · Java 21 | Realtime message delivery (WebSocket/STOMP, Redis-bridged across instances), presence, chat REST/CRUD, attachments (GridFS), calls signalling, reminders, FCM push, external-bot bridge (Bot Factory personal assistants) |
ai-service |
NestJS · TypeScript | AI assistant pipeline (Anthropic Claude with per-message model routing), agentic tool loop, long-term memory, hybrid RAG, skills, digest/scheduler, usage quota; merges per-user MCP tools into the agent |
connector-service |
NestJS · TypeScript | MCP connectors: OAuth to third parties, AES-256-GCM token vault, MCP client, governed tool exposure (workspace/personal + capability gates) |
client |
Flutter 3 · Dart | Cross-platform mobile app (Android, iOS) using Riverpod & GoRouter |
web |
Next.js 16 · React 19 · Tailwind 4 | Responsive web client — shadcn/ui, TanStack Query (server data), Zustand (auth), next-intl, STOMP |
- MongoDB (single
platformdatabase): Holds canonical user profiles, conversations, messages, and knowledge base metadata. - RabbitMQ: Durable AI job queue — chat-service publishes to exchange
ai.direct→ queueai.requests, ai-service consumes. 30-second message TTL, dead-lettered toai.requests.dlq. - Redis: User presence (heartbeats), AI response streaming (
ai:response:{conversationId}), Knowledge Base job channels (kb:process,kb:delete), and the cross-instance STOMP fan-out bridge (stomp:broadcast) so realtime works with more than one chat-service instance. - Qdrant Vector DB: Two collections —
knowledge(KB document chunks) andai_memory(long-term memory facts).
- Workspace / Departments / Roles: each deployment is one company (a singleton Workspace). Members belong to departments and carry a Role whose permission matrix (data, admin-editable) rides in the JWT. Preset roles: Owner / Admin / Manager / Member.
- MCP connectors (
connector-service, :3003): users connect third-party accounts via OAuth; tokens are stored in an AES-256-GCM vault. The assistant gains live tools (mcp__<provider>__<tool>) — governed by capability (workspace vs personal connectors, an admin allow-list, custom-MCP = admin-only, sensitive-action gating).
┌─────────────────────────────────┐
│ Flutter / Web Client │
│ Riverpod · STOMP · WebRTC │
└────────┬─────────────────┬──────┘
│ REST/HTTP │ WS/STOMP (port 8080)
▼ ▼
┌──────────────────┐ ┌─────────────────────────────────┐
│ auth-service │ │ chat-service │
│ NestJS (3001) │ │ Spring Boot 3 (8080) │
└────────┬─────────┘ └────────┬───────────┬────▲───────┘
│ │ REST/ │ │ STOMP
│ JWT │ GridFS │ │ Broadcast
│ Sign ▼ │ │
│ ┌──────────────┐ │ │
│ │ MongoDB │ │ │
│ │ (db:platform)│ │ │
│ └──────────────┘ │ │
│ │ │ Redis pub/sub
│ │ │ ai:response:{id}
│ RabbitMQ (5672) │ ┌─┴────────────┐
│ ┌──────────────┐ │ │ Redis (6379) │
│ │ ai.requests │◄──┘ │ ai:response │
│ │ queue (AMQP) │ │ kb:process │
│ └──────┬───────┘ │ presence │
│ │ └──────────────┘
└──────────────────────────────►│ (JWT validate) ▲
▼ │
┌─────────────────────────────────┐
│ ai-service │
│ NestJS (Claude + Voyage AI) │
└──────┬──────────────────┬───────┘
│ Embedding │ Vector Search
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Voyage AI │ │ Qdrant DB │
│ Embeddings │ │ (port 6333) │
└──────────────┘ └──────────────┘
- AI Message Flow: User tags
@AIin conversation → Client sends message tochat-service→chat-servicepersists the message and publishes a job (with the last 20 messages of context) to the RabbitMQai.requestsqueue →ai-serviceconsumes the job via AMQP, recalls relevant memory facts and KB chunks from Qdrant, routes the request to a model tier (Haiku / Sonnet / Opus by message size and history), calls the Anthropic Claude Streaming API → stream chunks are published to Redisai:response:{conversationId}→chat-servicelistens on that Redis channel and forwards each chunk down the STOMP socket to the client in real time. - Knowledge Base (RAG) Flow: User uploads a document →
chat-servicestores the file in GridFS and publishes metadata to Rediskb:process→ai-servicedownloads the file, extracts text, chunks it, requests Voyage AI embeddings (voyage-4-lite, 1024 dims by default —KB_EMBEDDING_MODEL), and upserts them to Qdrant (scanned PDFs / images go through Claude vision first) →ai-serviceupdates the status todoneand notifies the client over WebSocket. - Connector / Action Flow: User connects an account on the Integrations screen →
connector-service(:3003) runs OAuth and stores tokens in the encrypted vault → during an AI request,ai-serviceasksconnector-service's internal API for the user's permitted tools and merges them into the agent loop → when the model calls a tool,ai-serviceproxies it back throughconnector-service, which enforces RBAC (capability + allow-list + sensitive-action gates) and executes it against the third party (remote MCP for Notion/custom, REST adapter for Google).
Clickable, code-linked diagrams generated with archify from the real source. Every node opens the exact file and line range on GitHub; the toolbar lets you switch Views, trace a route and export PNG/SVG. They are plain self-contained HTML files, so they also work offline from a clone — open docs/diagrams/<name>/index.html in any browser.
| Diagram | Interactive | Preview |
|---|---|---|
| PON runtime architecture — every service, data store and external API on the Mac Mini deployment, with the trust boundary | Open ↗ · source | ![]() |
auth-service — sign-in and a request with a JWT — password / Google / OIDC sign-in, mandatory 2FA for admin-like roles, Redis session + HS256 JWT carrying RBAC claims, per-service verification against sess:{sid}, refresh rotation, and the claims-stale path that re-issues tokens on role changes without logging out |
Open ↗ · source | ![]() |
chat-service — one ordinary message — STOMP/REST → auth gate → rate limit → MessageService persists to Mongo → ClusterMessageBroker → Redis stomp:broadcast → STOMP topic on every instance, plus GridFS upload, notification/FCM fan-out, presence and the rejection branches |
Open ↗ · source | ![]() |
ai-service (Module A) — one @AI request — chat-service → RabbitMQ → ai-service (5 gates, context, model router, agentic loop, tools) → Redis → STOMP → client, plus the error branches |
Open ↗ · source | ![]() |
connector-service — connect an account, AI calls a tool — one-time OAuth connect (HMAC state, AES-256-GCM vault) and the per-request path where ai-service lists and calls governed mcp__* tools through the Notion/Google REST and remote-MCP adapters, with live RBAC gates, token refresh and audit |
Open ↗ · source | ![]() |
The interactive links are served by GitHub Pages from the docs/ folder of main. Each diagram is pinned to the commit it was drawn from (shown in its footer), so regenerate it when the flow it describes changes — see docs/diagrams/README.md.
platform/
├── apps/
│ ├── server/
│ │ ├── auth-service/ # NestJS (:3001) — identity, tokens, enterprise RBAC
│ │ │ └── src/modules/ # auth (password/OTP/OAuth/OIDC SSO), users, friends,
│ │ │ # workspace (depts/roles/members), admin, audit,
│ │ │ # ai-context, notifications, firebase, Email
│ │ ├── chat-service/ # Spring Boot 3 (:8080) — messaging engine
│ │ │ └── src/main/java/com/platform/chatservice/
│ │ │ ├── config/ # STOMP, security, RabbitMQ, Redis
│ │ │ ├── controller/ # REST: messages, conversations, uploads, KB, calls,
│ │ │ │ # reminders, external bots, AI memory/persona, usage
│ │ │ ├── websocket/ # STOMP handlers
│ │ │ ├── service/ # messages, AI publish/stream, cluster broker, FCM,
│ │ │ │ # BotFactoryClient/ExternalBotService, reminders, calls
│ │ │ ├── security/ # JWT + STOMP channel interceptor
│ │ │ └── model/ repository/ dto/ domain/ exception/
│ │ ├── ai-service/ # NestJS (:3002) — AI pipeline
│ │ │ └── src/
│ │ │ ├── ai/ # Claude streaming, model router, agentic tool loop
│ │ │ ├── memory/ # long-term facts (MongoDB + Qdrant ai_memory)
│ │ │ ├── kb/ # extraction, vision, chunking, embeddings, rerank
│ │ │ ├── tools/ # static tools + MCP connector client
│ │ │ ├── skills/ persona/ ai-context/ settings/
│ │ │ ├── scheduler/ # daily digest cron (reminders are swept by chat-service)
│ │ │ ├── call/ # AI notetaker summaries
│ │ │ ├── usage/ retention/ session/ conversation/ bot/
│ │ │ ├── rabbitmq/ redis/
│ │ │ └── eval/ # eval gate for AI regressions (ai-eval.yml)
│ │ └── connector-service/ # NestJS (:3003) — MCP connectors
│ │ └── src/
│ │ ├── catalog/ # built-in connectors (Notion, Gmail, Calendar, Drive)
│ │ ├── oauth/ # OAuth start/callback per provider
│ │ ├── vault/ # AES-256-GCM token vault
│ │ ├── mcp/ adapters/ # remote-MCP + Google REST adapters
│ │ ├── connections/ # user connections + custom MCP CRUD
│ │ ├── directory/ bot/ audit/
│ │ └── internal/ # service-to-service tools API (ai-service)
│ ├── client/ # Flutter 3 — Android / iOS (Riverpod, GoRouter)
│ │ └── lib/
│ │ ├── core/ # api, config, l10n, providers, router, services, theme, utils, widgets
│ │ ├── features/ # auth, home, chat, friends, profile, settings, notifications,
│ │ │ # assistant, ai_hub, ai_context, skills, integrations,
│ │ │ # reminders, admin, help
│ │ └── l10n/ # app_*.arb (7 languages)
│ └── web/ # Next.js 16 — web client
│ ├── app/ # (auth), (legal), (main) route groups + api/
│ ├── components/ # ui (shadcn), chat, call, ai, admin, integrations, layout, …
│ ├── lib/ # api, stomp, store (Zustand), webrtc, hooks, auth, …
│ └── messages/ # next-intl *.json (7 languages)
├── packages/
│ └── database/src/
│ ├── mongo/ # shared Mongoose schemas (User, Workspace, Department, Role…)
│ ├── redis/ # shared Redis module
│ ├── rbac/ # capability catalog + preset role matrix (single source of truth)
│ └── auth/ # shared JwtAuthGuard + @RequirePermission (all NestJS services)
├── infra/docker-compose/
│ ├── compose.yml # local: Mongo, Redis, RabbitMQ, Qdrant, Jaeger + 4 services
│ ├── compose.mini.yml # production host: 4 services + Qdrant + Caddy + cloudflared
│ ├── compose.prod.yml # self-host: full stack incl. data + web behind Caddy
│ ├── Caddyfile, Caddyfile.mini
│ └── bootstrap.sh # self-host: create .env + generate secrets
├── scripts/
│ ├── ci/ # check-env-parity, check-env-leaks, check-dev-only
│ ├── mini/up.sh # start the production host + Cloudflare Tunnel
│ └── migrations/
├── docs/ # api-spec, architecture, environments, observability,
│ # decisions, roadmap, engineering/ (standards), runbooks/
└── pnpm-workspace.yaml
main is the production branch: always deployable, no seed data or test accounts.
git clone https://github.com/MITOM06/platform.git && cd platform/infra/docker-compose
./bootstrap.sh # creates .env, generates missing secrets, validates
docker compose -f compose.prod.yml up -d --build # full stack incl. data stores + web behind Caddybootstrap.sh generates every internal secret and stops until the required values are set in .env:
DOMAIN, WORKSPACE_NAME, BOOTSTRAP_OWNER_EMAIL, ANTHROPIC_API_KEY; then re-run it.
Environments and their differences: docs/environments.md.
Contributors work on the dev branch (main + local-env commits: seed data, local bring-up). Full
instructions — prerequisites, what to ask the owner for, the feature workflow — are in
ONBOARDING.md. The short version:
git clone https://github.com/MITOM06/platform.git && cd platform
git checkout dev # dev = main + the local-env commits
pnpm install
cp infra/docker-compose/.env.example infra/docker-compose/.env
cp apps/server/auth-service/.env.example apps/server/auth-service/.env
cp apps/web/.env.example apps/web/.env.local
./scripts/dev/up.sh --seed # whole stack in Docker, seeded, web on :3000Editing one backend service with hot reload instead? That is path B in
ONBOARDING — start the infrastructure by name, because compose.yml holds
the four services too and a bare up -d starts them as well:
docker compose -f infra/docker-compose/compose.yml up -d \
mongo mongo-setup redis rabbitmq qdrant jaegerEach service's .env.example is the source of truth for its variables — they are
kept in sync with the code by scripts/ci/check-env-parity.sh, so read those
rather than a list in this file.
| Deployment | Compose file | Env template |
|---|---|---|
| Local development | infra/docker-compose/compose.yml |
infra/docker-compose/.env.example |
| Production (single host + tunnel) | infra/docker-compose/compose.mini.yml |
infra/docker-compose/.env.mini.example |
| Self-host (one company, one stack) | infra/docker-compose/compose.prod.yml |
infra/docker-compose/.env.example + infra/docker-compose/bootstrap.sh |
Local and production differ by configuration only; how a build moves between them — one variable per client — is docs/environments.md.
Two invariants worth knowing before you start: JWT_ACCESS_SECRET must be
identical across all four services, and MongoDB is on port 27018 locally, not
27017.
End-to-end distributed tracing is wired through chat-service and ai-service via OpenTelemetry. A single @AI request produces a unified trace spanning chat-service (publish) → RabbitMQ → ai-service (agentic loop) → Redis → chat-service (STOMP deliver), all visible in Jaeger at http://localhost:16686.
See docs/observability.md for the full propagation protocol, span names, and environment toggles.
Two deployment shapes, all from the same commit — they differ by configuration only (docs/environments.md):
| Backend | Web | |
|---|---|---|
| This project today | one host behind a Cloudflare Tunnel, compose.mini.yml — the four services + Qdrant + Caddy; Mongo/Redis/RabbitMQ stay managed (arm64 images built by .github/workflows/build-mini-images.yml) |
Vercel, auto-deploy on push to main |
| Self-host (a customer) | compose.prod.yml — the whole stack, data included, behind Caddy on one domain |
served by the same Caddy |
Deployment model (by design): PON is self-hosted, one deployment per company. Each customer
runs their own instance; companies are isolated at the infrastructure level (no shared multi-tenant
database). One deployment = one Workspace. This is the strongest isolation and the basis of the
enterprise security posture — not a limitation. A turnkey self-host kit (bootstrap runbook + optional
Helm) is on the roadmap; today the docker compose stack + per-service .env is the deployment unit.
Operational notes:
- First-boot bootstrap — set
WORKSPACE_NAME+BOOTSTRAP_OWNER_EMAIL; the first boot seeds the workspace, preset roles, and assigns the Owner. - Monthly AI token quota — controlled by
AI_MONTHLY_TOKEN_LIMIT(default 500,000 tokens). Adjust inapps/server/ai-service/.envto match your Anthropic plan. - Models —
ANTHROPIC_SIMPLE_MODEL/ANTHROPIC_MID_MODEL/ANTHROPIC_COMPLEX_MODEL(defaultsclaude-haiku-4-5/claude-sonnet-4-6/claude-opus-4-8); setANTHROPIC_ROUTER_ENABLED=falseto always useANTHROPIC_MODEL.
- 🔒 Secure Connection: Strict JWT validation on REST endpoints and STOMP connection channels.
- ⚡ Realtime Messaging: WebSocket messaging supporting text, images, videos, and generic attachments.
- 📞 1-on-1 WebRTC Calls: Audio and video streaming over WebRTC channels.
- 👥 Group Conversations: Realtime membership handling, role updates, and system message logs.
- 🎭 Reactions: Double-tap quick reaction, reaction sheets, and details modal.
- ⚙️ Chat Utilities: Typing indicators, online/offline status heartbeats, mute/unmute notifications, message search, and conversation archiving.
- 🌍 Localization: Ready with 7 default languages (EN, VI, FR, ES, KO, ZH, JA).
- 🚀 AI Bot Member: Add
@AI/PON AIto any direct or group conversation. Responses are streamed token-by-token directly to the UI. - 🧠 Conversation Memory:
- Short-term: chat-service sends the last 20 messages of the conversation with every AI job.
- Long-term: every 10 turns (
MEMORY_EXTRACT_EVERY) the assistant extracts facts about the user, deduplicates them, and stores them in MongoDB + Qdrant (ai_memory); the most relevant facts are recalled per request with recency decay, and expire after 180 days (AI_MEMORY_TTL_DAYS). - AI Context: role-aware, admin-managed context blocks injected into the system prompt.
- Memory Screen: Integrated UI for users to view and delete facts that the AI has gathered.
- 📚 Knowledge Base & RAG:
- Document Parsing: Upload PDF, DOCX, or TXT documents directly in conversations.
- Vector Embedding Pipeline: Automated chunking, Voyage AI vectorization, and Qdrant ingestion.
- Hybrid retrieval: vector search over an enlarged candidate pool, fused with in-process BM25 keyword scoring (RRF), optional Cohere rerank (
COHERE_API_KEY), after a low cosine recall floor (≥ 0.2,KB_SCORE_THRESHOLD) — voyage-4-lite scores real answers as low as ~0.26, so a precision cut on raw cosine drops them. - Source Citation: Renders citation cards below AI messages, linking directly to referenced documents.
- 🧩 Integrations gallery: connect third-party accounts (Notion via the Notion REST adapter; Gmail + Google Calendar via the Google REST adapter; Google Drive listed as coming soon; any other MCP server via the MCP directory or a custom endpoint) over OAuth, with a clear view of the scopes the assistant can touch.
- 🛠️ Bring-your-own MCP: point the assistant at any MCP server — discover its tools and use them on the next message (admin-gated for security).
- 🤖 Agentic actions from chat: the assistant decides which tool to call and acts — e.g. "book a sync Thursday and email the brief" → calendar + mail tools run in one turn.
- 🎚️ Skills: turn capabilities (Scheduler, Mail writer, Researcher, Project keeper) on/off; each shows the connectors it needs.
- 🔐 RBAC: preset roles (Owner/Admin/Manager/Member) with an admin-editable permission matrix stored as data and enforced from JWT claims across every NestJS service.
- 🏬 Workspace & departments: one company per deployment; members belong to departments; admins manage roles, members, and the connector allow-list.
- 🛡️ Connector governance: workspace vs personal connectors, an admin allow-list, custom-MCP restricted to admins, and sensitive-action gating (e.g. sending mail) by capability.
- 📋 Auditability: privileged actions (workspace/department/member/role mutations, connector connect, custom-MCP add, sensitive-skill run) are recorded to an append-only audit log, viewable from the admin console on web and mobile.
- 🖥️ Admin console (web + mobile): capability-gated workspace settings, departments, members, the role × capability matrix, and the audit log — mirrored on both platforms.
- 🔑 Enterprise SSO (OIDC): config-driven OIDC login with JIT provisioning and group → role/department mapping; coexists with password login (web + mobile).
- 👁️ Vision / image understanding: the assistant reads images and scanned PDFs — both inline in chat and as Knowledge Base documents — via Claude vision.
- ⏰ Proactive reminders & daily digest: reminders are delivered as real in-chat AI messages (web + mobile) and an optional daily digest summarizes what's due.
- 📞 Group calls + AI notetaker: multi-party WebRTC calls with an opt-in AI notetaker that transcribes and posts a summary back to the conversation.
- 🤝 Personal assistant (Bot Factory bridge): a member's own 1-1 assistant from the separate Bot Factory product joins PON chat as an external bot member, alongside the company
@AI. - 🧭 Model routing & caching: short messages go to Haiku, mid-size to Sonnet, complex to Opus; prompt and tool-result caching cut cost.
- 🛡️ Cost & safety guards: per-user request rate limiting (20 req/min, 3 concurrent by default), a monthly token quota with a usage screen, prompt-injection spotlighting on retrieved/untrusted content, and sensitive-action gating before destructive tools run.
Full build state and roadmap (P0–P8 enterprise foundation, connectors, group bot, self-host kit, SSO — all complete) live in docs/roadmap.md and CHANGELOG.md.
PON is developed with AI coding assistants (Claude Code). The owner sets the architecture, technology choices and acceptance criteria and reviews every change; the assistant writes most of the code. What keeps that honest is the same for any contributor, human or AI:
- Standards in
docs/engineering/— cross-platform sync, i18n, no raw system data in the UI, clean-code limits, and the dev/main branch split. - A test gate —
scripts/test-all.shwith a coverage ratchet that may only go up (docs/engineering/test-gate.md); CI runs the same gate, plus gitleaks, on every pull request. - Review before merge — merging to
maindeploys production, so nothing lands unreviewed.
MIT © Trần Phúc Khang — see LICENSE.




