Skip to content
OpenSmartRoute

osr-platform

.claude/skills/osr-platform/SKILL.md

Work on the hosted OpenSmartRoute platform under platform/ - the FastAPI platform API (osr_platform, accounts, API keys, plans, metered /api/v1/route, OpenAI-compatible /v1 proxy, admin, editions, the committed openapi.json snapshot) and the Next.js 16 web app (landing, models catalogue, rankings, playground, pricing, dashboard, route-handler proxies). Add or change a page of the end-user documentation site (/docs) rendered from the repository Markdown via sync-content.mjs, lib/docs/catalogue.ts and lib/docs/render.ts, the generated REST API reference (/docs/api) or the version menu; publish a new docs file or skill; fix the docs build, search index or sitemap. Build and deploy both containers to Azure Container Apps with azd (azure.yaml, infra/, the platform GitHub workflow). Use when editing platform/api, platform/web, the documentation site, azure.yaml or infra/.

Package
.claude/skills/osr-platform
Compatibility
OpenSmartRoute repository, Python >= 3.11, Node >= 22, Azure Developer CLI
License
Apache-2.0
Domains
coding general
Quality prior
0.85
Tags
opensmartroute platform nextjs fastapi docs azure azd

Install by copying .claude/skills/osr-platform/ into the skills folder of your coding assistant. To load every package as a routing target: osr --skills .claude/skills route "..." --plan.

Two deployables under platform/, one repository - deployed as three services: the API (osr-api), the public website (osr-web, opensmartroute.ai/, /docs) and the platform (osr-platform, everything under opensmartroute.ai/platform/...; the web image with the website forwarding /platform to it). Read platform/README.md first (developer documentation, repository only); the end-user guide is docs/PLATFORM.md, published at /docs/PLATFORM.

DirectoryServiceStackChecks
platform/apiosr-platform-apiFastAPI, SQLite, osr_platform packageruff format --check platform; ruff check platform; mypy platform/api/osr_platform; pytest platform/api/tests -q
platform/webosr-platform-webNext.js 16 App Router, React 19, Tailwind 4npm run check && npm run build (in platform/web; check = eslint + tsc + vitest run over tests/*.test.ts), then npm run e2e (Playwright: customer journey + operator console against the standalone build and a throw-away API bootstrapped with the OPERATOR from e2e/global-setup.ts; OSR_E2E_WEB / OSR_E2E_API / OSR_E2E_OPERATOR_USERNAME / _PASSWORD point it at servers you already run, e.g. the dev stack with admin / admin-dev-only)

Run the whole platform in Docker with live updates: docker compose up --watch (root compose.yaml, project opensmartroute-dev; dev images platform/api/Dockerfile.dev and platform/web/Dockerfile.dev, context = repository root with their own Dockerfile.dev.dockerignore; --profile llm adds Ollama wired through deploy/compose/providers.ollama.yaml; knobs OSR_API_PORT, OSR_WEB_PORT, OSR_OLLAMA_PORT, OSR_OLLAMA_MODEL). Compose Watch syncs saves into the containers - new source folders or content sources need a develop.watch entry there. The self-hosted production stack (PostgreSQL, Redis, Kafka) is platform/docker-compose.yml. Or without Docker: $env:PYTHONPATH="src;platform/api"; python -m osr_platform.app (env OSR_PLATFORM_*, port 8080) and OSR_API_URL=http://localhost:8080 npm run dev in platform/web (port 3000). The browser only talks to the web origin: src/app/api/[[...path]], v1/[[...path]] and openapi.json route handlers stream to OSR_API_URL. A new API response header must be added to FORWARD_RESPONSE_HEADERS in src/lib/api/proxy.ts or the browser never sees it.

Platform API (platform/api/osr_platform)#

  • app.py builds the FastAPI app (create_app(settings)), settings.py maps OSR_PLATFORM_<FIELD>; api.py public/account routes, proxy.py OpenAI-compatible /v1, admin.py, billing.py, docs.py (JSON mirror of the Markdown at /api/v1/docs; the web site does not use it).
  • Two editions: OSR_PLATFORM_EDITION=community (core Router) or enterprise (RouterBuilder with auto-learning, health, guard, metrics, hash-chained audit; enables tenants, stats, audit). Both are built in editions.py behind Engine (controls(), health_snapshot(), observability_snapshot()); the guard with PII redaction and MetricsTelemetry run in both.
  • Observability (observability.py): RequestContextMiddleware (echo/generate X-Request-Id, Server-Timing, JSON access log osr.platform.access, HttpMetrics per route template), /healthz, /readyz (readiness() checks database, catalogue, router, audit file), /metrics (prometheus_text; OSR_PLATFORM_METRICS_PUBLIC=false gates it behind X-Admin-Token). Public catalogue GETs return through api.cacheable(request, payload, max_age_s) (ETag, Cache-Control, 304 on If-None-Match; OSR_PLATFORM_HTTP_CACHE_MAX_AGE_S). OSR_PLATFORM_ROUTE_CACHE_TTL_S adds CacheMiddleware; OSR_PLATFORM_RETENTION_DAYS drives db.purge_usage from a background sweep in app.py.
  • Governance (api.py): POLICY_KEYS = tenant keys + daily_budget_usd / monthly_budget_usd; validate_policy(config, registry) (400 on unknown keys, unknown targets, allow/deny overlap) is used by both PUT /tenants and PUT /policy; apply_policy(constraints, cfg) merges (fill boundaries, stricter caps, union deny, intersect allow). metered() calls check_budget only when meta["spends"] (execute / chat) and records tenant on usage rows (db.spend, db.spend_by_tenant); exhaustion = 429 + Retry-After + X-Budget-Limit|Used|Period. GET /governance -> PlatformAPI.governance(p). Web pages app/platform/dashboard/governance/page.tsx (all editions) and app/platform/dashboard/health/page.tsx (enterprise, stats). New response headers must be added to EXPOSED_HEADERS in app.py and FORWARD_RESPONSE_HEADERS.
  • Traces and events (api.py): event_buffer() finds the tracer's MemorySink; trace_payload(p, rid) (GET /trace/{request_id}, 404 unless db.usage_by_request(account, rid) finds the row - always account-scope trace reads), events_payload(...) (GET /events, joined to the account through db.request_ids(account, since); request-less events only with the stats feature), status_payload() (public GET /status, 503 when readiness() fails), mark_traced(items) on /activity, trace_id_of(rid) -> trace_id + X-OSR-Trace-Id on /route. Feedback is persisted per request (db.record_feedback / db.feedback_for, table feedback); audit_records(p, rid) scans the audit file for the request (plans with audit). Web: components/observability/ trace-view.tsx (TraceView, TraceLoader, stageTone, summarise) used by app/platform/dashboard/events/ page.tsx, the activity drawer and the playground DecisionPanel; system-status.tsx (useStatus, StatusPill, SystemStatusCard). Event attributes never carry prompt text - keep it that way.
  • Learning (editions.py): PlatformSettings.slm -> RouterSLM.from_file + builder.with_strategy(SLMStrategy); PlatformSettings.autopilot -> _build_autopilot (mirror of cli._build_autopilot: SelfImprover on engine.core.feedback / .memory, Autopilot(tracer=engine.tracer) so cycles land in the platform's tracer, engine.core.observers.append(pilot.observe); with_router_options(remember_requests=...) keeps prompts in memory for feedback rows). A promoted model is saved to settings.autopilot_dir / "slm.json" and preferred over the mounted file on restart. Engine.learning_snapshot() (GET /api/v1/learning, any plan), Engine.feedback_snapshot() (NaN -> None via _finite), slm_info(model), Engine.start()/stop() wired as FastAPI startup/shutdown handlers in app.py. Admin POST /admin/autopilot/cycle -> pilot.trigger("api"). Web: app/platform/dashboard/learning/page.tsx (nav icon brain), types LearningSnapshot, SlmInfo, AutopilotStatus, ImprovementReport. SDK side: EnterpriseRouter._bookkeep runs Router.observers; SelfImprover emits learn.improve / learn.promote. Tests platform/api/tests/test_learning.py.
  • Marketplace import (harvest.py, docs/MARKETPLACE.md "Imported catalogue"): SkillsShSource (skills.sh sitemaps -> every SKILL.md per repo through GitHub.tree/raw), GitHubSearchSource (code search, token required, size buckets <= 1000 hits), MCPRegistrySource (stream ?version=latest, active only -> tool rows). listing_for(h) builds the OCM manifest (metadata.source = {provider, repository, path, ref, url}, slug owner-repo-skill / mcp-<name>, <= 63 chars + hash), import_rows(store, rows, refresh=) writes in one transaction per batch, Harvester(work_dir) keeps a resumable JSONL cache per source. Admin GET|POST /api/v1/admin/registry/import (max 500 items) and scripts/harvest_skills.py collect|stats|publish drive it. RegistryStore.facets() is cached (FACETS_TTL_S, invalidate() on write); sources() counts listings per provider. Seeded manifests have metadata.source as a string - always isinstance guard. Tests swap the transport: monkeypatch.setattr(harvest, "fetch", fake). Never git add the work dirs (.osr-harvest*/). Whole-catalogue publishes go OFFLINE (publish --data-dir, stop the API, replace platform.sqlite3 on the Azure Files share, start it): sustained bulk POSTs over SMB corrupted the listings btree once ("database disk image is malformed"). The endpoint is for incremental batches. Keeping current: MarketplaceRefresher (harvest.py) runs in the process owning marketplace when OSR_PLATFORM_MARKETPLACE_REFRESH_S > 0 (Bicep param marketplaceRefreshS, weekly) - cache under <data>/harvest/, existing_slugs() filter, batches of 100; POST /admin/registry/import/refresh = run now. Big catalogues need the covering indexes (ix_listings_browse, ix_listings_popular) BUILT OFFLINE and the file swapped: a cold SELECT tags FROM listings over 100k wide rows on Azure Files took 14 minutes.
  • Signup POST /api/v1/signup {email,name[,password]} (5/IP/day). Route body: text, objective (Objective field names quality/cost/latency), constraints, kinds, plan (Pro-gated), execute.
  • Operator console (/platform/admin, api.admin_router()): operators (db.operators, scrypt hashes via auth.hash_password / verify_password, osr_op_ sessions 12 h, roles operator / superadmin) sign in at POST /admin/auth/login; the self.admin dependency yields an OperatorPrincipal from a Bearer operator token or X-Admin-Token (counts as superadmin) - keep every /api/v1/admin/* route on it, call require_superadmin for operator management and audit(op, "<noun>.<verb>", ...) for mutations (an admin event). bootstrap_operator() runs at start-up from OSR_PLATFORM_ADMIN_USERNAME/_PASSWORD. Users: users.password_hash, POST /auth/password/login, POST|DELETE /auth/password, settings.password_login. Web: lib/auth/operator.tsx (OperatorProvider, useOperator, useAdminApi; token in localStorage osr.platform.operatorToken), components/admin/admin-shell.tsx, pages under app/platform/admin/(console)/, app/platform/admin/login; robots.ts disallows /platform/admin.
  • Shared state (cluster.py, db.py backends): Database(path_or_url) picks SqliteBackend or PostgresBackend (to_postgres() rewrites ? -> %s, INSERT OR IGNORE, REAL -> DOUBLE PRECISION, AUTOINCREMENT -> identity; qualify columns in ON CONFLICT ... DO UPDATE SET x=table.x+1). Run the suite on PostgreSQL with OSR_TEST_DATABASE_URL (one schema per test). make_limiter -> RedisRateLimiter (Lua sliding window) when OSR_PLATFORM_REDIS_URL; make_event_bus -> KafkaEventBus when OSR_PLATFORM_KAFKA_BOOTSTRAP (api.emit(topic, payload, key); topics usage, feedback, admin; MemoryEventBus in tests via c.app.state.api.bus = ...). /readyz reports redis, events, database_dialect without failing readiness. platform/docker-compose.yml = production stack (PostgreSQL, Redis, KRaft Kafka, API, web); infra/ clusterMode adds osr-postgres / osr-redis / osr-kafka container apps (internal TCP ingress, one share each, nobrl mounts) and multi-replica osr-api. Kafka image: CONTROLLER://localhost:9093 (0.0.0.0 fails validation), CLUSTER_ID must be 22 base64url chars; kafka-python bootstrap_connected() goes false after metadata - use _metadata.brokers() for health.
  • SLM service (slm_service.py, entry osr-platform-slm): SLMWorker (cold-starts an untrained bundle to autopilot_dir/slm.json, ingest(topic, payload) for training {request_id,text} into RequestMemory and feedback into a FeedbackStore, cycle() = SelfImprover.cycle, Kafka consumer group osr-slm), FastAPI /healthz /model /predict /reports /cycle on settings.slm_port (8090). API side: settings.slm_url, training_events (api.note_training next to note_decision in /route and /v1 chat), slm_reload_s (api.maybe_reload_slm() swaps core/calibrator/targets/meta in place from the promoted file's mtime); build_engine serves the promoted file when slm_url is set even without OSR_PLATFORM_SLM. Admin proxies /api/v1/admin/slm* use urllib (404 without slm_url, 503 unreachable). Web app/platform/admin/(console)/slm/page.tsx. Tests test_slm_service.py (in-process TestClient + a stdlib HTTP relay for the proxy).
  • Domain services (services.py): SERVICES specs (name, gateway prefixes, entry osr-platform-<name>), ports in settings.SERVICE_PORTS (docs 8091 ... admin 8098, mcp 8099, openai 8100, routing 8101, slm 8090), create_service_app(name) = PlatformContext.routers(name) + /healthz /readyz /metrics; the API includes gateway_router for a domain when OSR_PLATFORM_<NAME>_URL is set (X-OSR-Service on the answer, text/event-stream relayed chunk by chunk through forward_async). Include order in create_platform_app: rankings (/stats/public) before routing (/stats), routing (/admin/autopilot) + marketplace + providers before the admin catch-all, onboarding before accounts (/auth). ctx.owns(name) decides background work (owner of routing runs the autopilot and attaches the telemetry store). Adding a service = SERVICE_PORTS + SERVICES + main_<name> + pyproject script + routers() + loop tuple + both compose files + bicep domainServices/domainServiceEnv + platform/README table + test_services.py.
  • Notifications (notifications.py): NotificationStore (tables notification_channels, notifications = one episode per alert id with state firing/resolved and read_at, notification_deliveries), Notifier(api, interval_s) = daemon loop started by the api role (PlatformContext.start), leader through SET osr:alerts:leader NX EX in Redis; tick() retries last round's failures, re-evaluates api.alerts_payload(p) for every active workspace and alerts.deployment_alerts(...) for the deployment, _reconcile opens/refreshes/resolves episodes, emits Kafka topic alerts, delivers firing / escalated / resolved through send(ch, n, event, context) (email via api.mail, webhook JSON + X-OSR-Event, X-OSR-Delivery, X-OSR-Signature = sign(secret, body, ts), Slack Block Kit, Teams MessageCard; https only, _post_json never raises). notifications_router (workspace, accounts domain, tag account) and admin_notifications_router (deployment scope, admin domain) - add new alert rule names to the rules list in each router. Settings alerts_interval_s (OSR_PLATFORM_ALERTS_INTERVAL_S; tests pass 0.0 and call notifier.tick(force=True)). Web: components/notifications/notifications-center.tsx (shared by app/platform/dashboard/notifications/page.tsx with useApi and app/platform/admin/(console)/notifications/page.tsx with useAdminApi), notifications-bell.tsx in the dashboard header. Tests test_notifications.py (a stdlib Sink HTTP server receives the webhooks; header names arrive title-cased - compare lower-cased).
  • Console extras (admin_console.py): admin_console_router (admin domain) serves /analytics (SQL grouped by the portable usage.day column; signups bucketed in Python), /activity (cross-workspace request log, cursor before), /lookup?q=, /settings (settings_payload: dataclass fields -> OSR_PLATFORM_<NAME>, _mask / _scrub hide secret-looking names and nested client_secret / api_key keys - add new secret markers to _SECRET_MARKERS, new groups to _GROUPS), /retention + /retention/purge; admin_console_local_router serves /health from the routing process (register it with the other local admin routers before the gateway). Web: app/platform/admin/(console)/{analytics,health,activity,leads,plans,marketplace,settings,search}/page.tsx; ADMIN_NAV in components/admin/admin-shell.tsx (also the header search box -> /platform/admin/search?q=). Tests platform/api/tests/test_admin_extras.py.
  • CLI sign-in (osr login) is the RFC 8628 device grant in auth_router(): POST /auth/device/code (anonymous, device:{ip} limiter 10/min + 30/h, returns user_code XXXX-XXXX from auth.generate_user_code, verification_uri = <web_url>/platform/cli/authorize), GET /auth/device/{user_code} and POST /auth/device/approve|deny (signed in; approve needs admin and a free key slot; db.decide_device_code), POST /auth/device/token (authorization_pending / slow_down / access_denied / expired_token as HTTP 400 JSON; success mints a key via create_key and consume_device_code). Table device_codes stores only the SHA-256 of the device code (DEVICE_CODE_TTL_S 15 min). Web page: app/platform/(auth)/cli/authorize/page.tsx + components/auth/device-authorize.tsx. SDK side lives in src/opensmartroute/credentials.py (device_login, CredentialStore). Tests: tests/test_device_flow.py.
  • The installer scripts install.sh / install.ps1 at the repository root are copied by sync-content.mjs and served verbatim by app/install.sh/route.ts and app/install.ps1/route.ts (lib/installers.ts); install one-liners for the site come from install in lib/config/site.ts.
  • Chat proxy metadata key is opensmartroute; models[] = candidate list with fallbacks; stream = SSE.
  • Tests use platform/api/tests/conftest.py fixtures (temp data dir, examples/targets.yaml). Smoke a deployment with python platform/api/scripts/smoke.py <base-url>.
  • platform/api/openapi.json is a committed snapshot of the enterprise-edition OpenAPI document (OPENAPI_TAGS, SECURITY_SCHEMES and openapi_snapshot() in app.py). After changing any route, model or docstring run python -X utf8 platform/api/scripts/export_openapi.py; test_openapi_snapshot_is_current and the CI --check step fail on drift. Tag descriptions become the intro of each /docs/api/<tag> page; the admin and docs tags and every /api/v1/admin/* path (HIDDEN_TAGS, HIDDEN_PATH in lib/docs/openapi.ts) are hidden from the site; give a new tag a title in TAG_TITLES.

Web app (platform/web)#

  • Route groups: (marketing) (landing, marketplace, models, rankings, estimate, pricing, roi, compare, plus the prose pages support, terms, privacy built on components/marketing/prose-page.tsx), (docs), and everything signed-in under platform/ - the platform half of the site, served at /platform/...: platform/(auth) (login, signup, password recovery, auth/callback, invite/[token], cli/authorize), platform/dashboard/, platform/admin/ (operator console) and platform/(marketing) (playground, marketplace/publish - public-site chrome, the layout re-exports (marketing)/layout.tsx). /platform itself redirects to the dashboard. Write page paths with the prefix (/platform/dashboard/keys, /platform/login); src/lib/routes.ts has PLATFORM_PREFIX, platformPath() and LEGACY_PLATFORM_PATH (the pre-split paths that src/proxy.ts 308-redirects). In production the website and the platform are two container apps running the same image: osr-web has OSR_WEB_PLATFORM_URL and forwards /platform requests to osr-platform (NextResponse.rewrite); a single process (next dev, Compose) serves both. Site-wide names and nav live in src/lib/config/site.ts: headerNav (groups render as navigation-menu panels on desktop and sections in the mobile sheet, links are top-level; icon names resolve in components/layout/nav-icons.tsx), footerNav (Product / Developers / Resources / Company groups) and legalNav (footer bottom bar). Every layout renders <main id="main"> - the header's skip link targets it. New public pages also go into SearchCommand's PAGES, app/sitemap.ts and the container smoke step of .github/workflows/platform.yml. Server-side API reads in src/lib/api/server.ts (serverApi, memoised); client calls in src/lib/api/client.ts.
  • UI toolkit: src/components/ui is the shadcn/ui new-york kit (components.json: rsc, slate, CSS variables, lucide icons; aliases utils -> @/lib/utils/format, ui -> @/components/ui, hooks -> @/hooks). Add a component with npx shadcn@latest add <name>, then fix the generator output: it writes import { cn } from "cn" (must be @/lib/utils/format), it may scaffold next-themes/dark-mode code (remove it - the dark surface is the theme-dark class, not a theme provider) and its Math.random/setState-in-effect patterns fail the React Compiler lint. Radix comes from the radix-ui umbrella package only - never add @radix-ui/react-*. Brand tokens map onto the kit variables in globals.css (--background, --muted-foreground, ...): use text-muted-foreground, bg-muted, border-input, ring-ring rather than legacy text-muted. Legacy-named wrappers kept for call sites: Badge/Card/Alert/EmptyState/ StatCard in ui/card.tsx, Button/ButtonLink (external) in ui/button.tsx, Input/ Textarea/SearchInput/Select/Switch/Field in ui/field.tsx, Avatar/Progress/ Separator in ui/primitives.tsx, DataTable + Table* (TableRow interactive, TableCell numeric) in ui/table.tsx, FilterChip in ui/toggle.tsx, SegmentedControl in ui/tabs.tsx. Select renders a sizing wrapper around the native <select>: width/height/text-size classes go on className (e.g. className="h-8 w-52 text-xs"), never padding. No hand-rolled <table>, <select>, <details>, chip buttons or dropdowns in pages - reach for the kit component.
  • Design system (light, editorial; see the website sections of docs/BRAND.md): fonts come from next/font in app/layout.tsx - Inter (--font-sans, also font-display), Instrument Serif (--font-serif, weight 400 only, never font-bold), Geist Mono (--font-mono), Quicksand only for the wordmark (font-wordmark). Utilities in globals.css: heading-serif for display titles with text-display-sm/md/lg/xl, eyebrow (mono uppercase muted label), container-x (76rem). Marketing sections are <section className="border-b border-line py-20 sm:py-28"> with SectionHeading (eyebrow, serif title with an <em className="text-slate-ink"> second clause, description; left-aligned by default, as="h1" for the page title, size sm/md/lg) and divide-y divide-line ledgers with font-mono text-sm text-muted-foreground tabular-nums numerals - not icon cards. Alternate bands use bg-snow. Buttons: primary/default = ink fill, accent = brand-blue (rare), secondary = mist, outline, inverse on dark panes. Inline links are text-ink underline decoration-ink/30 underline-offset-4 hover:decoration-ink, highlighted cards use border-ink (no coloured rings), hover borders hover:border-ink/30, shadows hairline (shadow-card) or none. The header is always h-16; the docs topbar sticks at top-16 and the docs sidebar at top-[6.75rem]. Dashboard page titles are text-xl font-semibold (PageHeader); the dashboard sidebar is bg-sidebar (snow) and the content area white. Docs prose stays plain.
  • Next 16 lint rules (react-hooks v7): no setState directly in a useEffect body, no ref writes during render, use <Link> for internal paths (even /api/...).
  • SEO: every public page exports metadata = pageMetadata({ title, description, path, keywords?, type? }) from src/lib/seo.ts (canonical, Open Graph, Twitter card, merged seo.keywords from lib/config/site.ts; noIndex for one-time links) - dynamic routes call it from generateMetadata and return robots: { index: false } for unknown ids. Structured data goes through <JsonLd data={...}> (components/seo/json-ld.tsx, escapes <) with the builders in lib/seo.ts (organizationJsonLd/websiteJsonLd in the root layout, softwareJsonLd on the landing page, breadcrumbJsonLd + articleJsonLd on docs/compare pages, offersJsonLd + faqJsonLd on pricing, a SoftwareApplication per marketplace listing, itemListJsonLd on /vendors, /vendors/[vendor] and /compare). The default og:image is the generated card app/og/route.tsx (/og?title&subtitle&kicker, next/og + Poppins read from public/brand/fonts with fs - the standalone server has no asset fetch; clamped text; ogImageUrl() builds the URL, absoluteTitle pages keep the static social card). robots.ts disallows /platform/dashboard, /api/, /v1/, /mcp, /auth/, /cli/, /platform/invite/ and ?view=/?plan=/?next= variants; sitemap.ts lists canonical paths only (no query strings) including /vendors/<vendor> and /compare. Programmatic hubs: (marketing)/vendors (from serverApi.llms().vendors) and (marketing)/vendors/[vendor] (models table + FAQ, vendorHref() in components/models/reference-model.tsx); (marketing)/compare indexes COMPARISONS. Machine feeds: app/llms.txt/route.ts (llmstxt.org map built from docSections()), app/feed.xml/route.ts (Atom from releases(), advertised through ALTERNATE_TYPES in pageMetadata and the root layout - a page-level alternates replaces the layout's), app/indexnow.txt/route.ts (serves runtime INDEXNOW_KEY, 404 when unset) with scripts/indexnow.mjs submitting the sitemap after deploy (workflow step, OSR_INDEXNOW_KEY).
  • Analytics: components/analytics/google-analytics.tsx (rendered once in the root layout) loads the Google tag with Consent Mode v2 defaults denied, replays the stored choice, sends page_view per client navigation and mounts ConsentBanner; it renders nothing without NEXT_PUBLIC_GA_MEASUREMENT_ID or under /platform/dashboard. lib/analytics.ts exposes track(event, params) (used for the GA4 sign_up / login conversions in components/auth/*; when analytics.googleAdsConversions[event] is set it also fires the Ads conversion hit), trackCta, trackOutbound, trackWebVital (useReportWebVitals -> LCP/INP/CLS events), writeConsent and subscribeConsent (useSyncExternalStore, never setState-in-effect). Server components mark CTAs with data-track="<name>" data-track-location="<where>"; one delegated listener in GoogleAnalytics turns those and every external <a> into events, and CopyButton sends copy_code. Ids and verification tokens are build args (azure.yaml OSR_GA_MEASUREMENT_ID, OSR_GOOGLE_ADS_ID, OSR_GOOGLE_ADS_CONVERSIONS, OSR_GOOGLE_SITE_VERIFICATION, OSR_BING_SITE_VERIFICATION); the privacy page documents the tag and renders ConsentControls.
  • output: "standalone"; the image is platform/web/Dockerfile (context = platform/web). A running standalone server (node .next/standalone/server.js) holds .next/standalone locked on Windows and makes next build fail with EBUSY - stop it first.

Documentation site (/docs)#

The site is end-user documentation for the hosted platform, the Python SDK and the pip package. It renders the repository Markdown itself at build time - no API call, fully static. Internal material (docs/PLATFORM_PLAN.md, docs/GO_TO_MARKET.md, docs/BRAND.md, docs/sales/, platform/README.md) stays in the repository and is deliberately not in the catalogue; Markdown links to it fall back to GitHub.

  1. Content snapshot - node scripts/sync-content.mjs copies README.md, CHANGELOG.md, CONTRIBUTING.md, SECURITY.md, docs/** (minus docs/sales), deploy/README.md, spec/ocm/README.md, examples/leaderboard/README.md, platform/api/openapi.json and .claude/skills/*/SKILL.md into platform/web/content/ (git-ignored, same relative layout as the repo). npm predev/prebuild, the azd prepackage hook and the CI container job all run it; the Dockerfile copies content/ into the runtime image.
  2. Catalogue - src/lib/docs/catalogue.ts is the single source of truth for sections, order, slugs, titles, one-line summaries and icons. Sections: Start, Hosted platform (PLATFORM, MARKETPLACE, the api overview and its discovered api/<tag> children), Python SDK (SDK, ENTERPRISE, REFERENCE), Self-hosting (deploy), Concepts, Security, Agent skills (discovered) and Releases (changelog, ROADMAP, contributing). SECTION_TABS drives the top bar. Slugs: file stem for docs/*.md (/docs/GUIDE), plus readme, changelog, contributing, security-policy, deploy, ocm.
  3. Renderer - src/lib/docs/render.ts: remark-parse + GFM -> rehype-raw -> strip the first H1 (page header renders it) -> rehype-slug ids (GitHub-compatible; README anchors such as #10-routing-latency keep working) -> TOC (h2/h3) -> autolinked headings -> link rewriting (relative .md to /docs/<slug>, other repo paths to GitHub blob/tree, images to raw GitHub) -> Shiki (github-dark-default, languages listed in CODE_LANGS) -> HTML. ```mermaid fences become <pre class="mermaid"> and render client-side in components/docs/docs-article.tsx.
  4. REST API reference - src/lib/docs/openapi.ts reads the snapshot and groups operations by tag; components/docs/api-reference.tsx renders the overview (/docs/api: auth schemes, base URL, conventions, groups) and one page per tag (parameters, request body, responses, curl and JSON examples). Example bodies come from exampleBodyFor(); add a hint there for a new request model.
  5. Versions - src/lib/docs/versions.ts parses ## [x.y.z] - date headings from CHANGELOG.md; components/docs/docs-topbar.tsx shows the current release and a menu that links each release (and the unreleased section) to /docs/changelog#<anchor>. A release needs no code change here.
  6. Pages - src/app/(docs)/docs/page.tsx (index: intro, install, entry points per audience, one list per section), [...slug]/page.tsx (generateStaticParams from the catalogue, dynamicParams = false, breadcrumb, reading time, edit link, prev/next; API pages branch on kind === "api"), search.json/route.ts (static index; API pages index their operations) consumed by components/layout/search-command.tsx. sitemap.ts lists every catalogue entry.

To publish a new user-facing document: add the Markdown under docs/, add a STATIC_SECTIONS entry in catalogue.ts (slug, file, title, description, icon), link it from README.md (tests/test_docs.py requires every docs/*.md to be linked), run npm run build and open /docs/<slug>. Internal documents are linked from README.md only. A new .claude/skills/<name>/SKILL.md needs no code change. When a heading is renamed, search for the old anchor across docs/, README.md and platform/web/src.

Style: the docs pages are plain reference pages - headings, paragraphs, tables and code blocks. No icon cards, badges, pills, uppercase eyebrow labels, hover animations or drop shadows; no reading time or page counts; copy states what an endpoint or option does, without marketing adjectives.

Deploy#

azd provision            # infra/main.bicep: Container Apps osr-api + osr-web, Azure OpenAI, ACR, Files share, DNS zone
azd deploy api           # platform/api/Dockerfile, context = repo root
azd deploy web           # platform/web/Dockerfile, context = platform/web (prepackage hook syncs content/)
azd deploy platform      # same Dockerfile -> osr-platform (serves /platform; osr-web forwards to it)
.\scripts\domains.ps1    # custom domain: dns (zone + name servers) -> bind (certs) -> verify; -Phase clean retires osr-platform
.\scripts\sso.ps1        # sign-in providers: microsoft (az app registration + secret) | google | github | gitlab | show | push | apply

JSON-valued deployment inputs (OSR_PLATFORM_SSO_PROVIDERS_B64, OSR_PLATFORM_STRIPE_PRICES_B64) travel base64-encoded: azd substitutes ${VAR} textually into infra/main.parameters.json, so raw JSON breaks every provision ("invalid character after object key:value pair"); infra/main.bicep decodes with base64ToString. sso.ps1 is the only writer of the SSO map (local azd env -> gh secret set -> azd provision); every provider uses the single redirect <WEB_URL>/auth/callback (kept without the /platform prefix on purpose - the web app 308s it to /platform/auth/callback, so the URI registered at every provider never changes). Google and GitHub have no API for OAuth clients, so those commands print the console steps and prompt for the id and hidden secret.

.github/workflows/platform.yml runs API tests and the OpenAPI snapshot check, npm run check && npm run build, a two-container smoke (/, /docs, /docs/GUIDE, /docs/PLATFORM, /docs/api, /docs/api/routing, /docs/skills/osr-contributing, /docs/search.json, /pricing, /api/v1/info; /docs/sales/* must 404) and then azd up on main. After deploying, verify <WEB>/docs/api and <WEB>/api/v1/info (edition, sdk_version). NEXT_PUBLIC_SITE_URL is baked at build time via buildArgs in azure.yaml; SQLite on Azure Files needs nobrl mount options and a single API replica. The public domain is an Azure DNS zone (infra/dns.bicep, azd OSR_DNS_ZONE=opensmartroute.ai): apex A to the environment static IP, www/api CNAMEs, asuid.* TXT; the registrar delegates to the zone's name servers. www redirects come from platform/web/src/proxy.ts (OSR_WEB_REDIRECT_HOSTS), not from an edge proxy. Apex managed certificates use HTTP domain-control validation, subdomains CNAME (TXT never completes for an apex). The azd up job signs in with an OIDC federated credential whose subject is repo:isathish/OpenSmartRoute:environment:production (repository variables AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID, custom-domain OSR_* vars; secret OSR_PLATFORM_ADMIN_TOKEN) and seeds SERVICE_*_IMAGE_NAME from the running apps before azd provision; setup in platform/README.md ("Continuous deployment").