diff --git a/.env.example b/.env.example
index 9dbd6f5ae9..72672f23c7 100644
--- a/.env.example
+++ b/.env.example
@@ -39,8 +39,7 @@ HUB_ALLOW_GIT_INSIDE_WORK_TREE=1
# Directory of built user-interface files the hub serves at BASE_URL.
# Relative paths resolve against the hub's working directory (apps/hub
# under `bun run dev`), so ../web/dist is the web app's build output;
-# produce it with `bun run build` before starting the hub. Any directory
-# with an index.html works — ../hub/public is a minimal placeholder page.
+# produce it with `bun run build` before starting the hub.
HUB_STATIC_DIR=../web/dist
# The administrator identity `bun run dev` seeds for local development:
diff --git a/apps/hub/public/index.html b/apps/hub/public/index.html
deleted file mode 100644
index 71c5491ec2..0000000000
--- a/apps/hub/public/index.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
- Hub
-
-
-
- The hub is running. Its API is served under /api; point
- HUB_STATIC_DIR at an interface build to replace this page.
-
-
-
diff --git a/apps/hub/src/config.ts b/apps/hub/src/config.ts
index afee380a27..37f179ac83 100644
--- a/apps/hub/src/config.ts
+++ b/apps/hub/src/config.ts
@@ -119,7 +119,7 @@ const HubEnv = type({
"a filesystem directory for the hub's durable repo and asset state, e.g. .data/hub",
),
HUB_STATIC_DIR: type("string > 0").describe(
- "a directory of built user-interface files the hub serves, e.g. apps/hub/public",
+ "a directory of built user-interface files the hub serves, e.g. apps/web/dist",
),
"SIGNUP_RATE_LIMIT_WINDOW_SECONDS?": type(/^[1-9]\d*$/).describe(
"the per-IP sign-up rate-limit window, in seconds, e.g. 60",
diff --git a/apps/hub/test/config.test.ts b/apps/hub/test/config.test.ts
index 7a4495a2d9..4f4470c2c1 100644
--- a/apps/hub/test/config.test.ts
+++ b/apps/hub/test/config.test.ts
@@ -6,7 +6,7 @@ const validEnv = {
BASE_URL: "http://localhost:3000",
SESSION_SECRET: "insecure-dev-only-session-secret-0000",
HUB_DATA_DIR: ".data/hub",
- HUB_STATIC_DIR: "apps/hub/public",
+ HUB_STATIC_DIR: "apps/web/dist",
};
function readExpectingError(env: Record): string {
diff --git a/apps/hub/test/credential-cipher.test.ts b/apps/hub/test/credential-cipher.test.ts
index bb3db43d23..fecf374ace 100644
--- a/apps/hub/test/credential-cipher.test.ts
+++ b/apps/hub/test/credential-cipher.test.ts
@@ -16,7 +16,7 @@ const baseConfig: HubConfig = {
baseUrl: "http://localhost:3000",
sessionSecret: "insecure-test-only-session-secret-0000",
hubDataDir: ".data/hub",
- hubStaticDir: "apps/hub/public",
+ hubStaticDir: "apps/web/dist",
signupRateLimit: { windowSeconds: 60, max: 5 },
socialProviders: {},
allowPlaintextSecrets: false,
diff --git a/scripts/e2e/harness.ts b/scripts/e2e/harness.ts
index f66ffa9675..4a9763f285 100644
--- a/scripts/e2e/harness.ts
+++ b/scripts/e2e/harness.ts
@@ -262,14 +262,13 @@ export async function startHub(options: {
const baseUrl = `http://localhost:${options.port}`;
const app = spawnApp("hub", HUB_DIR, {
...osEnv(),
- // e2e hubs serve apps/hub/public and advertise BASE_URL as their
- // own listen address by default (signup is ungated in stock
- // composition, so no signup env is needed); a caller's
- // extraEnv can still override any of these (e.g. a real web build's
- // dist dir, or a public BASE_URL that differs from the actual listen
+ // e2e hubs point at the web build (absent for API-only suites, which
+ // makes non-/api paths 404) and advertise BASE_URL as their own
+ // listen address by default; a caller's extraEnv can override any of
+ // these (e.g. a public BASE_URL that differs from the actual listen
// port — see PORT below — for a browser-driven suite fronted by a
// dev-server proxy).
- HUB_STATIC_DIR: "public",
+ HUB_STATIC_DIR: "../web/dist",
BASE_URL: baseUrl,
// The routine scheduler's real production cadence is a 30s
// setInterval (routine-scheduler.ts); waiting that out for real on