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.
| Directory | Service | Stack | Checks |
|---|---|---|---|
platform/api | osr-platform-api | FastAPI, SQLite, osr_platform package | ruff format --check platform; ruff check platform; mypy platform/api/osr_platform; pytest platform/api/tests -q |
platform/web | osr-platform-web | Next.js 16 App Router, React 19, Tailwind 4 | npm 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.pybuilds the FastAPI app (create_app(settings)),settings.pymapsOSR_PLATFORM_<FIELD>;api.pypublic/account routes,proxy.pyOpenAI-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(coreRouter) orenterprise(RouterBuilderwith auto-learning, health, guard, metrics, hash-chained audit; enables tenants, stats, audit). Both are built ineditions.pybehindEngine(controls(),health_snapshot(),observability_snapshot()); the guard with PII redaction andMetricsTelemetryrun in both. - Observability (
observability.py):RequestContextMiddleware(echo/generateX-Request-Id,Server-Timing, JSON access logosr.platform.access,HttpMetricsper route template),/healthz,/readyz(readiness()checks database, catalogue, router, audit file),/metrics(prometheus_text;OSR_PLATFORM_METRICS_PUBLIC=falsegates it behindX-Admin-Token). Public catalogue GETs return throughapi.cacheable(request, payload, max_age_s)(ETag,Cache-Control, 304 onIf-None-Match;OSR_PLATFORM_HTTP_CACHE_MAX_AGE_S).OSR_PLATFORM_ROUTE_CACHE_TTL_SaddsCacheMiddleware;OSR_PLATFORM_RETENTION_DAYSdrivesdb.purge_usagefrom a background sweep inapp.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 bothPUT /tenantsandPUT /policy;apply_policy(constraints, cfg)merges (fill boundaries, stricter caps, union deny, intersect allow).metered()callscheck_budgetonly whenmeta["spends"](execute / chat) and recordstenanton usage rows (db.spend,db.spend_by_tenant); exhaustion = 429 +Retry-After+X-Budget-Limit|Used|Period.GET /governance->PlatformAPI.governance(p). Web pagesapp/platform/dashboard/governance/page.tsx(all editions) andapp/platform/dashboard/health/page.tsx(enterprise,stats). New response headers must be added toEXPOSED_HEADERSinapp.pyandFORWARD_RESPONSE_HEADERS. - Traces and events (
api.py):event_buffer()finds the tracer'sMemorySink;trace_payload(p, rid)(GET /trace/{request_id}, 404 unlessdb.usage_by_request(account, rid)finds the row - always account-scope trace reads),events_payload(...)(GET /events, joined to the account throughdb.request_ids(account, since); request-less events only with thestatsfeature),status_payload()(publicGET /status, 503 whenreadiness()fails),mark_traced(items)on/activity,trace_id_of(rid)->trace_id+X-OSR-Trace-Idon/route. Feedback is persisted per request (db.record_feedback/db.feedback_for, tablefeedback);audit_records(p, rid)scans the audit file for the request (plans withaudit). Web:components/observability/ trace-view.tsx(TraceView,TraceLoader,stageTone,summarise) used byapp/platform/dashboard/events/ page.tsx, the activity drawer and the playgroundDecisionPanel;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 ofcli._build_autopilot:SelfImproveronengine.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 tosettings.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 inapp.py. AdminPOST /admin/autopilot/cycle->pilot.trigger("api"). Web:app/platform/dashboard/learning/page.tsx(nav iconbrain), typesLearningSnapshot,SlmInfo,AutopilotStatus,ImprovementReport. SDK side:EnterpriseRouter._bookkeeprunsRouter.observers;SelfImproveremitslearn.improve/learn.promote. Testsplatform/api/tests/test_learning.py. - Marketplace import (
harvest.py,docs/MARKETPLACE.md"Imported catalogue"):SkillsShSource(skills.sh sitemaps -> everySKILL.mdper repo throughGitHub.tree/raw),GitHubSearchSource(code search, token required, size buckets <= 1000 hits),MCPRegistrySource(stream?version=latest, active only ->toolrows).listing_for(h)builds the OCM manifest (metadata.source = {provider, repository, path, ref, url}, slugowner-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. AdminGET|POST /api/v1/admin/registry/import(max 500 items) andscripts/harvest_skills.py collect|stats|publishdrive it.RegistryStore.facets()is cached (FACETS_TTL_S,invalidate()on write);sources()counts listings per provider. Seeded manifests havemetadata.sourceas a string - alwaysisinstanceguard. Tests swap the transport:monkeypatch.setattr(harvest, "fetch", fake). Nevergit addthe work dirs (.osr-harvest*/). Whole-catalogue publishes go OFFLINE (publish --data-dir, stop the API, replaceplatform.sqlite3on 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 owningmarketplacewhenOSR_PLATFORM_MARKETPLACE_REFRESH_S > 0(Bicep parammarketplaceRefreshS, 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 coldSELECT tags FROM listingsover 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 namesquality/cost/latency),constraints,kinds,plan(Pro-gated),execute. - Operator console (
/platform/admin,api.admin_router()): operators (db.operators, scrypt hashes viaauth.hash_password/verify_password,osr_op_sessions 12 h, rolesoperator/superadmin) sign in atPOST /admin/auth/login; theself.admindependency yields anOperatorPrincipalfrom a Bearer operator token orX-Admin-Token(counts as superadmin) - keep every/api/v1/admin/*route on it, callrequire_superadminfor operator management andaudit(op, "<noun>.<verb>", ...)for mutations (anadminevent).bootstrap_operator()runs at start-up fromOSR_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 localStorageosr.platform.operatorToken),components/admin/admin-shell.tsx, pages underapp/platform/admin/(console)/,app/platform/admin/login;robots.tsdisallows/platform/admin. - Shared state (
cluster.py,db.pybackends):Database(path_or_url)picksSqliteBackendorPostgresBackend(to_postgres()rewrites?->%s,INSERT OR IGNORE,REAL->DOUBLE PRECISION,AUTOINCREMENT-> identity; qualify columns inON CONFLICT ... DO UPDATE SET x=table.x+1). Run the suite on PostgreSQL withOSR_TEST_DATABASE_URL(one schema per test).make_limiter->RedisRateLimiter(Lua sliding window) whenOSR_PLATFORM_REDIS_URL;make_event_bus->KafkaEventBuswhenOSR_PLATFORM_KAFKA_BOOTSTRAP(api.emit(topic, payload, key); topicsusage,feedback,admin;MemoryEventBusin tests viac.app.state.api.bus = ...)./readyzreportsredis,events,database_dialectwithout failing readiness.platform/docker-compose.yml= production stack (PostgreSQL, Redis, KRaft Kafka, API, web);infra/clusterModeaddsosr-postgres/osr-redis/osr-kafkacontainer apps (internal TCP ingress, one share each,nobrlmounts) and multi-replicaosr-api. Kafka image:CONTROLLER://localhost:9093(0.0.0.0 fails validation),CLUSTER_IDmust be 22 base64url chars; kafka-pythonbootstrap_connected()goes false after metadata - use_metadata.brokers()for health. - SLM service (
slm_service.py, entryosr-platform-slm):SLMWorker(cold-starts an untrained bundle toautopilot_dir/slm.json,ingest(topic, payload)fortraining{request_id,text} intoRequestMemoryandfeedbackinto aFeedbackStore,cycle()=SelfImprover.cycle, Kafka consumer grouposr-slm), FastAPI/healthz /model /predict /reports /cycleonsettings.slm_port(8090). API side:settings.slm_url,training_events(api.note_trainingnext tonote_decisionin /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_engineserves the promoted file whenslm_urlis set even withoutOSR_PLATFORM_SLM. Admin proxies/api/v1/admin/slm*use urllib (404 withoutslm_url, 503 unreachable). Webapp/platform/admin/(console)/slm/page.tsx. Teststest_slm_service.py(in-process TestClient + a stdlib HTTP relay for the proxy). - Domain services (
services.py):SERVICESspecs (name, gateway prefixes, entryosr-platform-<name>), ports insettings.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 includesgateway_routerfor a domain whenOSR_PLATFORM_<NAME>_URLis set (X-OSR-Serviceon the answer,text/event-streamrelayed chunk by chunk throughforward_async). Include order increate_platform_app: rankings (/stats/public) before routing (/stats), routing (/admin/autopilot) + marketplace + providers before theadmincatch-all, onboarding before accounts (/auth).ctx.owns(name)decides background work (owner ofroutingruns the autopilot and attaches the telemetry store). Adding a service = SERVICE_PORTS + SERVICES +main_<name>+ pyproject script +routers()+ loop tuple + both compose files + bicepdomainServices/domainServiceEnv+ platform/README table +test_services.py. - Notifications (
notifications.py):NotificationStore(tablesnotification_channels,notifications= one episode per alert id withstatefiring/resolved andread_at,notification_deliveries),Notifier(api, interval_s)= daemon loop started by theapirole (PlatformContext.start), leader throughSET osr:alerts:leader NX EXin Redis;tick()retries last round's failures, re-evaluatesapi.alerts_payload(p)for every active workspace andalerts.deployment_alerts(...)for the deployment,_reconcileopens/refreshes/resolves episodes, emits Kafka topicalerts, deliversfiring/escalated/resolvedthroughsend(ch, n, event, context)(email viaapi.mail, webhook JSON +X-OSR-Event,X-OSR-Delivery,X-OSR-Signature=sign(secret, body, ts), Slack Block Kit, Teams MessageCard;httpsonly,_post_jsonnever raises).notifications_router(workspace,accountsdomain, tagaccount) andadmin_notifications_router(deployment scope,admindomain) - add new alert rule names to theruleslist in each router. Settingsalerts_interval_s(OSR_PLATFORM_ALERTS_INTERVAL_S; tests pass0.0and callnotifier.tick(force=True)). Web:components/notifications/notifications-center.tsx(shared byapp/platform/dashboard/notifications/page.tsxwithuseApiandapp/platform/admin/(console)/notifications/page.tsxwithuseAdminApi),notifications-bell.tsxin the dashboard header. Teststest_notifications.py(a stdlibSinkHTTP server receives the webhooks; header names arrive title-cased - compare lower-cased). - Console extras (
admin_console.py):admin_console_router(admindomain) serves/analytics(SQL grouped by the portableusage.daycolumn; signups bucketed in Python),/activity(cross-workspace request log, cursorbefore),/lookup?q=,/settings(settings_payload: dataclass fields ->OSR_PLATFORM_<NAME>,_mask/_scrubhide secret-looking names and nestedclient_secret/api_keykeys - add new secret markers to_SECRET_MARKERS, new groups to_GROUPS),/retention+/retention/purge;admin_console_local_routerserves/healthfrom 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_NAVincomponents/admin/admin-shell.tsx(also the header search box ->/platform/admin/search?q=). Testsplatform/api/tests/test_admin_extras.py. - CLI sign-in (
osr login) is the RFC 8628 device grant inauth_router():POST /auth/device/code(anonymous,device:{ip}limiter 10/min + 30/h, returnsuser_codeXXXX-XXXXfromauth.generate_user_code,verification_uri = <web_url>/platform/cli/authorize),GET /auth/device/{user_code}andPOST /auth/device/approve|deny(signed in; approve needsadminand a free key slot;db.decide_device_code),POST /auth/device/token(authorization_pending/slow_down/access_denied/expired_tokenas HTTP 400 JSON; success mints a key viacreate_keyandconsume_device_code). Tabledevice_codesstores only the SHA-256 of the device code (DEVICE_CODE_TTL_S15 min). Web page:app/platform/(auth)/cli/authorize/page.tsx+components/auth/device-authorize.tsx. SDK side lives insrc/opensmartroute/credentials.py(device_login,CredentialStore). Tests:tests/test_device_flow.py. - The installer scripts
install.sh/install.ps1at the repository root are copied bysync-content.mjsand served verbatim byapp/install.sh/route.tsandapp/install.ps1/route.ts(lib/installers.ts); install one-liners for the site come frominstallinlib/config/site.ts. - Chat proxy metadata key is
opensmartroute;models[]= candidate list with fallbacks;stream= SSE. - Tests use
platform/api/tests/conftest.pyfixtures (temp data dir,examples/targets.yaml). Smoke a deployment withpython platform/api/scripts/smoke.py <base-url>. platform/api/openapi.jsonis a committed snapshot of the enterprise-edition OpenAPI document (OPENAPI_TAGS,SECURITY_SCHEMESandopenapi_snapshot()inapp.py). After changing any route, model or docstring runpython -X utf8 platform/api/scripts/export_openapi.py;test_openapi_snapshot_is_currentand the CI--checkstep fail on drift. Tag descriptions become the intro of each/docs/api/<tag>page; theadminanddocstags and every/api/v1/admin/*path (HIDDEN_TAGS,HIDDEN_PATHinlib/docs/openapi.ts) are hidden from the site; give a new tag a title inTAG_TITLES.
Web app (platform/web)#
- Route groups:
(marketing)(landing, marketplace, models, rankings, estimate, pricing, roi, compare, plus the prose pagessupport,terms,privacybuilt oncomponents/marketing/prose-page.tsx),(docs), and everything signed-in underplatform/- 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) andplatform/(marketing)(playground,marketplace/publish- public-site chrome, the layout re-exports(marketing)/layout.tsx)./platformitself redirects to the dashboard. Write page paths with the prefix (/platform/dashboard/keys,/platform/login);src/lib/routes.tshasPLATFORM_PREFIX,platformPath()andLEGACY_PLATFORM_PATH(the pre-split paths thatsrc/proxy.ts308-redirects). In production the website and the platform are two container apps running the same image:osr-webhasOSR_WEB_PLATFORM_URLand forwards/platformrequests toosr-platform(NextResponse.rewrite); a single process (next dev, Compose) serves both. Site-wide names and nav live insrc/lib/config/site.ts:headerNav(groupsrender as navigation-menu panels on desktop and sections in the mobile sheet,linksare top-level; icon names resolve incomponents/layout/nav-icons.tsx),footerNav(Product / Developers / Resources / Company groups) andlegalNav(footer bottom bar). Every layout renders<main id="main">- the header's skip link targets it. New public pages also go intoSearchCommand'sPAGES,app/sitemap.tsand the container smoke step of.github/workflows/platform.yml. Server-side API reads insrc/lib/api/server.ts(serverApi, memoised); client calls insrc/lib/api/client.ts. - UI toolkit:
src/components/uiis the shadcn/uinew-yorkkit (components.json: rsc, slate, CSS variables, lucide icons; aliasesutils -> @/lib/utils/format,ui -> @/components/ui,hooks -> @/hooks). Add a component withnpx shadcn@latest add <name>, then fix the generator output: it writesimport { cn } from "cn"(must be@/lib/utils/format), it may scaffoldnext-themes/dark-mode code (remove it - the dark surface is thetheme-darkclass, not a theme provider) and itsMath.random/setState-in-effect patterns fail the React Compiler lint. Radix comes from theradix-uiumbrella package only - never add@radix-ui/react-*. Brand tokens map onto the kit variables inglobals.css(--background,--muted-foreground, ...): usetext-muted-foreground,bg-muted,border-input,ring-ringrather than legacytext-muted. Legacy-named wrappers kept for call sites:Badge/Card/Alert/EmptyState/StatCardinui/card.tsx,Button/ButtonLink(external) inui/button.tsx,Input/Textarea/SearchInput/Select/Switch/Fieldinui/field.tsx,Avatar/Progress/Separatorinui/primitives.tsx,DataTable+Table*(TableRow interactive,TableCell numeric) inui/table.tsx,FilterChipinui/toggle.tsx,SegmentedControlinui/tabs.tsx.Selectrenders a sizing wrapper around the native<select>: width/height/text-size classes go onclassName(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 fromnext/fontinapp/layout.tsx- Inter (--font-sans, alsofont-display), Instrument Serif (--font-serif, weight 400 only, neverfont-bold), Geist Mono (--font-mono), Quicksand only for the wordmark (font-wordmark). Utilities inglobals.css:heading-seriffor display titles withtext-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">withSectionHeading(eyebrow, seriftitlewith an<em className="text-slate-ink">second clause,description; left-aligned by default,as="h1"for the page title,sizesm/md/lg) anddivide-y divide-lineledgers withfont-mono text-sm text-muted-foreground tabular-numsnumerals - not icon cards. Alternate bands usebg-snow. Buttons:primary/default= ink fill,accent= brand-blue (rare),secondary= mist,outline,inverseon dark panes. Inline links aretext-ink underline decoration-ink/30 underline-offset-4 hover:decoration-ink, highlighted cards useborder-ink(no coloured rings), hover bordershover:border-ink/30, shadows hairline (shadow-card) or none. The header is alwaysh-16; the docs topbar sticks attop-16and the docs sidebar attop-[6.75rem]. Dashboard page titles aretext-xl font-semibold(PageHeader); the dashboard sidebar isbg-sidebar(snow) and the content area white. Docs prose stays plain. - Next 16 lint rules (react-hooks v7): no
setStatedirectly in auseEffectbody, no ref writes during render, use<Link>for internal paths (even/api/...). - SEO: every public page exports
metadata = pageMetadata({ title, description, path, keywords?, type? })fromsrc/lib/seo.ts(canonical, Open Graph, Twitter card, mergedseo.keywordsfromlib/config/site.ts;noIndexfor one-time links) - dynamic routes call it fromgenerateMetadataand returnrobots: { index: false }for unknown ids. Structured data goes through<JsonLd data={...}>(components/seo/json-ld.tsx, escapes<) with the builders inlib/seo.ts(organizationJsonLd/websiteJsonLdin the root layout,softwareJsonLdon the landing page,breadcrumbJsonLd+articleJsonLdon docs/compare pages,offersJsonLd+faqJsonLdon pricing, aSoftwareApplicationper marketplace listing,itemListJsonLdon/vendors,/vendors/[vendor]and/compare). The defaultog:imageis the generated cardapp/og/route.tsx(/og?title&subtitle&kicker,next/og+ Poppins read frompublic/brand/fontswithfs- the standalone server has no assetfetch; clamped text;ogImageUrl()builds the URL,absoluteTitlepages keep the static social card).robots.tsdisallows/platform/dashboard,/api/,/v1/,/mcp,/auth/,/cli/,/platform/invite/and?view=/?plan=/?next=variants;sitemap.tslists canonical paths only (no query strings) including/vendors/<vendor>and/compare. Programmatic hubs:(marketing)/vendors(fromserverApi.llms().vendors) and(marketing)/vendors/[vendor](models table + FAQ,vendorHref()incomponents/models/reference-model.tsx);(marketing)/compareindexesCOMPARISONS. Machine feeds:app/llms.txt/route.ts(llmstxt.org map built fromdocSections()),app/feed.xml/route.ts(Atom fromreleases(), advertised throughALTERNATE_TYPESinpageMetadataand the root layout - a page-levelalternatesreplaces the layout's),app/indexnow.txt/route.ts(serves runtimeINDEXNOW_KEY, 404 when unset) withscripts/indexnow.mjssubmitting 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, sendspage_viewper client navigation and mountsConsentBanner; it renders nothing withoutNEXT_PUBLIC_GA_MEASUREMENT_IDor under/platform/dashboard.lib/analytics.tsexposestrack(event, params)(used for the GA4sign_up/loginconversions incomponents/auth/*; whenanalytics.googleAdsConversions[event]is set it also fires the Adsconversionhit),trackCta,trackOutbound,trackWebVital(useReportWebVitals-> LCP/INP/CLS events),writeConsentandsubscribeConsent(useSyncExternalStore, never setState-in-effect). Server components mark CTAs withdata-track="<name>" data-track-location="<where>"; one delegated listener inGoogleAnalyticsturns those and every external<a>into events, andCopyButtonsendscopy_code. Ids and verification tokens are build args (azure.yamlOSR_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 rendersConsentControls. output: "standalone"; the image isplatform/web/Dockerfile(context =platform/web). A running standalone server (node .next/standalone/server.js) holds.next/standalonelocked on Windows and makesnext buildfail 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.
- Content snapshot -
node scripts/sync-content.mjscopiesREADME.md,CHANGELOG.md,CONTRIBUTING.md,SECURITY.md,docs/**(minusdocs/sales),deploy/README.md,spec/ocm/README.md,examples/leaderboard/README.md,platform/api/openapi.jsonand.claude/skills/*/SKILL.mdintoplatform/web/content/(git-ignored, same relative layout as the repo). npmpredev/prebuild, the azdprepackagehook and the CI container job all run it; the Dockerfile copiescontent/into the runtime image. - Catalogue -
src/lib/docs/catalogue.tsis the single source of truth for sections, order, slugs, titles, one-line summaries and icons. Sections: Start, Hosted platform (PLATFORM,MARKETPLACE, theapioverview and its discoveredapi/<tag>children), Python SDK (SDK,ENTERPRISE,REFERENCE), Self-hosting (deploy), Concepts, Security, Agent skills (discovered) and Releases (changelog,ROADMAP,contributing).SECTION_TABSdrives the top bar. Slugs: file stem fordocs/*.md(/docs/GUIDE), plusreadme,changelog,contributing,security-policy,deploy,ocm. - Renderer -
src/lib/docs/render.ts: remark-parse + GFM -> rehype-raw -> strip the first H1 (page header renders it) ->rehype-slugids (GitHub-compatible;READMEanchors such as#10-routing-latencykeep working) -> TOC (h2/h3) -> autolinked headings -> link rewriting (relative.mdto/docs/<slug>, other repo paths to GitHub blob/tree, images to raw GitHub) -> Shiki (github-dark-default, languages listed inCODE_LANGS) -> HTML.```mermaidfences become<pre class="mermaid">and render client-side incomponents/docs/docs-article.tsx. - REST API reference -
src/lib/docs/openapi.tsreads the snapshot and groups operations by tag;components/docs/api-reference.tsxrenders 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 fromexampleBodyFor(); add a hint there for a new request model. - Versions -
src/lib/docs/versions.tsparses## [x.y.z] - dateheadings fromCHANGELOG.md;components/docs/docs-topbar.tsxshows 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. - Pages -
src/app/(docs)/docs/page.tsx(index: intro, install, entry points per audience, one list per section),[...slug]/page.tsx(generateStaticParamsfrom the catalogue,dynamicParams = false, breadcrumb, reading time, edit link, prev/next; API pages branch onkind === "api"),search.json/route.ts(static index; API pages index their operations) consumed bycomponents/layout/search-command.tsx.sitemap.tslists 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").