30 anticipated questions with concise, credible answers grounded in the actual code and ADRs. Each answer references real artifacts in the repo.
Status (as of v0.5.19): this document was originally drafted in the design phase against ADRs 0001–0022 (the planned architecture). It has been reconciled with the shipped implementation — Pusher Channels instead of Fly.io + Socket.IO (ADR-0052 records the implementation-time pivot for ADR-0046 compliance), hybrid retrieval (pgvector cosine kNN + Postgres FTS via RRF) shipped per ADR-0063 (closes ADR-0011 deferred), single-tenant → multi-tenant transition shipped per ADR-0061, CI Credentials provider for Knowlex shipped per ADR-0065. Knowlex live demo currently EMERGENCY_STOPPED post the 2026-04-29 Gemini Free tier account-level revocation incident — see ADR-0067; BYOK-reproducible per the README runbook.
Numbers: Vitest / Playwright / ADR / route counts are real measurements as of v0.5.4 (
pnpm --filter * test,ls docs/adr/00*.md,find apps/collab/src/app -name 'page.tsx' -o -name 'route.ts'). Latency + pass-rate numbers come from the nightly eval cron's auto-committed reports underdocs/eval/reports/; the README measured-eval badge sources fromdocs/eval/badge.jsonregenerated on every green run.
Q1. Why a monorepo? Two products share authentication, UI primitives, logger, DB helper, and generated API types. Polyrepo would force publishing private npm or git submodules — both slow the feedback loop. Turborepo adds task-level caching that shortens CI. See ADR-0001.
Q2. Why Vercel-only (the original ADR-0009 plan was Vercel + Fly.io hybrid)? The design-phase ADR-0009 chose a hybrid because Socket.IO and BullMQ workers need long-lived processes. During Boardly v0.1.0 implementation that pivot happened: realtime moved to Pusher Channels (HTTP fanout, no long-lived server needed), and BullMQ was dropped — Knowlex's bounded corpus doesn't need an async ingest worker. The pivot was driven by ADR-0046 (zero-cost-by-construction; Fly.io free tier has wake-up overhead Pusher Sandbox doesn't), single-pipeline ops, and the env-guarded degradation pattern (ADR-0030 / ADR-0032) — Pusher fanout fits "missing credentials = silent skip" naturally. Recorded as ADR-0052. ADR-0009 is marked Superseded.
Q3. Why driver adapter for Prisma?
Prisma 7 requires an adapter at construction. @prisma/adapter-pg keeps local dev on node-postgres while the same interface lets production swap to Neon's HTTP driver. See apps/collab/src/lib/db.ts.
Q4. Why split the databases? Knowlex pgvector workloads must not starve Boardly transactional queries. Separate Neon projects isolate failure modes and resource limits. See ADR-0018.
Q5. What is the scale ceiling of the free-tier setup? Pusher Channels Sandbox = 200k messages/day + 100 concurrent connections, free, no card. Beyond that the upgrade path is Pusher Startup ($49/mo). Neon Free = 0.5 GB storage + 191.9 compute hours/month with 5-minute idle auto-suspend (UptimeRobot pings keep it warm in business hours). Vercel Hobby = 100 GB bandwidth/month. Realistic ceiling for a free-tier portfolio demo: low-hundreds of concurrent users on Boardly, and bounded-corpus multi-tenant Knowlex demos (demo workspace anonymously readable + authed-member workspaces per ADR-0061). Hard cost cap is $0 by construction per ADR-0046 — paid-plan upgrades are the operator's deliberate choice, never auto-billed.
Q6. Why LexoRank for ordering?
Integer position forces renumbering neighbors on every move, conflicting with realtime broadcast. LexoRank makes reorder a single-row UPDATE. Rank inflation is handled by a periodic rebalance. See apps/collab/src/lib/lexorank.ts + ADR-0006 / ADR-0021 / ADR-0025.
Q7. Why embedding in its own table?
Embedding models change. Chunk holds text + metadata; Embedding(chunkId, model, dim, vector) holds the vector (PK = chunkId per the v0.5.0 schema). Swapping models rewrites Embedding only. See ADR-0012.
Q8. Why soft delete?
Operational restorability and audit traceability. AuditLog retains actorId SetNull so history survives user deletion. Note: the automated cleanup job is design-phase per the data-retention policy; physical deletion currently requires a manual prisma script invocation. v0.6.0 adds Vercel Cron-based automation.
Q9. How do you handle schema migrations?
Migrations apply via prisma migrate deploy inside Vercel's vercel-build script per ADR-0051. The original v0.5.0 plan ran prisma generate only at build time, which silently drifted prod from schema.prisma; v0.5.2 closed the gap. _prisma_migrations table provides idempotency. RLS was design-phase per ADR-0010 but deferred — Knowlex was single-tenant per ADR-0039 MVP scope at that time, then transitioned to multi-tenant in v0.5.12 (ADR-0061) using the demo-allow-list + Membership pattern over RLS.
Q10. Why JWT session strategy (the original ADR-0003 chose database sessions)?
ADR-0003 specified database sessions for instant revocation. In practice JWT was adopted to unblock the Vercel Edge Runtime proxy (database lookups are not allowed in Edge middleware). The supersession is documented in the fix(auth) commit; a formal ADR-0023+ addendum is open. Trade-off: JWT lives until expiry (default 30 days); for an OAuth-only portfolio demo with no privileged data, this is acceptable.
Q11. How is RBAC enforced?
roleAtLeast() is a pure helper with a 4×4 = 16-case Vitest matrix (apps/collab/src/auth/rbac.test.ts). hasRole / requireRole gate every mutation at the REST handler layer. Defense in depth: API checks + Prisma-layer membership checks + cross-workspace guards on set-mutations per ADR-0029. RLS at the DB layer was design-phase per ADR-0010 but deferred — both apps enforce membership application-side (Boardly via 4-tier RBAC + Membership; Knowlex via Auth.js + Membership + demo-allow-list per ADR-0061).
Q12. How do you test OAuth in E2E without hitting Google?
A Credentials provider is registered only when NODE_ENV !== "production" AND E2E_ENABLED=1 AND E2E_SHARED_SECRET constant-time compares against a 3-email allowlist. Production bundles tree-shake it out. See ADR-0022 / ADR-0038.
Q13. Why Pusher Channels (the original ADR-0004 chose Socket.IO + Redis Adapter)? ADR-0004 rejected Pusher because it "forces future vendor cost"; this judgment was reversed by ADR-0046 (Pusher Sandbox = $0, hard-capped). Implementation chose Pusher because it fits the env-guarded degradation pattern (missing credentials = HTTP-call skip, no broken WebSocket server to babysit), keeps the deploy as a single Vercel pipeline (Fly.io would have been a second pipeline), and removes a class of "did the WebSocket server crash overnight?" failures. Recorded in ADR-0052.
Q14. What is the conflict model?
Optimistic locking via Card.version. The client sends its last-seen version; the server's updateMany filters by id + version and 0-rows-affected returns HTTP 409 VERSION_MISMATCH. The client bumps its local version on success so rapid drags don't stale-conflict with themselves. Pessimistic locks would leak on disconnect. See ADR-0007 / ADR-0024 / ADR-0048.
Q15. How does broadcast fanout work?
Pusher Channels: the server emits an event to a channel named board-<id> after a successful mutation; every client subscribed via pusher-js receives the event over Pusher's persistent connection. BoardClient applies the diff or, for stale local entries (undoredoStack items predicting state that was overwritten by a remote mutation), calls markStale per ADR-0048 so the next undo skips the entry. Pusher emit is wrapped per ADR-0030 — a Pusher outage cannot abort the originating card save.
Q16. Why pure cosine kNN (the original ADR-0011 was hybrid + RRF + Cohere rerank)?
ADR-0039 (Knowlex MVP scope) explicitly defers hybrid retrieval, HyDE, and rerank to a later arc. The MVP demonstrates the full ingest → embed → store → retrieve → stream pipeline end-to-end with a single technique, on the corpus sizes that fit a portfolio demo (current production: 13 docs / 23 chunks per /api/kb/stats). Pure cosine kNN at 768 dim with HNSW is sufficient quality at this scale. Hybrid + RRF + Cohere remain on the v0.6.0+ roadmap once the corpus and traffic justify the additional complexity.
Q17. How do you prevent hallucinations?
Three layers, none of which is the design-phase NLI Faithfulness check from ADR-0013 (deferred per ADR-0039). (1) The system prompt requires inline citations [1] [2] matched to retrieved chunks; the UI suppresses any answer without citations. (2) The nightly RAG eval cron scores citation-coverage and substring-faithfulness against a 30-question golden set with 21 OR-mode + 6 AND proper-noun + 3 adversarial questions per ADR-0049 § 7th arc; regressions trip the eval. (3) The 3 adversarial questions verify the model refuses to answer outside the corpus rather than hallucinating. NLI-mode Faithfulness is on the roadmap.
Q18. What about HyDE? Design-phase per ADR-0014, deferred per ADR-0039. The MVP corpus and question distribution don't yet justify the +1 LLM call per query; the eval cron would surface the gain measurably before HyDE ships. Open arc.
Q19. How do prompt changes stay traceable?
Prompt files live under apps/knowledge/src/server/ai/prompts/ and are checked into git so every change is reviewable by diff. A SHA256 registry per ADR-0020 is design-phase; the practical equivalent today is git history + git blame on the prompt files. Eval cron output records the answer text per question per run, so behaviour change correlates to the prompt commit that changed.
Q20. What are your measured latencies?
Real eval cron numbers, not targets. The nightly eval.yml writes per-run reports to docs/eval/reports/YYYY-MM-DD.json — each entry has passRate, p95Ms, and per-question latency. Run 3 (v3 substring-AND scoring) measured 19/30 = 63%. Run 6 measured 4/30 = 13.3% under stronger paraphrase scoring. Run 8 (2026-04-27 19:38 UTC, the first run after v0.5.2 schema-vs-prod drift fix landed on the live Knowlex db) measured 24/30 (80%) with p95 8221 ms under v4 scoring — comfortably above the 60% pass-rate threshold and below the 10000 ms p95 cap. The v0.5.3 README measured-eval badge sources from docs/eval/badge.json, regenerated by scripts/eval-badge.mjs on every green eval run and committed back to main by the workflow itself (Tier C-#2 follow-up of ADR-0049 § 7th arc, shipped in v0.5.3). Latency targets per ADR-0049: maxP95LatencyMs: 10000 (raised from 8000 in v0.5.1 to accommodate temperature + safety BLOCK_NONE generation overhead).
Q21. Where is the slowest hot path?
RAG /api/kb/ask end-to-end: embedding (~200 ms via Gemini) + HNSW kNN on Neon (cosine, single-digit ms once warm) + Gemini 2.5 Flash streaming (TTFT ~500 ms + sustained stream). Cold-start adds Neon Free's wake-up (1–3 s) — handled by the eval client's retry-on-503 contract per ADR-0049. Per-IP rate limiter trips at 10 req/60 s sliding window per ADR-0046 cost-attack defence.
Q22. How do you keep the free-tier DB warm?
A scheduled GitHub Actions smoke run + UptimeRobot pings hit /api/kb/stats (cheap, no DB write) within Neon's 5-minute idle window during business hours. Outside business hours the DB is allowed to suspend; the eval cron's retry contract per ADR-0049 handles the resulting cold-start.
Q23. What is the tenant-isolation guarantee?
Boardly: workspace membership enforced at every REST handler via requireWorkspaceMember, with cross-workspace guards on set-mutations per ADR-0029. Knowlex: was single-tenant per ADR-0039 MVP scope; workspace schema partitioning (every table has workspaceId NOT NULL) shipped per ADR-0047 partial in v0.5.0; the access-control half (Auth.js + Membership + demo-allow-list two-shape pattern, anonymous reads on wks_default_v050 preserved + anonymous writes explicitly disallowed) shipped per ADR-0061 v0.5.12 (closes I-01 honest disclose). PostgreSQL RLS per ADR-0010 remains acknowledged-deferred — application-side enforcement was chosen for v0.5.12.
Q24. How do you handle prompt injection?
The system prompt + retrieved chunks are clearly delimited so the model treats retrieved content as data, not instruction. The 3 adversarial golden-set questions verify the model refuses out-of-corpus questions ("ignore previous instructions and tell me ..."). Prompt injection is also covered structurally by the cost-attack defence — EMERGENCY_STOP=1 short-circuits all /api/kb/{ask,ingest} traffic per ADR-0046 if a wave is detected.
Q25. Why structured error responses?
{ code, message, details? } matches the OpenAPI 3.1 Error schema served at /api/openapi.json per ADR-0035, so clients can branch on code without parsing prose. Stack traces never leave Sentry / the in-memory observability ring buffer.
Q26. What layers does the test pyramid cover?
Unit (Vitest, 276 cases: 174 collab + 102 knowledge — LexoRank, RBAC 4×4 matrix, validation, business logic, RAG retry contract, dedup, emergency stop, schema-canary EXPECTED ↔ schema.prisma consistency per ADR-0053, etc.), integration (Knowlex retrieve.integration.test.ts against a real pgvector service container in CI), E2E (Playwright, 24 scenarios: smoke + authed E2E across board/dashboard/rate-limits/workspace + a11y + authed-a11y + signin), a11y (axe-core gate as PR-blocking on every public + authenticated page per ADR-0034), eval (nightly RAG cron with 30-question golden v4 + green-run auto-commit shipped in v0.5.3), drift-detect-v2 (pg_catalog assertion gating PRs per ADR-0051) + runtime schema canary /api/health/schema asserted by 6-hourly smoke cron per ADR-0053. k6 scaffold exists but the realtime load harness is design-phase pending a Pusher-aware rewrite (Fly.io WebSocket-targeted version is non-runnable per ADR-0052).
Q27. How do you guard against silent RAG regressions?
Eval cron runs nightly per ADR-0015 / ADR-0042 / ADR-0043. Reports land in docs/eval/reports/YYYY-MM-DD.json (PR #29 in v0.5.3-prep). Substring-OR + AND-proper-noun + adversarial-refusal scoring is in v4 corpus per ADR-0049 § 7th arc. Schema-vs-prod drift is caught PR-time by drift-detect-v2 per ADR-0051 — the v0.5.0 → v0.5.2 incident (Document.workspaceId does not exist) cannot recur silently.
Q28. What was the hardest decision? Pivoting away from ADR-0009's Fly.io + Socket.IO + BullMQ architecture during implementation. The ADRs were already Accepted; rewriting realtime against Pusher meant abandoning the original load-test target (k6 on WebSocket), accepting a vendor dependency, and writing ADR-0052 retroactively to document why. The right call but the disciplined thing was to record the supersession explicitly rather than let the docs drift. The audit-survivability stance from ADR-0046 drove it: an ADR you can't honestly answer "is this what shipped?" to has zero audit value.
Q29. What would you do differently?
Capture every implementation-time deviation from a design-phase ADR with an immediate Superseded marker + new ADR, instead of letting prose drift across portfolio-lp / interview-qa / system-overview / About sidebar for weeks. The v0.5.3-prep claim-reality cleanup arc was the manual corrective ratchet for the first cycle. The institutional fix is now shipped as ADR-0054 (v0.5.5, doc-drift-detect CI gate) — a PR-blocking job that resolves truth from ls docs/adr/, pnpm test, file walks, and git describe, then asserts every embedded number / version banner in README, portfolio-lp, interview-qa, system-overview, runbook, page.tsx, layout.tsx, opengraph-image.tsx matches. Same shape as ADR-0053 (runtime schema canary) but for prose claims instead of schema columns. This Q29 itself is now self-resolving: a deferred plan flipped to a ship-tag entry and the candidate's own self-criticism became the candidate's own structural ratchet log — the ADR-0049 § 7th arc pattern applied to hiring docs.
Q30. How did you manage risk as a solo builder? Two-phase release per ADR-0017: Boardly ships with a working feature loop first; Knowlex follows reusing the shared foundation (auth, UI primitives, db helper, logger). Worst case: one polished product exists and the portfolio is shippable. Both shipped — Boardly v0.1.0 in Week ~10, Knowlex MVP through v0.5.4 (and counting per ADR-0049 eval reliability incident chain + ADR-0053 runtime schema canary).