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/ |
Portal UI on FrontX (shell + microfrontends, ADR-0006) — what CI/release build, compose serves and k8s deploys. | React 19 + TS via FrontX templates |
studio-frontend-prototype/ |
The pre-FrontX portal SPA, kept as a playground; ships as its own image (studio-frontend-prototype) and runs with the compose stack on port 8081. Tested/built at release time, not in CI. |
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). gears-rust is a git dependency
pinned in Cargo.toml, so cargo fetches it during the image build — no sibling checkout
is needed. The first build compiles the whole gears workspace — grab a coffee; rebuilds
are cached. Stop with docker compose down (add -v to wipe data).
Fresh / empty database — handled automatically. On a brand-new Postgres volume the
LLM chain's oagw gear would otherwise abort boot: it resolves the platform root tenant
in its post_init, but account-management only seeds that root later, in its serve
phase. A plain docker compose up now closes this chicken-and-egg by itself — a one-shot
backend-bootstrap service (built --no-default-features, so no oagw) starts first,
reaches serve, seeds and realm-binds the root, then exits. The main backend is gated on
that seeder finishing (service_completed_successfully), so it only starts against an
already-seeded database. On a warm volume the seeder is a fast no-op and leaves nothing
running — the same docker compose up works cold or warm.
Prototype playground: the pre-FrontX SPA comes up with the stack automatically —
http://localhost:8081 (own image, same backend, same /cf/ proxy).
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 is needed: this repository's Build Images workflow
publishes cf-studio-theia beside every backend image.
- local Docker image:
ghcr.io/constructorfabric/studio-web/cf-studio-theia:edge - Kubernetes image: the immutable backend SHA through the cluster's GHCR proxy
- 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 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.
Local 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).
Inviting a user in the portal creates a real Keycloak user, not a local stub. Account-
management drives this through the official cf-gears-keycloak-idp-plugin (it replaced
the in-crate plugin): every tenant is bound to a Keycloak realm, and user operations
(invite, list) run against that realm.
All Studio tenants share one realm, studio. The binding is seeded once, at bootstrap —
account-management.bootstrap.root_tenant_metadata: { realm_name: "studio" } binds the
platform root, and every descendant (organization → workspace → project) inherits
studio through its parent context. There is nothing per-tenant to configure, and no
idp_provisioning flag is needed: account-management provisions every tenant with its
IdP plugin regardless.
Because the binding is written at bootstrap, it exists only on tenants created after it
was configured — turning it on requires a fresh database (docker compose down -v). The
studio-admin confidential client in docker/keycloak/realm-studio.json already carries
the realm-management roles the plugin needs, so docker compose up wires everything
with no manual Keycloak steps. Full swap notes: studio-backend/docs/keycloak-idp-migration.md.
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/ Test — on push/PR, path-filtered: backend (fmt, clippy-D warnings, locked build, tests, and--list-gearssmoke) and frontend (studio-frontend/: build and tests).release.yml/ Build Images — service tagsv*publish backend, frontend, prototype, and Theia images; infrastructure tagsinfra-v*publish graph PostgreSQL and Keycloak images. Every publication requires Test success for the exact commit.deploy.yml/ Deploy Services — manually deploys backend, frontend, or both. Branch snapshots are dev-only;v*releases may target any configured application environment.deploy-infra.yml/ Deploy Infra — manually reconciles graph PostgreSQL and Keycloak from a publishedinfra-v*release.
Service release: git tag v0.1.0 && git push origin v0.1.0.
Infrastructure release: git tag infra-v0.1.0 && git push origin infra-v0.1.0.
The current compatibility chart is in deploy/helm/studio-web. Environment
values and GitHub Actions deployment workflows live in this repository; there
is no GitLab or Argo CD deployment dependency. The pipeline contract, Secret
contract, and prerequisites live in deploy/PIPELINES.md,
deploy/helm/values-dmz.example.yaml, and deploy/README.md. Cluster v1
uses the Kubernetes per-session Pod driver when
backend.sessions.enabled=true (enabled for dev). The chart owns the
namespace-scoped Pod/Service RBAC and keeps the Theia image on the same
immutable service SHA as the backend (ADR-0003).