A retrieval-augmented Knowledge Base, a Sales Intelligence Dashboard on top of it — and now a multi-tenant commerce layer that sells the AI through AWS Marketplace and meters every call with credits. One Bun process integrates AWS, external SaaS, and three UIs.
The RAG knowledge base is the shared foundation. The Sales Dashboard reuses its ingest pipeline and mirror tables, then layers deterministic analytics + LLM agents on top. The newest layer commercializes both: every tenant gets its own workspace, every AI call is credit-metered, and AWS Marketplace owns subscription truth.
Normalizes 8 sources into S3 + sidecar metadata, indexes them in a Bedrock Knowledge Base, and answers questions through web chat, Slack/Lark bots, and an MCP server.
Fuses HubSpot / Fathom / Redmine / Gmail into a daily pipeline report, a grounded sales chat, and the proactive @SilkSales Slack bot. Deterministic first, LLM second.
Multi-tenant workspaces sold via AWS Marketplace (ResolveCustomer / GetEntitlements / EventBridge). Every AI call passes a credit gate (402) and lands in a usage ledger; a platform admin console manages plans, tenants and the awsDimension mapping.
All retrieval, all AI inference, the corpus, and the subscription market live on AWS. All source data lives externally at third-party SaaS. The self-hosted process (one Bun/Elysia app behind a reverse proxy) is the only bridge between the two — it pulls from external APIs, writes to AWS, and serves three UIs.
S3 — the corpus: one object + .metadata.json sidecar per ingested docBedrock KB — vector index + semantic retrieval (S3 Vectors backend, equals/notEquals metadata filters)Bedrock Agents — console RAG agent (InvokeAgent): orchestration + memory + tool bindingBedrock Runtime — Converse for every plain LLM call (classifier, judge, narrators); Nova Pro → ClaudeMarketplace APIs — ResolveCustomer · GetEntitlements (us-east-1) · EventBridge lifecycleGoogle — Gmail corpus, Drive mirror, Google SSO (app + sales portal)Slack / Lark — @SilkAI + @SilkSales bots, tracked channelsFathom / Fireflies — meeting transcripts (webhook + daily reconcile)HubSpot — deals · companies · contacts (10-min cron, Private App token)Redmine — projects · issues · members (the "existing customer" anchor)Bun + Elysia — single process behind BASE_URL reverse proxy; all crons, webhooks, orchestration, meteringPostgreSQL — mirror tables, credits, subscriptions, usage ledger, traces, settings (58 models)React 19 + AntD — main app (JWT) · sales portal (SSO) · admin console (SUPERADMIN)Docker Compose — local Postgres 13.5 (+ Redis present but not wired)The connection graph: external SaaS is pulled in by our API (cron + webhook), normalized to S3 + Postgres, indexed by AWS, then served back out to chat / Slack / Lark / MCP — with the commerce layer gating every AI call.
| Service | Plane | Role in the architecture | Data direction |
|---|---|---|---|
| S3 | aws | Corpus store — normalized content + metadata sidecar; data source for the Bedrock KB; powers the (currently hidden) knowledge-base file tree. | API → S3 → KB |
| Bedrock Knowledge Base | aws | Vector index + semantic retrieval. Only filter vocabulary is equals/notEquals — tenant / sensitivity / source / month filters are pre-computed at ingest. | S3 → KB → all surfaces |
| Bedrock Agents | aws | Console-defined RAG agent (InvokeAgent) — owns orchestration, session memory, tool binding + KB filter for chat / Slack / Lark / MCP retrieval. | API ⇄ agent ⇄ KB |
| Bedrock Runtime (Converse) | aws | Every plain LLM call: chat time-intent classifier, eval judge, sales narrators (dealRisk / upsell / execAction / narrative), query rewrite. Default Nova Pro → Claude when unlocked. | API → Converse |
| AWS Marketplace | aws | Subscription commerce. ResolveCustomer at registration; GetEntitlements (us-east-1) is the entitlement truth; EventBridge lifecycle events arrive at our webhook. Events are triggers only — truth is always re-derived. | AWS → webhook → subscriptions |
| Gmail / Drive | external | Email + document corpus (3-day cron + push webhook; Drive mirror + extract). Google also provides SSO for the main app and sales portal. | Google → API (OAuth) |
| Slack | external | Two apps: @SilkAI (KB Q&A, tracked channels, 10-min poll) and @SilkSales (separate app, own signing secret, mid-day push + DM). Signature-verified webhooks. | API ⇄ Slack |
| Lark | external | KB Q&A bot + tracked chats; mirrors the Slack integration surface. | API ⇄ Lark |
| Fathom · Fireflies | external | Meeting transcripts (webhook + a daily reconciliation sweep that retracts deleted recordings from S3 / DB / KB). | SaaS → API → S3 + DB |
| HubSpot | external | CRM: deals / companies / contacts (10-min cron). Needs a Private App token — the whole sales layer runs on empty data until one is set. | HubSpot → API |
| Redmine | external | Projects / issues / members; matched to HubSpot companies to decide "existing customer" for upsell. | Redmine → API |
| Bun + Elysia API | self | The only compute. 14 crons, 6 webhooks, RAG orchestration, sales aggregation, credit gate, marketplace, admin console. Exposed at /api behind the reverse proxy. | hub — all planes |
| PostgreSQL | self | Mirror + dedup tables (deterministic queries), subscriptions / credits / usage ledger / traces, settings. Tenant-scoped fail-closed. | API ⇄ DB |
| React apps ×3 | self | Main app (JWT), sales portal (Google SSO, separate token), admin console (SUPERADMIN, no tenant scope). All typed end-to-end via Eden Treaty. | Browser → API |
Architect's note: there is deliberately no cross-account AWS infrastructure beyond what's listed — no Lambda, no SQS, no API Gateway of our own. The Elysia process is the single integration point: it owns every credential, every webhook verifier, and every cron. AWS holds the intelligence; the process holds the integration.
Every source normalizes content into an S3 object plus a .metadata.json sidecar. One Bedrock Knowledge Base indexes them; three surfaces retrieve with the same filter vocabulary. Every run is also traced (ai_query_traces: intent → retrieval config → context → answer).
Every source writes both representations. The structured plane makes the next cron tick cheap and powers deterministic queries; the semantic plane powers free-text retrieval.
Indexed by the Bedrock Knowledge Base for free-text retrieval across chat, Slack/Lark and MCP. The sidecar carries the filterable metadata (type, source, month, year…) plus the governance keys tenant_id / client_id / sensitivity.
Dedup ("have I seen this id?"), sync bookkeeping, and deterministic queries. A row's existence is what lets the next tick skip already-ingested ids — and what the Sales system reads.
HTTP routes — status, manual "sync now", account CRUD, webhook (signature-verified).
Fetch + normalize logic for that source's external API.
The run loop — fetch → filter → write S3 (+meta) → upsert Postgres → trigger KB.
Interval scheduler wired into main.ts; runs wrapped by runSyncTask.
The store supports only equals / notEquals — no ranges. That single constraint shapes the whole metadata design.
| Source | Key filterable metadata | Note |
|---|---|---|
| shared | month year updated_at tenant_id | Pre-computed at ingest — the equals-only workaround for date ranges. tenant_id is ANDed into every retrieval (fail-closed tenancy). |
| meeting | type title recording_id date is_external | Fathom = type:meeting, Fireflies = type:fireflies. |
| slack | source channel client_id is_private | client_id enables strict per-customer scoping. |
| redmine | source project_name ticket_id status | Omits month/year to stay under the ~10-attribute cap. |
source account from_addr subject direction | Attachments inherit the parent's metadata. |
Hard cap: ~10 filterable attributes per chunk. A field is promoted to metadata only if it's filtered on; everything else stays inside the embedded body text. That's why duration_minutes lives in the markdown, not the sidecar. Governance keys (tenant_id, client_id, sensitivity) always take precedence over content keys.
Four personas route by classifier — knowledge · customer · generative · ops — plus the sales intent when the KB filter preset points at sales data.
Two backends: bedrock (InvokeAgent — full orchestration + memory) and stream (Retrieve + ConverseStream, real token deltas). Citations dedupe to inline [1] refs; every run writes a trace.
A cheap Converse call classifies the question to an enabled agent, then InvokeAgent answers scoped to the current channel. Suffixed with — answered by <byline>.
search_knowledge, get_customer_brief, get_meeting_summary, list_recent_docs — for external MCP clients (Claude Desktop, etc.), authenticated by static per-tenant key or OAuth (RFC 7591 DCR + .well-known discovery).
Status: the knowledge-base page and /api/s3 tree API are temporarily hidden (commit 0ea0633) — KB content is currently consumed through Chat and the Agents pages. A migration to drop S3 / Bedrock KB / Bedrock Agents in favor of self-controlled pgvector RAG + agentic loop is proposed but not implemented (docs/2026-08-16_migration-remove-bedrock-kb-agents.md).
Deterministic aggregation reads the mirror tables first; LLM agents only narrate, rank and classify on top — they never invent figures.
Numbers come from read-time SQL over mirror tables. LLMs narrate, rank and classify on top — pinned to candidate indices so they can never fabricate a deal or a figure.
open/won/lost, by-stage/owner/quarter, Deal Pulse (stale/overdue), rep health, weighted forecast vs target, win-loss, velocity, movement, quota — all read-time aggregation, owner-scoped.
dealRisk · upsell · execAction · narrative — one sales_reports row per run with deltas vs the last. Best-effort: a failed narrative still ships a SUCCESS report. Action acknowledgements land in action_resolutions; pushed alerts are deduped via silksales_sent_alerts.
The match between a HubSpot company and a Redmine project is what makes "existing customer" and upsell possible. Auto after every sync, with a human review gate for fuzzy matches.
Exact + suffix-stripped name match, then a hierarchy walk so a subproject inherits its parent's company. MatchType = exact · normalized · inherited · manual.
manual+company = pinned · manual+null = durable "not a customer" exclude · null = auto. Auto-match never overrides a manual decision.
Fuzzy (normalized) matches surface in a "Needs review" filter; POST /customers/confirm pins a page of them to manual.
3 headline numbers + AI daily briefing + weighted-Q / of-target stats.
Per-rep health (Active / Quiet / Dark), tier, calls-this-week, last activity.
Deterministic candidates → LLM chief-of-staff re-rank, never fabricated.
push_risk · competitive · stalled · quarter_slip · se_gap.
Filterable table: stale ≥30d, overdue, days-since-update, risk badge.
Won/open vs quarter & year target; upsell incl. launch-approaching.
Its own Slack app (token + signing secret). In "sales mode" it answers everything via the sales agent + daily-snapshot grounding. A mid-day cron pushes critical signals.
HubSpot credential gotcha: needs a Private App access token (pat-…, scopes crm.objects.{deals,companies,contacts}.read). A bare Developer API Key / legacy hapikey does not work — until set, the whole intelligence layer runs on empty data.
AWS is the source of truth for subscription state; our credits mirror it locally and enforce usage. Events are triggers — every decision re-derives truth from GetEntitlements.
Contract-based billing: AWS sells the subscription, we enforce usage locally with credits. No Metering API — BatchMeterUsage is deliberately not implemented.
Before every AI call (chat, agents, sales narrators, KB search): billingService.precheck(tenantId) throws 402 when the balance is ≤ 0. Free/internal tenants (billingExempt) skip the gate entirely.
After the call, tokens are converted to credits (creditPerInputToken / creditPerOutputToken from billing_settings, 30s cache) and debited from tenant_credits; each call lands a row in ai_usage_logs. Searches consume a fixed searchCreditCost. Debits may go negative — the next precheck fails closed.
Tenants see usage in the main app (/ai-usage: series, rollups, no-result proxy, module/status grouping); the Chat UI surfaces per-query traces from ai_query_traces. Both tables are tenant-scoped.
plans carry credits / limits / period plus an awsDimension column mapping to the AWS entitlement dimension — admin-managed, strict (no env fallback), delete-guarded while in use.
auth guard. Email/password or Google SSO; injects { auth, scope } and refuses tokens without a tenantId.
salesAuth guard. Separate Google-SSO token kind; re-resolves the user per request and rejects missing/suspended tenants.
adminAuth guard. Platform console with no tenant scope: tenants, plans + awsDimension, user roles, audit log. tenant_settings is deliberately unscoped.
mcpController accepts a static per-tenant key (tenant_mcp_keys) or OAuth (DCR per RFC 7591, auth codes, 30-day refresh, tokens scoped to /api/mcp).
One codebase, N isolated workspaces. There is no global "current tenant" — every query on tenant-owned data must be explicitly scoped.
The only sanctioned helpers: tenantWhere(scope) on reads/updates/deletes, tenantData(scope) on creates. A bare findMany on a tenant-owned model is a bug — no Prisma auto-injection extension exists as a backstop. Scopes are resolved at the entry point (guard, webhook resolver, cron loop, OAuth state) and threaded down.
KB retrieval always ANDs the tenant bound into the metadata filter — an unscoped MCP/chat retrieval refuses to run. The MCP tool chain ANDs tenantIdEqualsFilter into every query. eval_* and oauth_* tables are intentionally global.
AWS-side config still pending (product side): seller account, Product Code → AWS_MARKETPLACE_PRODUCT_CODE, EventBridge rule → webhook, IAM for ResolveCustomer / GetEntitlements, and the real dimension names to fill into plans.awsDimension. Code is done (docs/2026-08-14_launch-plan-aws-marketplace.md).
One Bun process. Elysia API with end-to-end type safety via Eden Treaty; React 19 + Ant Design front-end; Amazon Bedrock for all inference; AWS Marketplace for commerce.
Single-process API, /api prefix, public + 3 guarded groups (auth / salesAuth / adminAuth).
58 models: mirror tables, sync bookkeeping, subscriptions, credits, ledger, traces, settings.
Retrieve · InvokeAgent · Converse. Default Nova Pro → Claude when unlocked. Marketplace = ResolveCustomer / GetEntitlements / EventBridge.
Main app + sales portal + admin console; Eden Treaty end-to-end types; Zustand; violet _sales/ui kit.
58 Prisma models across three groups: the RAG mirror + governance, the Sales analytics, and the commerce/platform core.
| RAG / ingest | Holds |
|---|---|
| email_messages | Gmail dedup + S3 key + direction |
| drive_files | Mirrored Drive files |
| redmine_* | projects · issues · members · roles · users |
| slack_messages · tracked_channels | Tracked-channel messages |
| lark_messages · tracked_chats | Tracked-chat messages |
| fathom / fireflies_meetings | Transcripts + attendees |
| hubspot_* | accounts · companies · contacts · deals |
| slack_agents | Personas + kbFilter presets (4 intents + sales) |
| slack_bots · lark_bots | Per-account bot credentials + signing secrets |
| tenant_settings | Per-tenant switches (sensitivity privacy filter…) |
| sync_tasks · items | Per-run bookkeeping |
| eval_test_cases · reports · results | Agent regression tests (daily sweep) |
| projects · accounts · webhooks · mcp_keys | Source accounts + webhook secrets + MCP keys |
| Sales-specific | Holds |
|---|---|
| hubspot_deal_snapshots | One row per deal per day |
| sales_reports | Daily report payload + deltas |
| risk_flags | Deal-risk agent output |
| exec_actions | Ranked action list |
| action_resolutions | Action ack / status tracking |
| upsell_cards | Upsell opportunities |
| reps · sales_settings | AE roster + quota targets + cached pipeline config |
| silksales_sent_alerts | Push dedup (mid-day alerts) |
| Commerce / platform | Holds |
|---|---|
| tenants · users | Workspaces + members (roles, Google SSO) |
| plans · subscriptions | Plan catalog + awsDimension mapping; AWS-linked subs |
| tenant_credits · credit_transactions | Balance + full credit ledger |
| billing_settings | Credit-per-token rates, search cost, period |
| marketplace_customers | AWS account id → tenant mapping (licenseArn, productCode) |
| ai_usage_logs · ai_query_traces | Metered AI ledger + per-query traces |
| tenant_mcp_keys · tenant_webhooks | MCP access + per-tenant webhook config |
| oauth_clients · auth_codes · tokens | OAuth for external MCP clients (global) |
| admin_users · admin_audit_logs | SUPERADMIN console + audit trail |