$open case-study/01
project 01ai · multi-tenant saas

AI Customer Service Platform

A multi-tenant support platform where several businesses share one deployment: a unified conversation inbox across WhatsApp and Telegram, an AI assistant grounded in each tenant's own knowledge base, and an admin surface for the teams running it.

role
Frontend architecture & lead implementation
type
Multi-tenant SaaS platform
stack
Angular · NgRx · Rust · Axum · PostgreSQL · pgvector
timeframe
2025 — ongoing
github ↗Live demo — not public (multi-tenant data)
app.support / inbox
full inbox view with AI assistant panel
01

Product Context

context.md
what it is

A customer-service workspace that collects messages from WhatsApp and Telegram into one inbox, with an AI assistant that drafts replies from the tenant's knowledge base.

who it serves

Support agents handling day-to-day conversations, and the admins who configure channels, articles and team access for each tenant.

why it matters

Customers already message businesses on chat apps. Consolidating those threads and grounding answers in real documentation is what makes small teams able to keep up.

02

The Challenge

04 constraints
  1. 01Tenant isolation the UI can be trusted onEvery request, cache entry and socket subscription has to be scoped to the active tenant. A single stale cache key is a data-leak bug, not a rendering bug.
  2. 02Live conversation stateMessages arrive from external channels at any moment while an agent is typing, filtering or switching threads. Optimistic updates must reconcile with server order.
  3. 03Grounding AI answersA generic model answer is worse than none. Replies need retrieval over the tenant's own content and a visible trail back to the source passage.
  4. 04Channel differencesWhatsApp and Telegram differ in message types, delivery semantics and attachment handling. The UI needs one mental model without lying about what a channel supports.
03

My Role

Frontend architecture, end to end.

I owned the Angular application and worked down the stack into the Rust API where the frontend's data shape demanded it.

Frontend architecture

Feature boundaries, tenant-scoped store design, routing and the shared component layer.

AI product surface

The assistant panel: suggestion states, streaming output, source citations and failure handling.

API contract

Defined the response shapes with the backend and implemented parts of the Axum service and its queries.

Quality bar

Accessibility for the inbox keyboard flow, render performance on long threads, and the testing strategy.

04

Architecture

06 layers
L1 · presentation
User interface
Inbox, conversation view, knowledge base, admin.
AngularTailwind
L2 · components
Component & design-system layer
Presentational components with typed inputs, OnPush change detection.
StandaloneOnPush
L3 · state
State management
Tenant-scoped feature stores; signals for local UI state.
NgRxSignals
L4 · transport
API layer
Typed HTTP clients plus a socket channel for live messages.
HTTPWebSocket
L5 · services
Backend services
Tenant resolution, channel webhooks, retrieval and completion.
RustAxum
L6 · data
Data & AI
Relational data with row-level tenant scoping; embeddings alongside it.
PostgreSQLpgvectorLLM
05

Technical Decisions

04 records

Each decision below records what it cost as well as what it bought.

decision 01
problem
Tenant context leaking between sessions in a shared client cache.
decision
Tenant identity is part of the store shape, and switching tenant destroys and re-creates feature state rather than merging it.
reason
Isolation becomes structural instead of a rule every developer has to remember on every new selector.
trade-off
A full refetch on tenant switch, and no cross-tenant views without deliberate new work.
decision 02
problem
Webhook ingest plus embedding work on the same request path as agent traffic.
decision
Rust with Axum for the API layer instead of extending an existing Node service.
reason
Predictable latency and memory under concurrent channel webhooks, with strong compile-time guarantees on tenant-scoped queries.
trade-off
A smaller pool of contributors and slower iteration on early, still-changing endpoints.
decision 03
problem
Retrieval needs vector search, but the product already depends on relational data.
decision
pgvector inside the same PostgreSQL instance rather than a dedicated vector database.
reason
Embeddings stay joinable to tenant and document rows, so one transaction and one backup story covers everything.
trade-off
Index tuning is bound to Postgres, and very large corpora would eventually need a separate service.
decision 04
problem
Two kinds of state — server truth and transient UI — were tangled in one store.
decision
NgRx for conversation and channel data; signals for local UI state such as panels, drafts and filters.
reason
Server state gets traceable, debuggable transitions; ephemeral UI state stays where it is used with far less ceremony.
trade-off
Two state idioms in one codebase, which needs a written convention to stay legible to new engineers.
06

Knowledge Base

Answers are only as good as the source.

Each tenant curates its own articles. Documents are chunked and embedded on ingest, and every assistant reply surfaces the passages it drew from, so an agent can verify before sending.

app.support / knowledge
knowledge base + cited sources
07

Admin & Tenants

app.support / admin
admin dashboard + tenant switcher

Switching tenant is a hard reset, by design.

Changing tenant tears down feature state and re-subscribes rather than merging caches. It costs a refetch and removes an entire class of cross-tenant bug.

08

Engineering Quality

08 dimensions
Accessibility

Keyboard navigation across inbox, thread and composer; live regions announce new messages; visible focus throughout.

Performance

Virtualised message lists, OnPush components, and lazily loaded feature routes.

Testing

Unit tests on reducers and selectors, component contract tests, Playwright on the send-and-reply path.

Error handling

Typed failure states for send, retrieval and model timeouts, each with a recovery action rather than a toast.

State management

One direction of data flow; derived data via selectors; no component reaching into another's state.

Component architecture

Standalone components split into container and presentational roles with typed inputs and outputs.

Responsive design

Three-pane desktop layout collapses to a stacked, one-view-at-a-time flow on small screens.

Maintainability

Feature-sliced folders, shared design-system primitives, and lint rules that block cross-feature imports.

09

Outcome

● in active development

Where it stands.

resultsA working multi-tenant platform: agents answer WhatsApp and Telegram conversations from one inbox, with assistant drafts grounded in tenant documentation.
technicalTenant isolation enforced by store structure and row-level scoping; retrieval served from the same database as the relational data.
lessonsSplitting server state from UI state early prevented most of the bugs I expected from live channels. Retrieval quality depended far more on chunking and source curation than on model choice.
statusIn active development. Next up: per-tenant analytics, more channels, and a reply-suggestion evaluation loop.
next projectInventory Desktop Application