Skip to content

Converge the dev stacks on one origin: empty VITE_SERVER_URL, proxy for the split host - #5765

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-5745-one-origin-dev-configs
Aug 23, 2026
Merged

Converge the dev stacks on one origin: empty VITE_SERVER_URL, proxy for the split host#5765
os-zhuang merged 1 commit into
mainfrom
claude/issue-5745-one-origin-dev-configs

Conversation

@os-zhuang

@os-zhuangos-zhuang commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Fixes#5745

The ruled prerequisite for #5702. Landing this alone is harmless; landing #5702 without it breaks the standard pnpm dev stack.

What changed

filebeforeafter
apps/console/.env.developmentVITE_SERVER_URL=http://localhost:3000empty
examples/console-starter/.env.developmentVITE_SERVER_URL=http://localhost:3000empty
examples/console-starter/.env.productionVITE_SERVER_URL=https://demo.objectstack.aiempty
examples/console-starter/vite.config.tsno server block at allone /api proxy stanza, no port pinned

apps/console/.env.production and apps/console/vite.config.ts are untouched.

The enumeration (this was the substance, not the three-line edit)

Full-repo scan, no node_modules present at scan time: 114 mentions of VITE_SERVER_URL, of which ~50 are executable read sites. Every one was traced to the path it actually builds — through prop-passing consumers (FlowRunner, createConsoleServerActionHandler, createObjectStackUploadAdapter, useEnvironmentEntitlements, createIdentityImportDataSource, MetadataClient, ObjectStackAdapter) and into @objectstack/client@17.1.0's own route table.

Class 1 — statically under /api (the overwhelming majority). Every fetch-building site: /api/v1/..., /api/data/..., /api/settings. The existing /api proxy covers all of them, because a Vite proxy key matches by prefix.

Class 2 — suffix supplied by metadata or by server data.${baseUrl}${resolvedTarget} (useConsoleActionRuntime.tsx:346, RecordDetailView.tsx:640), ${apiBase}${resolved} (MetadataTypeActions.tsx:151), storage download URLs (RecordApprovalsPanel.tsx:312, ApprovalsInboxPage.tsx:723, RecordAttachmentsPanel.tsx:363), ${apiBase}${url} / ${apiBase}${warmUrl} (CloudAiModelStatus.tsx:99, CloudOnboardingNext.tsx:125). Not statically bounded — this is exactly the class #5702 exists for. Every first-party default in it is /api/....

Class 3 — server-supplied route table.@objectstack/client's getRoute() prefers discoveryInfo.routes[key] over its built-in map. That built-in map is 100% /api/v1/* and its own comment says both discovery producers emit the conventional paths — but a server could advertise a route outside /api, and the proxy would not cover it. Named, not solved.

The counterexamples — reported, not worked around

The card's premise was that convergence is clean. It is, for every fetch. It is not universally clean, and both exceptions are navigations, not fetches:

  1. packages/core/src/actions/ActionRunner.ts:1343 promotes /api/, /_auth/ and /_account/ to ${apiBase}${url} and then does a full-page window.location.href. /_auth/ and /_account/ are not under /api and are covered by neither proxy. Reachability measured: 7 repo-wide hits, all CHANGELOG / test / doc prose — no first-party producer of such an action target exists. /_account/* is a sibling SPA mount (apps/console/src/utils/consoleBase.ts:37), and apps/console/src/App.tsx:7 records that it is being retired into the console SPA itself, so a same-origin /_account/... landing on the console is arguably the intended end state.

  2. packages/app-shell/src/environment/entitlements.ts:48DEFAULT_UPGRADE_URL = '/settings/billing', rendered by resolveCtaHref as ${apiBase}/settings/billing. Non-/api, and an anchor href that no proxy could help. Emptying improves this site: today it opens http://localhost:3000/settings/billing in a new tab (external: Boolean(base)); empty makes it a same-origin in-app route — which is already the shipped production behaviour, since apps/console/.env.production has been empty since 2026-05-24.

I did not widen apps/console/vite.config.ts (read-only under this card's fence). Reason it is not needed: the /api proxy already covers every fetch site, and the two non-/api paths are navigations with no first-party producer. If the maintainer wants /_auth + /_account proxied for symmetry, that is a one-stanza follow-up — flagged rather than taken.

.env.production — established, not guessed

The ruling said revisit, not change. What was established:

  • examples/console-starter is private: true, on the changeset ignore list, and referenced by no workflow, no vercel config, no deploy (controlled grep: apps/console hits 3+ workflows, console-starter hits none).
  • Git history (clone deepened from 70 to 4598 commits to make this a measurement rather than a boundary artifact): apps/console/.env.production carried the identical https://demo.objectstack.ai until c351c9604, 2026-05-24, "fix(console): default published SPA to same-origin (clear VITE_SERVER_URL)" — whose changeset reads "CORS-blocked auth/i18n/discovery calls were preventing the SPA from rendering when embedded in any host other than the demo deployment." That commit touched onlyapps/console/.env.production plus its changeset. The starter was left behind by it.
  • 10d2a5119 (the starter's apparent origin commit) is a pure rename with 0-byte diffs, so the value predates it as a shared sibling default.

Conclusion: not deliberate — it is the pre-convergence value that the 2026-05-24 same-origin fix did not reach. As a fork-ready scaffold its committed value is the production origin every fork inherits.

Port

console-starter stays on Vite's default 5173, deliberately: its README already documents pnpm dev # Vite; no server.port is set, so the default 5173, and pinning one would only collide with a real fork's choice. apps/console keeps 5180.

Evidence

Union re-run at final commit 645b024cb, tree clean.

Measured before/after of a relative action target, computed off the real env files (before read from git at the merge-base, after from the working tree) using the resolution rule copied verbatim from useConsoleActionRuntime.tsx:326+:346:

=== apps/console (page origin http://localhost:5180) ===
action target : /api/v1/data/lead/query
BEFORE resolves to : http://localhost:3000/api/v1/data/lead/query
BEFORE same-origin as page? : false
AFTER resolves to : /api/v1/data/lead/query
AFTER same-origin as page? : true
AFTER path covered by proxy?: true
=== examples/console-starter (page origin http://localhost:5173, Vite default) ===
BEFORE resolves to : http://localhost:3000/api/v1/data/lead/query same-origin: false
AFTER resolves to : /api/v1/data/lead/query same-origin: true
AFTER proxy keys in config : ["/api"] covered: true

End-to-end, real dev server against a recording stub backend — the starter's new stanza actually forwards:

curl http://localhost:39312/api/v1/data/lead/query => HTTP=200
body: {"ok":true,"sawPath":"/api/v1/data/lead/query"}
stub: HITS=[{"method":"GET","url":"/api/v1/data/lead/query","host":"localhost:39311"}]

Ablation (stanza removed by taking origin/main's config; mutation proved on disk by marker count — DEV_PROXY_TARGET 0 occurrences, control workspaceAliases still 2; vite reads vite.config.ts directly at startup so no build step is involved; restore leg proved by git status --porcelain empty). Predicted direction stated before running — hits go to zero while HTTP stays 200:

HTTP=200 <- unchanged, so status is a FALSE-GREEN signal
body: an HTML doctype line, i.e. the <- the SPA index.html fallback, not JSON
SPA index.html fallback page
stub: HITS=[] <- the discriminating signal

That is also the hazard in miniature: without the proxy, an empty VITE_SERVER_URL would return HTML with a 200 rather than failing loudly.

Gates, exit codes captured before any pipe, each line quoted from the gate's own verdict:

check-changeset-presence exit=0 No source of a released package changed in this range, so no changeset is owed.
check-changeset-no-major exit=0 No changeset declares a `major` bump.
check-control-bytes exit=0 OK (scanned 4804 tracked text file(s); skipped 85 binary).
check-doc-links exit=0 Links are valid across 13 scan roots.
check-lint-coverage exit=0 lint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).
check-type-check-coverage exit=0 45/46 via `type-check`, 0 known-broken; tests 41/41.
vitest (3 affected files) exit=0 Test Files 3 passed (3) / Tests 26 passed (26)

Tests chosen because they are the ones that actually read the changed files: committed-telemetry-endpoint.test.ts (enumerates every committed .env* via git ls-files and parses it from disk), vite-alias-closure.test.ts (reads console-starter/vite.config.ts), runtimeConfigBootDedup.test.ts (pins the %VITE_SERVER_URL% HTML substitution, including the empty case).

Lint narrowing is a measurement, not a skip: eslint itself was asked which of the 5 changed files it considers — 4 report as ignored, 1 is linted; --format json reports files linted: 1, errors: 0, warnings: 0; and eslint.config.js declares no projectService / parserOptions.project (grep exit 1, controlled by rules hitting 10x in the same file), so type-aware linting is off and this diff cannot move the verdict on any untouched file. Independently, check-lint-coverage reports 46/46 packages clean.

process.env in the starter's vite config is safe: tsc --showConfig reports its program as 2 files (src/App.tsx, src/main.tsx) and includes vite.config.ts: false, matching the __dirname already in that file; eslint does lint it and is clean.

Not run and not claimed: the repo-wide pnpm lint / pnpm test farms, which CI runs exactly once regardless; and the three gauges known to be broken in a worktree (check-eager-closure-budget, check-doc-snippet-types, check-published-dist-tooling). Build Docs should report a ~10s skip, since this diff touches neither apps/site/ nor content/ — that skip is not evidence the docs site builds.

Known gap this PR does not close

Doc drift, outside this card's write fence, filed as #5766examples/console-starter/README.md still tabulates the two old env values, apps/console/README.md:36 still says "defaults to http://localhost:3000", and apps/console/README.md:54 claims Vite proxies /api/*and/_account/* when vite.config.ts:709-711 proxies only /api (that last one is pre-existing). Say the word and I will fold the doc half in here.

Generated by Claude Code

Empty `VITE_SERVER_URL` in both dev env files and give `console-starter`
the `/api` dev proxy it never had, so a relative `type: 'api'` action
target resolves SAME-ORIGIN and the Vite proxy makes the split-host hop.
Both dev envs pointed `VITE_SERVER_URL` at `http://localhost:3000` while
the page was served from `:5180` (console) and `:5173` (starter). Every
client in these apps coalesces an unset value to '' and then builds a
relative `/api/...` URL, so an empty value routes through the dev proxy
instead of off-origin.
This is the ruled prerequisite for the `sameOriginOnly` action-runtime
default: under that default a non-empty `VITE_SERVER_URL` makes every
relative-target `type: 'api'` action resolve cross-origin and be fetched
bare -- no Authorization, no X-Tenant-ID, no Accept-Language -- i.e. a
401 for the standard `pnpm dev` stack.
`examples/console-starter/.env.production` is emptied too. Its committed
`https://demo.objectstack.ai` is the same pre-convergence value the
sibling console carried until c351c96 ("default published SPA to
same-origin") cleared it there for CORS-blank-page reasons and touched
only `apps/console/.env.production`; the starter was left behind by that
commit. As a fork-ready scaffold, its committed value is the production
origin every fork inherits.
No port is pinned for `console-starter` -- its README documents the
example on Vite's default 5173.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3917.7 KB3990.2 KB
Main entry chunk (gzip)152.5 KB350 KB
Entry fileindex-m2frlNnO.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)10.04KB3.72KB
app-shell (runtime-config.js)12.80KB4.47KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)16.66KB6.35KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)33.99KB8.57KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)510.39KB114.67KB
core (index.js)4.92KB1.97KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)164.55KB45.67KB
fields (index.js)238.40KB59.89KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.65KB18.32KB
plugin-chatbot (index.js)181.41KB43.22KB
plugin-dashboard (index.js)128.41KB32.95KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)242.34KB60.98KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.63KB30.64KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.80KB27.20KB
plugin-map (index.js)20.06KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.61KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)44.39KB14.99KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)6.92KB2.40KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.88KB1.85KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)3.40KB1.68KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-zhuang
os-zhuang marked this pull request as ready for review August 23, 2026 05:46
@os-zhuang
os-zhuang added this pull request to the merge queueAug 23, 2026
Merged via the queue into main with commit bc21c70Aug 23, 2026
23 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5745-one-origin-dev-configs branch August 23, 2026 05:46
os-zhuang pushed a commit that referenced this pull request Aug 23, 2026
…n READMEs
PR #5765 emptied VITE_SERVER_URL in both dev env files (same-origin by
default, per the 2026-08-23 ruling on #5702). Three doc-prose items had
drifted from that:
1. apps/console/README.md's Quick Start and Development Mode sections still
said VITE_SERVER_URL defaults to http://localhost:3000 — it now ships
empty, with the Vite dev proxy (DEV_PROXY_TARGET, defaulting to
http://localhost:3000) forwarding /api/* to the backend.
2. examples/console-starter/README.md's Backend table still tabulated the
old non-empty .env.development/.env.production values — both now ship
empty (same-origin), documented with the same DEV_PROXY_TARGET recipe.
3. apps/console/README.md claimed the dev proxy covers /api/* and
/_account/* — vite.config.ts only ever proxied /api. Per triage
discretion on #5766 (zero first-party producers of /_auth/ or /_account/
action targets on origin/main, and /_account is recorded as retired into
the console SPA), this fixes the doc rather than widening the proxy:
states /api/* only and notes /_auth/* and /_account/* are not proxied.
Fixes#5766
Filed #5802 (out of scope) for the same stale-default drift in
content/docs/guide/console.md, which sits outside this card's scope.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuPCi56cnGyykygi3z9w4m
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 23, 2026
…n READMEs (objectstack-ai#5803)
PR objectstack-ai#5765 emptied VITE_SERVER_URL in both dev env files (same-origin by
default, per the 2026-08-23 ruling on objectstack-ai#5702). Three doc-prose items had
drifted from that:
1. apps/console/README.md's Quick Start and Development Mode sections still
said VITE_SERVER_URL defaults to http://localhost:3000 — it now ships
empty, with the Vite dev proxy (DEV_PROXY_TARGET, defaulting to
http://localhost:3000) forwarding /api/* to the backend.
2. examples/console-starter/README.md's Backend table still tabulated the
old non-empty .env.development/.env.production values — both now ship
empty (same-origin), documented with the same DEV_PROXY_TARGET recipe.
3. apps/console/README.md claimed the dev proxy covers /api/* and
/_account/* — vite.config.ts only ever proxied /api. Per triage
discretion on objectstack-ai#5766 (zero first-party producers of /_auth/ or /_account/
action targets on origin/main, and /_account is recorded as retired into
the console SPA), this fixes the doc rather than widening the proxy:
states /api/* only and notes /_auth/* and /_account/* are not proxied.
Fixesobjectstack-ai#5766
Filed objectstack-ai#5802 (out of scope) for the same stale-default drift in
content/docs/guide/console.md, which sits outside this card's scope.
Claude-Session: https://claude.ai/code/session_01EuPCi56cnGyykygi3z9w4m
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-zhuang@claude