diff --git a/.env.example b/.env.example
index 46085685..f75a4461 100644
--- a/.env.example
+++ b/.env.example
@@ -31,3 +31,6 @@ OPENAI_BASE_URL=xxx
GOOGLE_CLIENT_ID=xxx
GOOGLE_CLIENT_SECRET=xxx
GOOGLE_HOSTED_DOMAIN=
+
+# The feature flags.
+FEATURE_AI_CHAT=true
diff --git a/src/routes/(main)/(protected)/+layout.server.ts b/src/routes/(main)/(protected)/+layout.server.ts
index 650dd707..6d4daaf4 100644
--- a/src/routes/(main)/(protected)/+layout.server.ts
+++ b/src/routes/(main)/(protected)/+layout.server.ts
@@ -1,5 +1,6 @@
import { error, redirect } from '@sveltejs/kit';
+import { env } from '$env/dynamic/private';
import { db, type UserFindUniqueArgs, type UserGetPayload } from '$lib/server/db';
import type { LayoutServerLoad } from './$types';
@@ -39,5 +40,8 @@ export const load: LayoutServerLoad = async (event) => {
username: user.name,
csrfToken: event.locals.session.csrfToken(),
onboarded,
+ featureFlags: {
+ aiChat: env.FEATURE_AI_CHAT === 'true',
+ },
};
};
diff --git a/src/routes/(main)/(protected)/+layout.svelte b/src/routes/(main)/(protected)/+layout.svelte
index b7e60e21..b070e658 100644
--- a/src/routes/(main)/(protected)/+layout.svelte
+++ b/src/routes/(main)/(protected)/+layout.svelte
@@ -198,7 +198,9 @@
/>
{/if}
-
+ {#if page.data.featureFlags.aiChat}
+
+ {/if}
{/if}
@@ -327,6 +329,8 @@
{/if}
-
+{#if page.data.featureFlags.aiChat}
+
+{/if}
diff --git a/src/routes/(main)/hooks.server.ts b/src/routes/(main)/hooks.server.ts
index 9228c00c..76003ce0 100644
--- a/src/routes/(main)/hooks.server.ts
+++ b/src/routes/(main)/hooks.server.ts
@@ -1,6 +1,7 @@
-import { type Handle, redirect } from '@sveltejs/kit';
+import { type Handle, json, redirect } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';
+import { env } from '$env/dynamic/private';
import { HOME_PATH, nanoid } from '$lib/helpers/index.js';
import { learnerAuth } from '$lib/server/auth/index.js';
import { logger } from '$lib/server/logger.js';
@@ -9,13 +10,25 @@ import { logger } from '$lib/server/logger.js';
* A handle that adds a request ID to the response headers and attaches a scoped logger to the
* event. Downstream handles are expected to use the scoped logger for logging.
*/
-const requestLoggingHandle: Handle = async ({ event, resolve }) => {
+const requestLoggingHandle: Handle = ({ event, resolve }) => {
const requestId = nanoid();
event.setHeaders({ 'X-Request-Id': requestId });
event.locals.logger = logger.child({ requestId });
- return await resolve(event);
+ return resolve(event);
+};
+
+/**
+ * A handle that gates API routes behind feature flags.
+ * Returns 403 for disabled features before any further processing.
+ */
+const featureFlagHandle: Handle = ({ event, resolve }) => {
+ if (event.url.pathname.startsWith('/api/messages') && env.FEATURE_AI_CHAT !== 'true') {
+ return json(null, { status: 403 });
+ }
+
+ return resolve(event);
};
/**
@@ -52,6 +65,7 @@ const routeProtectionHandle: Handle = async ({ event, resolve }) => {
export const handle: Handle = sequence(
requestLoggingHandle,
+ featureFlagHandle,
learnerAuth.handle,
routeProtectionHandle,
);