Constructor Studio web server — backend, frontend, installer.
| Project | What | Stack |
|---|---|---|
studio-backend/ |
Studio API service assembled from CF/Gears: multi-tenancy, users, groups. REST + OpenAPI at /cf/docs. |
Rust (axum/tokio/sea-orm via gears) |
studio-frontend/ |
Walking-skeleton SPA: token → /me → tenant list through the gateway. |
Vite + React 19 + TS, vitest |
One click (Docker): everything — Postgres, backend built from source, frontend on nginx:
docker compose up --build -d
# portal: http://localhost:8080 (sign in: studio-admin-token)
# API/docs: http://localhost:8090/cf/docsRequires Docker (Desktop with WSL integration is fine) and a gears-rust checkout as a
sibling of this repo. The first build compiles the whole gears workspace — grab a
coffee; rebuilds are cached. Stop with docker compose down (add -v to wipe data).
Daily dev (fast iteration): infra in Docker, backend on the host (WSL), frontend via Vite:
docker compose up -d postgres keycloak # once
# Environment (put these in your shell profile / a sourced env file):
export STUDIO_PG_PASSWORD=<compose postgres password>
export STUDIO_LLM_API_KEY=<LLM provider key> # AI chats + in-IDE Theia AI
# free default: Groq — console.groq.com → API Keys
export STUDIO_REGISTRY_USER=<your github login> # pulls the IDE image from ghcr
export STUDIO_REGISTRY_TOKEN=<PAT with read:packages> # (Docker API ignores `docker login`)
cd studio-backend && cargo run -- --config config/oidc.yaml run # WSL
cd studio-frontend && npm install && npm run dev # http://localhost:5173Sign in with SSO (admin/studio) — see "OIDC login" below for the one-time
self-signed-cert step. Static-token profiles remain for scripts:
config/postgres.yaml (Postgres) and config/dev.yaml (zero-Docker, SQLite).
Secrets self-heal on every boot (studio-secrets-bootstrap gear): the LLM key
is re-seeded into credstore automatically — no manual curls after restarts.
Switching the LLM provider is env-only: STUDIO_LLM_BASE_URL,
STUDIO_LLM_MODEL (Theia AI proxy) and STUDIO_LLM_HOST (mini-chat/OAGW)
override the Groq defaults; any OpenAI-compatible endpoint works.
"Open Studio" launches a dedicated Theia IDE container per workspace via the
studio-session gear (our first own gear — see
studio-backend/docs/adr/0003-theia-sessions.md).
No local image build needed: CI publishes the IDE image on every fabric-poc
push touching poc/theia/** (workflow theia-image.yml), and the gear pulls
and refreshes it automatically:
- image:
ghcr.io/constructorfabric/fabric-poc/cf-studio-theia:edge - auth: the package is private — set
STUDIO_REGISTRY_USER/STUDIO_REGISTRY_TOKEN(PAT withread:packages) before starting the backend.docker loginalone is NOT enough: the gear talks to the Docker API directly, which ignores the CLI credential store. - freshness:
always_pull: truere-pulls the mutableedgetag on every launch; a failed pull falls back to the local copy (offline-friendly). - hacking on the image locally:
cd ../fabric-poc/poc/theia && docker build -t cf-studio-theia:latest ., then in the config setimage: cf-studio-theia:latest+always_pull: false.
In the portal: workspace → Open Studio → Launch. Optional Git URL is
cloned into the workspace on first launch. Sessions bind to loopback ports
41000-41099, live 4 h (reaper), survive backend restarts (label adoption),
and can be stopped from the launcher. Inside the IDE, Theia AI (chat with
@Universal/@Coder agents, inline completion) is configured automatically by
the portal bridge through the backend's studio-llm-proxy — the provider
key never enters the container.
Requirements: Docker daemon reachable from the backend (/var/run/docker.sock).
In the full-docker profile the compose file mounts the socket and
/srv/cf-studio-workspaces into the backend (host and container paths must be
identical — bind sources are resolved by the host daemon).
The static dev tokens stay for scripts and quick starts; real browser login
uses the oidc-authn-plugin gear against a Keycloak shipped in compose.
docker compose up -d postgres keycloak
cd studio-backend && cargo run -- --config config/oidc.yaml runThen in the portal press "Sign in with SSO" — users admin / demo
(password studio). Dev Keycloak runs self-signed TLS on
https://localhost:8443: open that URL once and accept the certificate
before the first login. Admin console: same URL, admin/admin.
Sessions renew silently: the refresh token is kept in sessionStorage and
used to mint a new access token a minute before expiry, after any 401, and on
page load — so a reload keeps you signed in and the hourly access-token expiry
is invisible. Sign out (or closing the tab) drops it.
How it fits together: the portal does Authorization Code + PKCE
(src/oidc.ts, no dependencies), Keycloak issues a JWT whose sub is the
user UUID and whose tenant_id claim (from a user attribute, see
docker/keycloak/realm-studio.json) is the home tenant UUID; the
oidc-authn-plugin validates it via discovery/JWKS (the dev CA is trusted
through http_client.custom_ca_certificate_paths) and maps claims into the
platform SecurityContext. mini-chat's background S2S goes through the same
realm (s2s_oauth, confidential client mini-chat).
The GitHub/GitLab chips compose github.com / gitlab.com URLs. For a
self-hosted host use the Git URL source with the full HTTPS clone URL and
a PAT:
| Field | Value |
|---|---|
| name | csh_hypotheses_back (becomes the directory) |
| source | Git URL |
| url | https://gitlab.constr.dev/hypotheses/csh_hypotheses_back.git |
| PAT | a GitLab personal access token with the read_repository scope |
| mount at | optional — e.g. .workspace-sources/hypotheses/csh_hypotheses_back to match a CLI-created workspace layout |
The workspace root can be a repository too. A Studio workspace created by
the CLI is a git repo (manifest, docs, .workspace-sources/). Put its clone
URL in the dashboard's Workspace root field (plus a PAT and branch if
needed) and the session clones it on first launch — nothing has to exist on
the backend host. Sources then clone into it, and because CLI workspaces
gitignore .workspace-sources/, the root repo stays clean. The local-folder
field remains as the alternative and takes precedence when both are set.
HTTPS, not SSH: the session container has no SSH key or agent, while a PAT
travels as a credstore secret reference and is injected into the clone through
an inline credential helper (never written to .git/config). If the workspace
manifest lists git@… SSH remotes (as CLI-created ones do), the portal's
HTTPS source is what actually materializes the working copy; the manifest entry
stays untouched.
- Create a public client with PKCE (S256), redirect URI
http://localhost:5173/*(or your portal origin) and matching web origin. - Tokens must carry: UUID
sub, and atenant_idclaim with the user's home-tenant UUID (custom claim/attribute mapper). Adjustjwt.claim_mappinginconfig/oidc.yamlif your claim names differ. - Point
jwt.trusted_issuers(ands2s_oauth.discovery_url, if used) at your issuer URL — https required; add your corporate root CA viahttp_client.custom_ca_certificate_pathswhen it is not in system roots. - Frontend: set
VITE_OIDC_ISSUERandVITE_OIDC_CLIENT_ID.
ci.yml— on push/PR, path-filtered: backend (fmt, clippy-D warnings, build, test,--list-gearssmoke) and frontend (test, build). The backend job checks outconstructorfabric/gears-rustnext to the repo — path dependencies expect../../gears-rust; add aGEARS_RUST_TOKENsecret if that repo is private. DCO is enforced — commit with-s.release.yml— on tagv*: release binary + frontend dist → GitHub Release; Docker images →ghcr.io/constructorfabric/studio-web/studio-{backend,frontend}; then adeployjob gated by theproductionenvironment.- Theia IDE image — built in
fabric-poc(theia-image.yml):edgeon main pushes touchingpoc/theia/**,vX.Y.Zontheia-v*tags.
Release: git tag v0.1.0 && git push origin v0.1.0.
Chart in deploy/helm/studio-web (rendered/linted against the dmz values),
environment values + pipeline in GitLab constructorfabric/studio-web-ci
(Pattern B: clone the mirror → Trivy → mirror images to Harbor → helm).
The Secret contract and prerequisites live in
deploy/helm/values-dmz.example.yaml and deploy/README.md. Cluster v1
runs with IDE sessions disabled (studio-session.enabled=false in
config/k8s.yaml) until the per-session Pod driver lands (ADR-0003).