diff --git a/apps/nextjs/src/app/[locale]/layout.tsx b/apps/nextjs/src/app/[locale]/layout.tsx
index e7db1f0e3..493ccdc9c 100644
--- a/apps/nextjs/src/app/[locale]/layout.tsx
+++ b/apps/nextjs/src/app/[locale]/layout.tsx
@@ -14,7 +14,6 @@ import type { DayOfWeek } from "@mantine/dates";
import { NextIntlClientProvider } from "next-intl";
import { api } from "@homarr/api/server";
-import { getFeatureFlagsAsync } from "@homarr/api/features";
import { env } from "@homarr/auth/env";
import { auth } from "@homarr/auth/next";
import { db } from "@homarr/db";
@@ -87,7 +86,14 @@ export default async function Layout(props: {
const session = await auth();
const user = session ? await api.user.getById({ userId: session.user.id }).catch(() => null) : null;
const serverSettings = await getServerSettingsAsync(db);
- const features = await getFeatureFlagsAsync(db);
+ // Resolved on the server so the assistant runtime chunk is never referenced by a page when the
+ // feature is off. Reuses the same availability check the assistant management page invalidates.
+ const assistantEnabled = session
+ ? await api.assistant
+ .getAvailability()
+ .then((availability) => availability.enabled)
+ .catch(() => false)
+ : false;
const colorScheme = await getCurrentColorSchemeAsync();
const direction = isLocaleRTL((await props.params).locale) ? "rtl" : "ltr";
@@ -126,7 +132,7 @@ export default async function Layout(props: {
(innerProps) => ,
(innerProps) => ,
(innerProps) => ,
- (innerProps) => ,
+ (innerProps) => ,
]);
const { locale } = await props.params;
diff --git a/apps/nextjs/src/app/api/features/route.ts b/apps/nextjs/src/app/api/features/route.ts
deleted file mode 100644
index 06d33f73b..000000000
--- a/apps/nextjs/src/app/api/features/route.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { getFeatureFlagsAsync } from "@homarr/api/features";
-import { auth } from "@homarr/auth/next";
-import { db } from "@homarr/db";
-
-/**
- * Reports which optional features are switched on for this instance.
- *
- * Kept deliberately small: booleans only, no configuration detail, so it stays cheap to call and
- * safe to hand to the client. It is the single place to look when deciding whether a feature's
- * client code needs to be loaded at all.
- *
- * Requires a session because whether an instance runs an AI assistant is not something an
- * anonymous visitor needs to know.
- */
-export async function GET() {
- const session = await auth();
- if (!session) {
- return Response.json({ error: "Unauthorized" }, { status: 401 });
- }
-
- const features = await getFeatureFlagsAsync(db);
-
- return Response.json({ features }, { headers: { "Cache-Control": "private, max-age=60" } });
-}
diff --git a/packages/api/package.json b/packages/api/package.json
index 14c833eeb..94fb1a00d 100644
--- a/packages/api/package.json
+++ b/packages/api/package.json
@@ -15,7 +15,6 @@
"./shared": "./src/shared.ts",
"./open-api": "./src/open-api.ts",
"./assistant": "./src/assistant.ts",
- "./features": "./src/features.ts",
"./custom-widget/display-data": "./src/router/custom-widget/display-data.ts",
"./mcp": "./src/mcp.ts"
},
diff --git a/packages/api/src/features.ts b/packages/api/src/features.ts
deleted file mode 100644
index 9c100b3b3..000000000
--- a/packages/api/src/features.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { eq } from "@homarr/db";
-import type { Database } from "@homarr/db";
-import { assistantConfigurations } from "@homarr/db/schema";
-import { assistantProviderRequiresApiKey } from "@homarr/definitions";
-
-/**
- * Instance-level feature switches. These say whether a feature is turned on for the whole Homarr
- * instance, never whether the current user may use it - permission checks stay on the individual
- * procedures.
- *
- * The point of collecting them in one place is that the server can decide, before rendering,
- * whether a feature's client bundle needs to exist on the page at all. A feature that is off must
- * not cost the browser anything.
- */
-export interface FeatureFlags {
- assistant: boolean;
-}
-
-export const getFeatureFlagsAsync = async (db: Database): Promise => {
- const assistantConfiguration = await db.query.assistantConfigurations.findFirst({
- where: eq(assistantConfigurations.id, "default"),
- columns: { enabled: true, provider: true, modelId: true, encryptedApiKey: true },
- });
-
- const requiresApiKey = assistantConfiguration
- ? assistantProviderRequiresApiKey(assistantConfiguration.provider)
- : false;
-
- return {
- assistant: Boolean(
- assistantConfiguration?.enabled &&
- assistantConfiguration.modelId &&
- (!requiresApiKey || assistantConfiguration.encryptedApiKey),
- ),
- };
-};
diff --git a/packages/api/src/test/features.spec.ts b/packages/api/src/test/features.spec.ts
deleted file mode 100644
index df0d4f11f..000000000
--- a/packages/api/src/test/features.spec.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { describe, expect, test } from "vitest";
-
-import type { Database } from "@homarr/db";
-import { assistantConfigurations } from "@homarr/db/schema";
-import { createDb } from "@homarr/db/test";
-
-import { getFeatureFlagsAsync } from "../features";
-
-const seedConfigurationAsync = async (db: Database, values: Partial) => {
- await db.insert(assistantConfigurations).values({ id: "default", ...values });
-};
-
-describe("getFeatureFlagsAsync", () => {
- test("reports the assistant as off when it was never configured", async () => {
- const db = createDb() as unknown as Database;
-
- await expect(getFeatureFlagsAsync(db)).resolves.toEqual({ assistant: false });
- });
-
- test("reports the assistant as off while it is configured but not switched on", async () => {
- const db = createDb() as unknown as Database;
- await seedConfigurationAsync(db, {
- enabled: false,
- provider: "openrouter",
- modelId: "some/model",
- encryptedApiKey: "a.b",
- });
-
- await expect(getFeatureFlagsAsync(db)).resolves.toEqual({ assistant: false });
- });
-
- test("reports the assistant as off when a required api key is missing", async () => {
- const db = createDb() as unknown as Database;
- await seedConfigurationAsync(db, { enabled: true, provider: "openrouter", modelId: "some/model" });
-
- await expect(getFeatureFlagsAsync(db)).resolves.toEqual({ assistant: false });
- });
-
- test("reports the assistant as off when no model was picked", async () => {
- const db = createDb() as unknown as Database;
- await seedConfigurationAsync(db, { enabled: true, provider: "openrouter", encryptedApiKey: "a.b" });
-
- await expect(getFeatureFlagsAsync(db)).resolves.toEqual({ assistant: false });
- });
-
- test("reports the assistant as on once it is fully configured", async () => {
- const db = createDb() as unknown as Database;
- await seedConfigurationAsync(db, {
- enabled: true,
- provider: "openrouter",
- modelId: "some/model",
- encryptedApiKey: "a.b",
- });
-
- await expect(getFeatureFlagsAsync(db)).resolves.toEqual({ assistant: true });
- });
-
- test("does not require an api key for providers that run without one", async () => {
- const db = createDb() as unknown as Database;
- await seedConfigurationAsync(db, { enabled: true, provider: "ollama", modelId: "llama3" });
-
- await expect(getFeatureFlagsAsync(db)).resolves.toEqual({ assistant: true });
- });
-});