Skip to content

Architecture

One Node process serves the API, the SPA and the MCP endpoint. There is no separate frontend server, no worker container and no queue service.

Request paths

PrefixHandled by
/api/*Hono JSON API — session cookie auth + CSRF, every body and query validated with zod
/auth/*Google sign-in and callback
/oauth/*, /.well-known/*The built-in OAuth 2.1 authorization server and its discovery metadata
/mcpThe MCP endpoint (Streamable HTTP), authorized by PAT or OAuth access token
/healthzLiveness probe used by the container healthcheck
everything elseThe React SPA — served by Vite in development, from dist/web in production

In development, @hono/vite-dev-server mounts the Hono app inside Vite for the backend prefixes above, so a single npm run dev on a single port serves both halves. That matters for OAuth: dev and production share one APP_URL, so one registered Google redirect URI covers both.

Code layout

src/
├── server/          # Hono app
│   ├── routes/      # JSON API routes
│   ├── middleware/  # session, CSRF, auth
│   ├── oauth/       # authorization server
│   ├── mcp/         # MCP server, tool registrations, resources
│   │   └── tools/   # one file per tool
│   ├── funds/       # market-agnostic fund pipeline
│   │   └── providers/  # one upstream source each
│   ├── sec/         # EDGAR client
│   ├── crypto/      # CoinGecko client
│   ├── notes/  skills/  watchlist/
│   └── db/          # Drizzle schema, migrations, seed
├── web/             # React SPA (routes, components, lib)
└── shared/          # types and zod schemas used by both halves

Two layout rules are load-bearing:

  • Nothing in src/server/funds/ may know about a specific market. The pipeline (ingest, repo, exposure, on-demand cache) is market-agnostic.
  • Everything market-specific lives in a provider. Code shapes, weight and size units, scope vocabulary and response parsing all belong to src/server/funds/providers/<id>/, behind the FundProvider interface. Adding a market means adding a provider plus a descriptor in src/shared/funds.ts — never a branch in the pipeline.

Build

npm run build runs two steps:

  1. vite build — the SPA into dist/web
  2. node scripts/build-server.mjs — esbuild bundles src/server/index.ts and src/server/funds/ingest-cli.ts into dist/server, ESM, with node_modules left external

npm start runs dist/server/index.js, which waits for PostgreSQL, applies Drizzle migrations, seeds ADMIN_EMAILS and then listens.

Further reading

The full design document lives in the repository at docs/PLAN.md, and project conventions in CLAUDE.md.

Market data is delayed and provided as-is. Not investment advice.