Imported from HereNotThere/slashtalk (
apps/server/AGENTS.md). Install upstream withnpx skills add HereNotThere/slashtalk --skill server. Copyright stays with the author.
apps/server (@slashtalk/server)
Elysia + Bun backend. Composes auth, ingest, sessions, social, user, chat, analyzers, presence, and ws plugins. See ../../ARCHITECTURE.md for the domain map; this file focuses on layout + commands + recipes.
Keep this file current. When you change build commands, scripts, conventions, layout, a plugin name, or the auth split, update this file in the same change.
Layout
src/
├── index.ts # boot: RedisBridge + createApp() + pollers + scheduler
├── app.ts # Elysia composition — add new plugins here
├── config.ts # env loader; throws at boot if required var is unset
├── db/
│ ├── index.ts # drizzle connection
│ └── schema.ts # SOURCE OF TRUTH for all tables
├── auth/
│ ├── github.ts # githubAuth + cliAuth (OAuth + exchange)
│ ├── middleware.ts # jwtAuth + apiKeyAuth
│ ├── sessions.ts # issue/rotate/revoke session+refresh tokens; cookie helpers
│ └── tokens.ts # JWT/refresh/setup/encryption helpers
├── ingest/
│ ├── routes.ts # POST /v1/ingest, GET /v1/sync-state, POST /v1/heartbeat
│ ├── classifier.ts # raw event → {kind, turnId, callId, eventId, parentId}
│ └── aggregator.ts # processEvents(): event stream → SessionUpdates
├── sessions/
│ ├── routes.ts # /api/session(s)/...
│ ├── snapshot.ts # DB row → SessionSnapshot (+ insights)
│ └── state.ts # classifySessionState()
├── social/
│ ├── routes.ts # /api/feed, /api/feed/users
│ ├── github-sync.ts # matchSessionRepo() — called from ingest + user routes
│ └── pr-poller.ts # 60s poll, publishes pr_activity
├── user/
│ ├── routes.ts # /api/me/*, incl. POST /api/me/repos (claim)
│ └── dashboard.ts # /api/users/:login/{prs,standup} — info-card user surface
├── repo/
│ └── overview.ts # /api/repos/:owner/:name/overview — info-card project surface
├── chat/
│ └── routes.ts # /api/chat/ask (stateless Q&A)
├── presence/
│ └── routes.ts # POST /v1/presence/spotify, GET /api/presence/peers; publishes to user:<id> + repo:<id>
├── web/
│ ├── shared.ts # path sanitization, MIME map, fileResponse — used by all static handlers
│ ├── routes.ts # GET /app and /app/* static serving for the installable PWA
│ └── blog-routes.ts # GET /blog and /blog/* static serving for the public Astro blog
├── landing/
│ └── routes.ts # GET / + landing assets (apps/landing/dist) for the marketing homepage
├── managed-agent-sessions/
│ └── routes.ts # PUT/GET /v1/managed-agent-sessions (apiKeyAuth)
├── mcp/
│ ├── routes.ts # root /mcp Streamable HTTP resource (MCP OAuth + device API key compatibility)
│ └── session-pool.ts # MCP HTTP session lifecycle
├── ws/
│ ├── handler.ts # WS upgrade, channel subscriptions, ping
│ └── redis-bridge.ts # ioredis pub/sub, soft-fail
├── analyzers/
│ ├── index.ts # barrel re-export consumed by src/index.ts
│ ├── scheduler.ts # tick loop, candidate selection, worker pool
│ ├── registry.ts # array of Analyzers — add yours here
│ ├── types.ts # Analyzer<T> interface
│ ├── llm.ts # callStructured() — Anthropic client + pricing
│ ├── publish.ts # session_insights_updated → repo:<id>
│ ├── names.ts # analyzer name string constants
│ ├── event-compact.ts # shared event → compact-text helpers
│ ├── summary.ts # title + description analyzer (Haiku 4.5)
│ └── rolling-summary.ts # rolling narrative analyzer (Haiku 4.5)
└── install/ # vestigial install.sh — do not extend
Scripts: scripts/gen-db-schema.ts — regenerates docs/generated/db-schema.md.
Commands
Run from apps/server/:
bun run dev # --watch src/index.ts
bun run start # one-shot
bun run typecheck # tsc --noEmit
bun run test # bun test (ingest, classifier, chat, PR poller, integration)
bun run test test/upload.test.ts # single file
bun run db:generate # drizzle-kit; after editing schema.ts
bun run db:migrate # apply pending migrations to $DATABASE_URL
bun run gen:db-schema # regenerate docs/generated/db-schema.md
bun run gen:db-schema:check # CI check: fail if db-schema.md is stale
From repo root: bun --filter @slashtalk/server <script>.
Environment
src/config.ts throws if any required var is unset.
Required: DATABASE_URL, REDIS_URL, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, JWT_SECRET, ENCRYPTION_KEY, BASE_URL.
Optional: PORT (10000), ANTHROPIC_API_KEY (analyzer scheduler disabled if unset), ANALYZER_TICK_MS (300_000), ANALYZER_MAX_SESSIONS_PER_TICK (200), ANALYZER_CONCURRENCY (5).
Auth split
/v1/*→apiKeyAuth/mcp→ explicit MCP resource-server exception; accepts MCP OAuth access tokens, plus device API keys for desktop-local proxy and legacy clients/auth/*+/api/*→jwtAuth/ws→ browsersessioncookie, else?token=JWT/API key
See core-beliefs #2. A new auth scheme gets a new plugin in auth/middleware.ts, not an overload.
Adding a route plugin
- Create
src/<area>/routes.tsexporting a factory(db, redis?) => new Elysia({ name: "<area>", prefix: "/<prefix>" }).use(jwtAuth|apiKeyAuth(...)).... nameis required — Elysia dedups plugins by name. See core-beliefs #3.- Mount in
src/app.tswith.use(yourRoutes(db, redis)). - Auth follows route prefix.
- Add a test under
test/using helpers intest/helpers.ts(mocks GitHub OAuth + DB). bun run typecheck && bun run test.
Adding an LLM analyzer
- Add a name constant in
src/analyzers/names.tsand extendAnalyzerName. - Create
src/analyzers/my-analyzer.tsexportingmyAnalyzer: Analyzer<MyOutput>withname,version,model,shouldRun(ctx),run(ctx). Model:src/analyzers/summary.ts. - Register: append to the array in
src/analyzers/registry.ts. - If the UI needs the output, extend
loadInsightsForSessions()insrc/sessions/snapshot.tsand add the field topackages/shared/src/index.ts. - Pick a model from the allowed list (core-beliefs #8):
claude-haiku-4-5-20251001(default for high-volume cheap labels),claude-sonnet-4-6,claude-opus-4-7. Update pricing inllm.tsif you introduce a new ID. - Set
shouldRunthresholds (line-seq delta + min-time) so you don't melt the API on noisy sessions. bun run test test/integration.test.tsto confirm scheduler pickup.
Adding a database column or table
Rules: core-beliefs #4. Short workflow:
- Edit
src/db/schema.ts. bun run db:generate— read the generated SQL underdrizzle/.- For rename/destructive ops, regenerate with
bunx drizzle-kit generate --custom --name=<slug>. bun run db:migrateagainst local DB.bun run gen:db-schemato refresh the agent-readable schema.- Commit schema + SQL + journal/snapshot +
db-schema.mdas ONE commit. - Never hand-edit
drizzle/meta/_journal.jsonor*_snapshot.json.
Adding a WebSocket message type
- Define the message shape in
packages/shared/src/index.ts. Discriminate bytype. - Pick a channel: per-repo broadcasts →
repo:<id>, per-user →user:<userId>. - Publish through
ws/redis-bridge.ts(no rawawait redis.publish(...)— core-beliefs #7). - Handle on the desktop in
apps/desktop/src/main/ws.ts's switch. - WS clients must ignore unknown
typefields — keep this property when adding new messages.
Adding a new event source (beyond Claude / Codex / Cursor / Pi)
- Add the source string to
SOURCESinpackages/shared/src/index.ts. - Extend
src/ingest/classifier.tsto map the source's raw events toEVENT_KINDS. - Extend
src/ingest/aggregator.tsto fold the source into session aggregates without weakening existing source behavior. - Add a test file
test/classifier-<source>.test.tsmirroringclassifier.test.ts.
Before committing
bun run typecheck
bun run test
bun run gen:db-schema:check # if you touched schema.ts
All must pass.