* feat(web): opt-in Google Analytics via GOOGLE_ANALYTICS_ID (MUL-8253)
Co-authored-by: multica-agent <[email protected]>
* fix(i18n): add missing zh-Hant keys for model override and scheduled wakeup
Co-authored-by: multica-agent <[email protected]>
* fix(web): keep credential-bearing URLs out of Google Analytics
Co-authored-by: multica-agent <[email protected]>
* feat(selfhost): pass GOOGLE_ANALYTICS_ID through compose and helm
Co-authored-by: multica-agent <[email protected]>
* fix(web): stop GA for the document after sensitive navigation
Co-authored-by: multica-agent <[email protected]>
* test(views): await subscription cleanup and dialog focus
Co-authored-by: multica-agent <[email protected]>
* fix(web): stop GA before credential-bearing link clicks and form submits
GA4 enhanced measurement reports outbound/download link_url and
form_destination without a URL change, so signed attachment downloads
leaked their signature. Stop the tag for the document in a window
capture listener before gtag's own listeners observe the interaction.
Object URLs are exempt. Adds jsdom tests and a live gtag.js regression
covering the ~5s batch delay.
Co-authored-by: multica-agent <[email protected]>
* test(web): slim GA tests; move views test fixes to #9555
Drop the opt-in live-tag e2e spec (never runs in CI) and condense the GA
unit tests to the cases that guard against credential leaks. The two
unrelated views test-timing fixes now live in #9555.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: Bohan-J <[email protected]>
* feat(selfhost): wire feature flags through Compose and Helm
Feature flags were documented in .env.example but neither self-host
deployment path forwarded them: the Compose backend environment is an
explicit allowlist and the Helm ConfigMap had no flag entries. Workflows
shipped GA in v0.7.0 but stayed hidden on a default self-host install.
- forward MULTICA_FEATURE_FLAGS_FILE and FF_PROJECT_WORKFLOWS_V1 (default
true) from the Compose backend service
- render backend.config.featureFlags as FF_<FLAG_KEY> entries in the Helm
ConfigMap, keep Workflows on for --reuse-values upgrades, and add
featureFlagsFile for operators who mount a YAML rule file
- document flag precedence, self-host defaults, and per-path overrides in
SELF_HOSTING_ADVANCED.md and .env.example
- extend selfhost-config.test.sh and helm-config.test.sh so the wiring and
the documented defaults cannot drift silently again
* docs(feature-flags): add the self-host feature flag reference
Add a Feature flags page under Self-hosting in the documentation site,
translated into all five locales, and link it from the self-hosting
section of every navigation tree. The page covers flag precedence, the
self-host defaults, Compose and Helm overrides, the YAML rule file, and
how to verify a change through /api/config.
* feat(server): allow workspace creation only for listed emails (self-host)
Add WORKSPACE_CREATION_ALLOWED_EMAILS. When set, POST /api/workspaces
returns 403 for callers whose email is not listed. Empty keeps the
current behavior; DISABLE_WORKSPACE_CREATION=true still blocks everyone.
No instance-admin role, no domain allowlist, no UI hiding.
Refs #9400, MUL-8136
Co-authored-by: multica-agent <[email protected]>
* docs(self-host): clarify which signup vars /api/config exposes
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(daemon): report the checkout's own repo sync error
ensureRepoReady read the sync failure from a per-workspace slot that any
concurrent background sync overwrote, so a later successful sync of another
repo erased the error and checkout reported only "repo is configured but
not synced". syncWorkspaceReposContext now returns its error to the caller
and the shared slot is removed.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* feat(server): share SSL_CERT_DIR CAs with daemons for git
Daemons clone with the system git, which never sees the backend's
SSL_CERT_DIR, so a repository on a host with an internal CA could not
sync even after the operator made the backend trust that CA.
The new gitca package mirrors crypto/x509's SSL_CERT_DIR loading, skips
the system directories, and keeps only certificates crypto/x509 trusts.
The register and workspace repos responses carry them as ca_certs. The
daemon writes its system CA bundle plus those certificates to a bundle
file, adds http.sslCAInfo to its git environment, and records it in the
cache and isolated checkout config so agents' fetch and push use it too.
An empty value removes the bundle; Windows daemons skip it.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* docs: document private CA trust for daemons
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
* fix(daemon): keep operator CA trust and existing checkouts working
Review of the private CA support found two regressions for repositories
that already worked:
- Removing the last backend CA deleted the bundle while existing linked
worktrees and isolated checkouts still pointed at it, so their HTTPS
fetch and push failed. The bundle is now rewritten to the machine's own
trust instead, and each repository drops the reference the next time
the cache configures it.
- The bundle started from the system bundle and was injected through the
environment, overriding an operator's general http.sslCAInfo and any
repository-local value. It now starts from the file a general
http.sslCAInfo names (system bundle otherwise), the environment no
longer carries it, clone passes it with -c, and repository config is
only set where unset and only unset where it is ours. URL-scoped
settings, repository-local values, and GIT_SSL_CAINFO keep precedence.
Isolated checkouts record the setting before their first checkout, which
can lazily fetch from origin, and clones before the refspec migration
fetch.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---------
Co-authored-by: Iliya Anastasov <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
* fix(vcs): report untrusted provider TLS certificates on connect (MUL-7639)
ConnectVCS reported every non-token failure as "could not reach the
provider instance" and logged nothing, so a Gitea behind a private CA
looked like a network problem. Log the underlying validation error and
tell certificate failures (untrusted CA, host name mismatch, other
verification failures such as expiry) apart from unreachable instances.
Status codes are unchanged.
Co-authored-by: multica-agent <[email protected]>
* feat(helm): trust extra CA certificates in the backend (MUL-7639)
backend.extraCACerts.configMap mounts an existing ConfigMap of PEM
certificates read-only and points SSL_CERT_DIR at the system directory
plus the mount, so the backend trusts an internal CA without dropping
public CAs or disabling TLS verification. Unset renders unchanged.
Co-authored-by: multica-agent <[email protected]>
* docs(self-host): document trusting a private CA (MUL-7639)
Co-authored-by: multica-agent <[email protected]>
* fix(helm): render when values predate extraCACerts (MUL-7639)
helm upgrade --reuse-values from an older chart carries no
backend.extraCACerts key, and reading .configMap on it failed the whole
render with a nil pointer even when no CA was wanted. Fall back to an
empty dict, and cover the missing-key, default and enabled renders in
the chart test.
Co-authored-by: multica-agent <[email protected]>
* docs(self-host): restart Compose backend after replacing a CA (MUL-7639)
up -d keeps the running container when only a mounted CA file changed,
so the backend kept its old trust store. Use restart instead.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-agent <[email protected]>
* fix(timeouts): close the two gaps the 2h inactivity budget left open (MUL-6737)
Two independent places where a timeout still answered the wrong question.
Codex runs a semantic-inactivity timer inside the app-server protocol, and
unlike the daemon's watchdog it cannot see that a tool is in flight: a
commandExecution that prints nothing for the window trips it anyway. At a
fixed 10m ceiling that timer killed a quiet test suite or an
output-buffering `docker build` long before the 2h daemon budget meant to
protect it, which made "we raised the budget to 2h" false for Codex users.
It now derives from the daemon budget, taking the larger of the idle and
tool windows because it stands in for both. A tool budget of 0 means "never
force-stop during a tool", which this timer cannot express, so it falls back
to the idle budget rather than running unbounded; with the watchdog suite
disabled entirely Codex keeps its own built-in default, as it always has.
The env override and flag are unchanged.
Queued tasks expired on a pure wall clock: queued longer than a TTL meant
failed. That conflated "nobody is coming for this" with "the queue ahead is
long", and only the first is a failure. MUL-6558 was the second — a
self-hosted runtime with low concurrency held its own queue past 2h and
healthy work died as queued_expired. The TTL knob added then only moved the
cliff. Expiry now keys on whether the owning runtime still proves liveness,
the same signal FailTasksForOfflineRuntimes already uses for
dispatched/running rows, so a daemon going down retires its queued and its
in-flight work on one clock instead of two. A busy runtime keeps its
backlog for as long as it takes. MULTICA_TASK_QUEUED_TTL is removed with
the wall clock it configured.
Heartbeat age is read directly instead of gating on runtime.status='online'
so a row stuck at 'online' with a dead heartbeat still releases its queue,
and rows with no usable runtime binding — runtime_id IS NULL from before
migration 251, whose CHECK landed NOT VALID, or a runtime row that is gone —
are expired on sight rather than stranded forever by a JOIN that matches
nothing.
Tests: the queued sweep test now runs both phases against the same rows,
asserting the 5h-old task survives while its runtime heartbeats (the
MUL-6558 regression) and only expires once that runtime goes silent.
Assertions are scoped to the test's own task ids, because a runtime-keyed
sweep legitimately also collects queued rows other tests left on the shared
fixture runtime.
Co-authored-by: multica-agent <[email protected]>
* fix(sweeper): give a queued task its own reconnect grace before expiring it
Review catch: keying expiry on runtime liveness alone was too aggressive in
one direction. Enqueue binds a task to agent.runtime_id without checking
that the runtime is up (task.go CreateAgentTask call sites), so for a
runtime that has already been dark for longer than the grace, the liveness
clause is satisfied the moment a new task is created — assigning an issue
to a laptop closed overnight would fail it inside one 30s sweep tick
instead of waiting for the machine to come back. That is the opposite of
what "reconnect grace" promises.
Require the row's own age as well: a queued task now waits a full grace,
counted from when it started waiting, before it can be given up on. A
heartbeating runtime still never expires its backlog, so the MUL-6558 fix
is unchanged. The age bound also stops the sweep re-evaluating the runtime
subquery against every queued row on every tick.
Also remove the deploy surfaces the previous commit missed — the knob was
deleted from the server but helm (values + configmap), the self-host
compose file, and the CI-run helm render assertions still advertised it, so
operators would have kept setting a variable nothing reads. Same for five
orphaned comment lines in .env.example describing the removed variable.
Correct the fail-closed comment on the unbound-runtime arms: they are not
reachable for "rows predating migration 251" — runtime_id was NOT NULL from
migration 004 until 251 replaced it with a CHECK that every insert and
update is verified against, and the migration-004 FK is ON DELETE RESTRICT.
The arms stay as defence against a future schema change, which is what the
comment now says.
Tests: a third phase covers the reported case — a task enqueued against an
already-dead runtime survives, then expires once it has waited a full grace
of its own. Phase 2's expectation is corrected accordingly: the row created
at now() no longer expires when its runtime dies, because it has not waited
its own grace yet.
Co-authored-by: multica-agent <[email protected]>
* docs(timeouts): state both queued-expiry conditions and correct the FK claim
Review follow-up on b567511.
The user docs still described queued expiry as a single condition — "fails
once the runtime has been silent past the reconnect grace" — which is the
pre-b567511 behaviour and skips exactly the case that commit fixed. A
reader assigning work to a machine that went offline last night would
conclude the task already qualifies to fail. All four locales of tasks,
daemon-runtimes and troubleshooting now state both conditions and why the
second one exists.
The environment-variables pages contradicted themselves: the env table had
been updated to say the Codex semantic budget is derived, while the
`multica config` supported-keys table further down still advertised a 10m
default. That table now describes the resolution rule instead of a value.
The schema comment added in b567511 named the wrong constraint.
agent_task_queue_runtime_id_fkey (migration 004) is ON DELETE CASCADE; the
ON DELETE RESTRICT one is agent_runtime_id_fkey on agent.runtime_id. The
conclusion is unchanged — a queued row cannot reference a deleted runtime —
but the mechanism is a cascade plus migration 251's explicit unbind of
history rows, not a restrict.
Also log the resolved codex_semantic_inactivity at daemon startup, for the
same reason tool_watchdog is logged: it is now a derived value, so without
it the effective Codex budget cannot be read anywhere until a timeout
fires. And correct a test comment that still claimed both non-exempt rows
fail — the fresh row now stays queued, which is the assertion right below it.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Replace the separate Fleet/capacity URLs and seat-capacity switches with the single managed MULTICA_CLOUD_URL connection. Strict seat admission turns on whenever Multica is Cloud-connected and fails closed when the capacity machine token is absent or invalid; the recovery worker starts only for a valid executor so live intents keep their retry budget during credential outages.
Split invitation throttling into a non-consuming precheck and post-reservation consumption. Persistent capacity_full and capacity_overcommitted rejections charge only the actor budget; transient failures charge none. Preserve every Cloud/proxy HTTP 429 as retryable with both Retry-After forms, defer outbox rows without spending attempts, and use Cloud's rate-limit scope so a workspace 429 delays only that tenant while global or unscoped 429s stop the current batch.
BREAKING: delete/unset MULTICA_FLEET_URL, MULTICA_CLOUD_FLEET_URL, MULTICA_CLOUD_FLEET_TIMEOUT, MULTICA_SUBSCRIPTION_CAPACITY_ENABLED, MULTICA_SUBSCRIPTION_CAPACITY_URL and MULTICA_SUBSCRIPTION_CAPACITY_WORKER_ENABLED before rollout. The startup guard rejects any non-empty value, so MULTICA_SUBSCRIPTION_CAPACITY_ENABLED=false still blocks boot. Configure MULTICA_CLOUD_URL, MULTICA_CLOUD_TIMEOUT and MULTICA_SUBSCRIPTION_CAPACITY_SERVICE_TOKEN instead; entitlements keep their independent MULTICA_ENTITLEMENT_POLICY_* configuration.
Companion: multica-ai/multica-cloud#57
* feat(billing): purchase seats from full invite flow
* fix(billing): harden full-capacity invite flow
* fix(billing): explain overcommit with cloud seat occupancy
Cloud refuses a reserve on used + reserved, but the overcommit toast filled
members_over_capacity_description with the product member count. The most
common trigger is a first ledger snapshot whose members alone still fit
inside the purchased seats, so the message read "3 members but only 3
purchased seats" and did not explain the rejection.
Add occupancy_over_capacity_description, which names occupied seats,
purchased seats, members and pending invitations, and feed it from the
Cloud summary. members_over_capacity_description stays as-is: the billing
tab banner is genuinely member-count driven.
* MUL-6625: enforce prepaid member seat capacity
Co-authored-by: multica-agent <[email protected]>
* fix(seat-capacity): address review findings
Co-authored-by: multica-agent <[email protected]>
* fix(seat-capacity): close retry and worker races
Co-authored-by: multica-agent <[email protected]>
* fix(seat-capacity): bound lock waits during rollback
Co-authored-by: multica-agent <[email protected]>
* fix(seat-capacity): pin the cross-repo confirm settlement budget
Multica Cloud must wait longer than this worker's whole retry budget before it
treats a member row as one its ledger never recorded; adopting a member whose
confirm is still retrying counts one person as two seats, and an over-estimate
is what asks a customer to buy seats they do not owe.
That budget was only implicit here — 10 attempts of exponential backoff plus a
tick and a lock wait each — and Cloud's horizon had been set from a single
retry. Extract the backoff into retryBackoff, expose SettlementBudget, and
assert the worst case (29m15s) stays inside Cloud's 45m horizon, so raising
maxAttempts or the backoff cap fails review here instead of widening the window
in which Cloud cannot quantify a ledger.
Co-authored-by: multica-agent <[email protected]>
* Revert "fix(seat-capacity): pin the cross-repo confirm settlement budget"
This reverts commit d0ec36f647.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Lets self-hosted deployments raise the queued-task expiry window above the built-in 2h default, so low-concurrency runtimes stop losing legitimately-pending work to queued_expired. Default behavior is unchanged. Covers .env.example, Compose, Helm values/ConfigMap, and the behavior docs in all four locales.
Co-authored-by: Martin Montes <[email protected]>
* docs(self-hosting): correct the PostgreSQL extension requirements
The docs claimed Multica requires pgvector and told operators to run
`CREATE EXTENSION IF NOT EXISTS vector`. Nothing in the codebase uses it:
across all 429 migrations there is no `vector` column and no migration
creating the extension, and no Go package imports a pgvector driver. On a
stock PostgreSQL 17 that `CREATE EXTENSION` fails outright, so the
instruction actively blocked the "use your own PostgreSQL" path it appeared
in.
There are exactly four `CREATE EXTENSION` statements in the migration set.
`pgcrypto` (001) and `pg_trgm` (137) are bare statements that fail the
migration when missing; both ship with every standard PostgreSQL 17 build.
`pg_bigm` (032) and `pg_cron` (076) are wrapped in `DO ... EXCEPTION` and
skip gracefully. Document all four with their real status, and note that a
`schema_migrations` row does not prove a conditionally skipped object
exists.
`pg_bigm` is described as what it actually is — a CJK search-quality
improvement, not a correctness requirement. Migrations 138-142 install
portable `pg_trgm` fallback indexes covering every column the search
handlers touch, so a deployment without `pg_bigm` still searches on an
index.
References to the bundled image name are left alone: those describe which
image the compose file pulls, which is still accurate. The Helm chart
description and values comment are updated because they present pgvector as
part of the stack rather than as an image name.
Co-authored-by: sail-cn <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* docs(self-hosting): document the local disk attachment storage variables
The File Storage section only covered S3 and CloudFront, but local disk is
what the backend uses whenever `S3_BUCKET` is unset — every default
self-host install. `LOCAL_UPLOAD_DIR` and `LOCAL_UPLOAD_BASE_URL` were
absent from the self-hosting docs.
The default `./data/uploads` resolves against the backend's working
directory. Inside the bundled image that is `/app`, where the compose file
mounts the `backend_uploads` volume, so it happens to be correct. Running
the binary manually resolves it against whatever directory the process was
launched from, which silently scatters attachments as that directory
changes and leaves `attachment` rows pointing at files the server no longer
looks for.
Co-authored-by: sail-cn <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* docs(search): fix the migration numbers in the statement-timeout comment
The comment on `searchStatementTimeout` cited migration 134 as the one that
installed the extensions the search fast path needs. 134 is
`134_runtime_profile_add_qoder`, which widens a `protocol_family` CHECK
constraint and has nothing to do with search. The migration that runs
`CREATE EXTENSION IF NOT EXISTS pg_trgm` is 137.
The referenced index list is also extended to 137-142: 137 only installs
the extension, while the trigram indexes themselves are created by 138-142.
Co-authored-by: sail-cn <[email protected]>
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: sail-cn <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* feat(vcs): gate self-hosted Git providers to self-host deployments only (MUL-3772)
The Forgejo/Gitea/GitLab integration is intended for self-hosted Multica, where
Multica can reach a Git instance on the operator's own network. On the managed
multi-tenant cloud it adds an SSRF surface (connect validates a user-supplied
instance URL from the server) and would store third-party Git tokens for all
tenants under one key, while only serving the small subset of users whose
instance is publicly reachable. Product decision: offer it on self-host only.
- Add an explicit deployment switch MULTICA_VCS_INTEGRATION_ENABLED (default
off). Connect, rotate, and webhook now require BOTH the switch on AND a valid
MULTICA_VCS_SECRET_KEY — the switch is the product boundary, not key presence
alone. When off, connect/rotate return 404 and the webhook returns a bare 404
(no config leak), independent of the frontend.
- /api/config exposes vcs_integration_available (mirrors the switch, omitted
when false) so the Settings UI hides the whole "Git providers" section on
cloud instead of surfacing an operator-only "missing key" hint.
- docker-compose.selfhost.yml defaults the switch on; .env.example documents it.
- Docs (en/zh) lead with a callout: available on self-hosted Multica only, not
Multica Cloud, and clarify "self-hosted" means Multica itself, not just Git.
#5006 / #5883 stay in place — the schema and backend capability are retained;
this only gates availability. No cloud VCS connection can exist (connect always
required the key, which the cloud never set), so nothing needs migrating.
Verified: go build/vet + VCS/config handler tests on a fresh migrated DB
(incl. a new disabled-deployment 404 test); pnpm typecheck (core + views) and
the integrations-tab + core schema/config vitest suites pass.
Co-authored-by: multica-agent <[email protected]>
* fix(vcs): complete self-host integration gating (MUL-5138)
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* fix(web): resolve upstream URLs at runtime
* fix(web): keep unconfigured upstreams same-origin
* fix(web): restore dev-only localhost fallbacks for API and docs upstreams
`next dev` on a developer machine now falls back to the conventional
http://localhost:8080 backend (honoring BACKEND_PORT) and
http://localhost:4000 docs origin when nothing is configured, so a bare
`pnpm dev:web` keeps proxying out of the box. Builds and the runtime
proxy keep the strict resolvers, so prebuilt images still leave unset
upstreams unproxied.
Co-Authored-By: Claude Fable 5 <[email protected]>
---------
Co-authored-by: Naiyuan Qing <[email protected]>
Co-authored-by: Claude Fable 5 <[email protected]>
Closes#4237
Add a `postgres.external.enabled` toggle (default: false) so operators
can skip the built-in Deployment/PVC/Service and point Multica at any
externally managed PostgreSQL (CNPG, RDS, Cloud SQL, Neon, etc.).
When enabled:
- postgres Deployment, PVC, and ClusterIP Service are not rendered
- The hard-coded DATABASE_URL env var is omitted from the backend pod;
DATABASE_URL must be supplied in existingSecret and reaches the
container through the existing envFrom.secretRef mechanism
- POSTGRES_DB / POSTGRES_USER are omitted from the ConfigMap (unused
when DATABASE_URL is provided directly)
Also update the existingSecret creation example in values.yaml to show
both the built-in and external-postgres variants so users know to add
DATABASE_URL instead of POSTGRES_PASSWORD when switching modes.
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
cmd/migrate previously ran a check-then-apply loop on a *pgxpool.Pool
with no locking, so two backend pods starting at the same time (multi-
replica Deployment, scale-up, or a manual run overlapping with pod
startup) could both pass the EXISTS check on a pending migration and
race on the DDL or the schema_migrations INSERT, crashing the loser.
Take a single connection from the pool, hold a session-level
pg_advisory_lock for the entire migration loop, and release it on the
way out. We use the blocking variant so a late arriver queues behind
the current runner and then no-ops on the EXISTS checks instead of
crash-looping. The loop deliberately stays outside a transaction so
existing CREATE INDEX CONCURRENTLY migrations keep working.
Also refresh the values.yaml / backend.yaml comments next to
backend.replicas: the chart still ships replicas: 1 by default, but
that is now a recommendation (Recreate strategy, no leader split), not
a correctness requirement.
Refs https://github.com/multica-ai/multica/issues/3647
Co-authored-by: multica-agent <[email protected]>
Adds a value (default true for backward compatibility) that gates the
uploads PersistentVolumeClaim, the backend container's volumeMount, and
the pod-spec volume. Operators who serve uploads from S3 (S3_BUCKET set)
can now set backend.uploads.persistence.enabled=false to drop the PVC
entirely, removing the ReadWriteOnce Multi-Attach barrier on the storage
side for replicas > 1.
Also makes the PVC accessModes configurable (default [ReadWriteOnce]) so
operators with a ReadWriteMany-capable StorageClass can share the
uploads volume across replicas without object storage.
Documentation: values.yaml comments and the SELF_HOSTING.md resource
list are updated to describe the new toggle.
Refs: https://github.com/multica-ai/multica/issues/3646
Co-authored-by: multica-agent <[email protected]>
* feat(self-host): DISABLE_WORKSPACE_CREATION env var (MUL-2777, #3433)
When self-hosters set DISABLE_WORKSPACE_CREATION=true, POST /api/workspaces
returns 403 for every caller and the UI hides every "Create workspace"
affordance (sidebar, modal, /workspaces/new page, onboarding Step 2). This
closes the gap where ALLOW_SIGNUP=false still let any signed-in user open
an isolated workspace the platform admin couldn't see.
- server: new Config.DisableWorkspaceCreation, gate in CreateWorkspace,
workspace_creation_disabled in /api/config, Go tests.
- frontend: new workspaceCreationDisabled in configStore, hide sidebar
entry, swap NewWorkspacePage / CreateWorkspaceModal / onboarding
StepWorkspace to a "creation disabled, ask for invite" state when the
flag is on, EN + zh-Hans locale strings.
- ops: .env.example, docker-compose.selfhost, helm values + configmap,
SELF_HOSTING.md, SELF_HOSTING_ADVANCED.md, environment-variables docs
(EN + zh).
Co-authored-by: multica-agent <[email protected]>
* fix(onboarding): drive create path off workspaceCreationAllowed (#3433)
PR #3441 review: when DISABLE_WORKSPACE_CREATION=true and the user already
has a workspace, StepWorkspace still walked the resume copy (`headline_resume`
/ `lede_resume` mentioning "or start another") and `creatingActive` ignored
the flag, leaving a stale clickable create CTA possible if /api/config
arrived late.
Refactor StepWorkspace to derive a single `workspaceCreationAllowed`
boolean from the config store. It now drives:
- Initial `mode` state (defaults to "existing" when disabled + reusing so
the CTA is pre-armed for the only valid action).
- `creatingActive` so the footer CTA cannot fall back into the create
branch even mid-render.
- Eyebrow / headline / lede strings — adds
`creation_disabled_{eyebrow,headline,lede}_resume` (EN + zh-Hans) for
the disabled + reusing variant.
Tests: cover the three reachable shapes — flag off + no existing, flag on
+ no existing, flag on + existing.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
* Include k8s deployment instructions
* Use helm for deployment
* docs(self-host): add Helm / Kubernetes deployment to quickstart (en + zh)
* fix(helm): gate backend ExternalName alias behind a value
The unprefixed Service/backend in the chart is load-bearing, but as
written it limits the chart to one release per namespace and fails
helm install whenever a Service/backend already exists in the
namespace (without --take-ownership).
Gate the alias behind frontend.compatibility.backendAlias (default
true, so existing installs are unchanged). Operators running a web
image with a patched REMOTE_API_URL can set it to false to drop the
Service entirely. Document the one-release-per-namespace constraint
and the opt-out in values.yaml and the SELF_HOSTING.md Kubernetes
section.
Addresses review item #1 on PR #2377.
* fix(helm): add backend startupProbe so cold installs survive migrations
The entrypoint runs `./migrate up` before serving traffic. On a cold
cluster (Postgres still coming up) this can take minutes, during which
the livenessProbe (initialDelaySeconds 30 / periodSeconds 30) trips and
restarts the pod 1-2 times.
Add a startupProbe on /healthz (failureThreshold 30, periodSeconds 10,
~5 min budget). Kubernetes disables liveness/readiness until it passes,
so migrations finish without the pod being killed, and the aggressive
livenessProbe is untouched for steady-state. Update the SELF_HOSTING.md
install step, which no longer expects 1-2 restarts.
Addresses review item #2 on PR #2377.
* fix(helm): roll backend pods on config/secret change via checksum annotations
envFrom does not watch the referenced ConfigMap/Secret, and helm
upgrade alone does not change the pod template hash, so editing
values.yaml + `helm upgrade` left the old backend pods running stale
config.
Add checksum/config (hash of the rendered configmap.yaml) and
checksum/secret (hash of the live existingSecret via lookup, since it
is created out-of-band and has no chart template) to the backend pod
template. Config edits now actually re-roll the backend on upgrade,
and Secret rotations do too. lookup is empty under
`helm template`/`--dry-run`; that placeholder is harmless and
documented inline.
Addresses review item #3 on PR #2377.
* docs(self-host): sync quickstart with new startupProbe behavior
SELF_HOSTING.md was updated to reflect that the backend now stays
Running but not Ready while Postgres comes up (startupProbe absorbs
it, so no restart), but the EN/ZH quickstart docs still described the
pre-startupProbe behavior of "may restart 1-2 times". Bring them in
line.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Bohan Jiang <[email protected]>
Co-authored-by: multica-agent <[email protected]>