API-first Venture Studio OS — manages startup lifecycle from intake to portfolio.
Two changelogs, keep them in sync.
CHANGELOG.md— the technical/engineering log. Task IDs, file paths, code refs welcome. Newest-first. (No longer symlinked intofrontend/public/: task 433/D300 found nothing reads that copy, and it published the whole engineering log at/CHANGELOG.mdon every deploy.)frontend/public/CHANGELOG-user.md— the in-app Docs → "What's new" page. Plain-English, no task IDs, no file paths, no code. Write it for the people using the platform. Any user-facing change needs a line in BOTH. Do not date task entries in the technical file.
The following steps were completed when this project was imported into Replit:
- Frontend dependencies installed —
cd frontend && npm install(installs Vite, React, Tailwind, etc. intofrontend/node_modules/). JWT_SECRETadded to Replit Secrets — required; the dev backend (backend/app/api/routes/auth.py) raisesRuntimeErrorat import time if unset.- Workflows verified running:
Backend API—UV_PROJECT_ENVIRONMENT=.venv uv run uvicorn backend.app.main:app --host 127.0.0.1 --port 8000 --reload(port 8000)Start application—cd frontend && npm run dev(port 5000, proxies/api→ 8000)
postgresql-16module restored in.replit(was accidentally removed during import; dev backend uses a local SQLite file via SQLModel, but the module is kept for parity).
If you clone or fork this repl, repeat steps 1–2.
- Dev:
npm run dev(frontend) +python backend/main.py(FastAPI, dev-only). - Build:
npm run build· Deploy (prod):npm run deploy(Cloudflare Worker) - D1 migrations auto-apply on deploy:
npm run deployrunspredeploy→node scripts/migrate-d1.mjs --remote, a forward-only runner that applies only the pendingcloudflare-worker/sql/migrations/*.sql(in numeric order, deterministic on duplicate prefixes) and records each in theschema_migrationsledger. A failing migration aborts the deploy loudly (non-zero, names the file). Re-deploy with no new migrations = fast no-op. Other targets:npm run d1:migrate:local/d1:migrate:preview; audit-onlynpm run d1:audit; preview the plan withnode scripts/migrate-d1.mjs --remote --dry-run. Needs Node 22+ (sameexport PATH=…nodejs-22…/binas manual wrangler). One-time baseline: ✅ DONE (2026-07-08) — the prod ledger now holds all 149 files (idempotent ones executed, non-idempotent ones recorded-only;funnel_events+user_role_reviewPRAGMA-verified present). Deploys now auto-apply only new migrations. Note:--dry-runalways prints the plan assuming an empty ledger — to see what's really pending, run the live runner (it prints "N already applied, M pending" before touching anything) or queryschema_migrationsdirectly. - Typecheck: runs inside
npm run test:drift(tsc --noEmitincloudflare-worker/; there is no standalonenpm run typecheck) · Drift check:npm run test:drift(required pre-merge) - Required env:
JWT_SECRET,SCORING_HMAC_SECRET(≥32 bytes, hard-required in prod),AXAL_ENCRYPTION_SECRET(falls back to JWT_SECRET),VAPID_PUBLIC_KEY/VAPID_PRIVATE_KEY/VAPID_CLAIM_EMAIL(push).
| Situation | Command |
|---|---|
| Clean fast-forward | bash scripts/git-sync.sh |
Diverged from origin/main |
bash scripts/git-sync.sh --auto |
| Push only | bash scripts/git-push.sh |
git-push.sh auto-detects workflow-file commits and falls back to GITHUB_TOKEN (Replit's OAuth lacks the workflow scope until ops item (d) below is done). Full reconcile flow lives in scripts/sync-reconcile.sh.
- Frontend: React 19 + Vite 7 + Tailwind 4 + react-router 7
- Prod API: Cloudflare Worker (Hono, TypeScript) on D1
- Dev API: FastAPI on SQLite — never deployed
frontend/·cloudflare-worker/src/index.ts(route mounts) ·routes/*.ts·services/*.ts·integrations/providers/*.ts·sql/migrations/NNN_*.sql·backend/(dev FastAPI) ·scripts/check-*.mjs(CI guards).
- Prod = Worker on D1; dev = FastAPI on SQLite. Never deploy FastAPI.
- Apex routing (rewritten 2026-09-03 — the earlier text described path-scoped
axal.vc/{api,app,dashboard,…}routes over a GitHub Pages/Jekyll apex, a topology gone since 2026-09-01) — thestudioosWorker serves BOTHaxal.vcandapp.axal.vcas whole-host Workers Custom Domains (pattern = "axal.vc"/"app.axal.vc",custom_domain = true, in BOTH the top-level[[routes]]block and[[env.production.routes]]inwrangler.toml; keep them in lockstep —frontend/test/apex_route_coverage.test.mjsasserts the two tables match). Every path on either host is answered by the Worker:/api/*by Hono, everything else by the[assets]binding (directory = "./docs",not_found_handling = "single-page-application",run_worker_first = ["/api/*", "/landing/*", "/p/*", "/assets/*"]). There is no Jekyll, no GitHub Pages and no per-page route list any more: a new top-level SPA route needs nowrangler.tomlentry. Never add a path-scoped apex route (axal.vc/*,axal.vc/assets/*) — it would take those URLs away from the assets binding and break the SPA fallback, the mechanism behind the 2026-08-31 blank-page outage (entry module 404 →?__reboot=watchdog loop);cloudflare-worker/test/apex_cutover_bootstrap.test.mjsrefuses it. The flip to whole-host domains landed in1d320dda9(2026-09-01, "Remove stale documentation asset files" — a message that does not mention it), so who serves a host is read from the deploy log's "Deployed studioos triggers" lines, never from prose. One build sits behind both hosts and ships on everywrangler deploy(.github/workflows/cloudflare-worker-deploy.ymlon push tomain, ornpm run deploy); a missing/assets/*file gets a real 404 fromindex.tsrather than the SPA shell, so a stale tab reloads onto the current build, andnpm run build(scripts/build-frontend.mjs) still retains the last few builds' hashes (ledgerdocs/.asset-retention.json,ASSET_RETAIN_BUILDSdefault 3). The Cloudflare Pages mirror (studioos-2p8.pages.dev) and its workflow were retired on 2026-09-03 (DECISIONS.mdD36) after its dashboard showed "Production" rows for commits whose Worker deploy had failed; nothing ships a build anywhere but the Worker. The static security headers on SPA responses come fromdocs/_headers(built fromfrontend/public/_headers, read natively by Workers static assets — it does not apply to therun_worker_firstpaths, which getsecurityHeaders.ts), andscripts/check-spa-live.mjsasserts them live on every shell route. Canonical-host status (Phase 2 complete):APP_URL/PUBLIC_BASE_URLare nowhttps://axal.vc;OAUTH_CALLBACK_BASE_URL = "https://app.axal.vc"pins all OAuthredirect_uriregistrations toapp.axal.vcuntil provider dashboards are updated. All integration providers and calendar/LinkedIn OAuth deriveredirect_uriviacallbackBase()incloudflare-worker/src/util/url.ts. Remaining ops step: update provider redirect URI registrations → deleteOAUTH_CALLBACK_BASE_URLenv var. Edge 301 middleware inindex.ts(369-390) is written to redirectapp.axal.vc/*→axal.vc/*for non-/api/*paths — whether it fires for SPA paths the assets binding answers before the Worker runs is unverified live (see U10); full SPA-bookmark convergence (paths not handled by the worker) requires the Cloudflare bulk-redirect rule on theapp.axal.vczone — see ops item (e). Public marketing surface (Task #6 ID) — decision recorded: the SPA is the public surface. Since 2026-09-01 that is every path on the apex, so the former list of apex-routed public pages (/spinout-lab,/about,/insights,/directory,/contact,/articles) needs no route entries; what remains true:https://axal.vc/articles/{slug}is the author editor's copy-link / "View live" share URL,/aboutreusesTeamPage(Guillaume's card),/team301s to/about, and/insightsis a self-contained index over public list endpointGET /api/market-intel-public/publications(published +audience != 'internal'only) whose cards deep-link to/insights/public/:slug. Route-table changes only take effect onwrangler deploy. - API drift prevented by
npm run test:drift. - Per-bucket rate limiting + strict CSP headers.
- Theme/density via CSS-vars in
frontend/src/index.css(--app-bg/--app-surface/--app-text/--app-input-*). Tailwind 4@custom-variant dark.SettingsContexttoggles bothdata-themeand.darkon<html>. New pages needdark:variants on hardcodedbg-white/text-gray-*. - Frontend storage via
safeReadJSON/safeWriteJSON; errors viareportError.
- Clear, concise communication.
- Iterative — confirm before major architectural changes or significant refactors.
- Security-first; explicit error handling over silent fallbacks.
- API keys UI is gated behind a server-driven feature flag.
Admin docs live behind roles: ['admin'] on sections/subsections in frontend/src/pages/docs/sections/*.js. DocsLayout filters rail/body/TOC and the hash-deep-link guard by useAuth().role; lib/docs/search.js::createDocsFuse(role) filters search. Adding a new admin doc = add roles: ['admin']; no other wiring. Never link to #admin/* from non-admin surfaces. The surface moved to /help (Task #103, 2026-09-07) — HelpCenterPage (was DocsPage) mounts it, /docs redirects with hash and query preserved, and the ticket flow sits under it at /help/tickets. Caveat found while doing that: sections/admin.js is not imported by sections/index.js — 2c38e60b3 ("Remove administrative sections from user documentation", 2026-05-22) dropped it deliberately and left the file. So SECTIONS holds 13 entries, adminOnlyAnchors() returns an empty set, and AdminDocsPathGuard redirects admins to an anchor nothing renders. The role machinery above is correct and still runs; there is simply no admin section in the manifest for it to act on, and re-importing one reverses that decision rather than fixing a bug.
The full, detailed gotchas now live in documentation/architecture/GOTCHAS.md so this README stays a scannable overview. Subsections (each in documentation/architecture/GOTCHAS.md):