diff --git a/src/__tests__/manager-auth-route.test.ts b/src/__tests__/manager-auth-route.test.ts
new file mode 100644
index 0000000..90ffa7f
--- /dev/null
+++ b/src/__tests__/manager-auth-route.test.ts
@@ -0,0 +1,26 @@
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+// The Manager must not load the business Auth.js module even if a stale
+// client requests `/api/auth/session`.
+vi.mock("@/auth", () => {
+ throw new Error("Business Auth.js must not load in Manager mode.");
+});
+
+import { GET } from "@/app/api/auth/[...nextauth]/route";
+
+describe("Manager business-auth boundary", () => {
+ const originalManagerMode = process.env.MANAGER_MODE;
+
+ afterEach(() => {
+ if (originalManagerMode === undefined) delete process.env.MANAGER_MODE;
+ else process.env.MANAGER_MODE = originalManagerMode;
+ });
+
+ it("returns 404 without importing business Auth.js", async () => {
+ process.env.MANAGER_MODE = "true";
+
+ const response = await GET(new Request("https://manager.example.com/api/auth/session"));
+
+ expect(response.status).toBe(404);
+ });
+});
diff --git a/src/__tests__/manager-framework-routes.test.ts b/src/__tests__/manager-framework-routes.test.ts
new file mode 100644
index 0000000..941019b
--- /dev/null
+++ b/src/__tests__/manager-framework-routes.test.ts
@@ -0,0 +1,39 @@
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+vi.mock("@/lib/db/prisma", () => {
+ throw new Error("Business Prisma must not load in Manager framework routes.");
+});
+
+vi.mock("@/lib/business/branding", () => {
+ throw new Error("Business branding must not load in Manager framework routes.");
+});
+
+import { GET as health } from "@/app/api/health/route";
+import manifest from "@/app/manifest";
+
+describe("Manager framework routes", () => {
+ const originalManagerMode = process.env.MANAGER_MODE;
+
+ afterEach(() => {
+ if (originalManagerMode === undefined) delete process.env.MANAGER_MODE;
+ else process.env.MANAGER_MODE = originalManagerMode;
+ });
+
+ it("serves health without Prisma", async () => {
+ process.env.MANAGER_MODE = "true";
+
+ const response = await health();
+
+ expect(response.status).toBe(200);
+ expect(await response.json()).toMatchObject({ status: "healthy", deployment: "manager" });
+ });
+
+ it("serves Manager manifest without business branding", async () => {
+ process.env.MANAGER_MODE = "true";
+
+ const result = await manifest();
+
+ expect(result.name).toBe("NextChair Manager");
+ expect(result.start_url).toBe("/manager");
+ });
+});
diff --git a/src/__tests__/proxy-manager-mode.test.ts b/src/__tests__/proxy-manager-mode.test.ts
index dbe8e5e..8e4f4fe 100644
--- a/src/__tests__/proxy-manager-mode.test.ts
+++ b/src/__tests__/proxy-manager-mode.test.ts
@@ -26,16 +26,26 @@ describe("proxy deployment separation", () => {
isInstalledForRequest.mockReset();
});
- it("serves the Manager root without loading or checking business installation state", async () => {
+ it("routes the Manager root before loading or checking business installation state", async () => {
process.env.MANAGER_MODE = "true";
const response = await handleProxyRequest(request("/"));
- expect(response.status).toBe(200);
+ expect(response.status).toBe(307);
+ expect(response.headers.get("location")).toBe("https://manager.example.com/manager");
expect(response.headers.get("content-security-policy")).toContain("default-src 'self'");
expect(isInstalledForRequest).not.toHaveBeenCalled();
});
+ it("serves Manager routes without business installation state", async () => {
+ process.env.MANAGER_MODE = "true";
+
+ const response = await handleProxyRequest(request("/manager"));
+
+ expect(response.status).toBe(200);
+ expect(isInstalledForRequest).not.toHaveBeenCalled();
+ });
+
it("checks installation state for a normal business request", async () => {
process.env.MANAGER_MODE = "false";
isInstalledForRequest.mockResolvedValue(false);
diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts
index 86c9f3d..04f79dc 100644
--- a/src/app/api/auth/[...nextauth]/route.ts
+++ b/src/app/api/auth/[...nextauth]/route.ts
@@ -1,3 +1,20 @@
-import { handlers } from "@/auth";
+/**
+ * Business Auth.js is intentionally unavailable in the standalone Manager
+ * deployment. Keeping this check before the dynamic import prevents a stale
+ * SessionProvider or direct request from initialising Prisma there.
+ */
+function managerAuthDisabled() {
+ return process.env.MANAGER_MODE === "true";
+}
-export const { GET, POST } = handlers;
+export async function GET(request: Request) {
+ if (managerAuthDisabled()) return new Response(null, { status: 404 });
+ const { handlers } = await import("@/auth");
+ return handlers.GET(request);
+}
+
+export async function POST(request: Request) {
+ if (managerAuthDisabled()) return new Response(null, { status: 404 });
+ const { handlers } = await import("@/auth");
+ return handlers.POST(request);
+}
diff --git a/src/app/api/cron/process-notifications/route.ts b/src/app/api/cron/process-notifications/route.ts
index 651b3fc..dbad571 100644
--- a/src/app/api/cron/process-notifications/route.ts
+++ b/src/app/api/cron/process-notifications/route.ts
@@ -1,10 +1,5 @@
import { NextResponse } from "next/server";
-import { prisma } from "@/lib/db/prisma";
-import { isInstalledForRequest } from "@/lib/installation/installation-state";
import { secretsMatch } from "@/lib/security/secrets-match";
-import { expireOverdueRewards } from "@/services/loyalty/loyalty.service";
-import { getEmailProvider, getPushProvider } from "@/services/notifications/providers";
-import { processDueNotificationDeliveries } from "@/services/notifications/reminder-worker";
/**
* The external trigger point for the background notification processor —
@@ -30,6 +25,8 @@ import { processDueNotificationDeliveries } from "@/services/notifications/remin
* expected secret.
*/
export async function POST(request: Request) {
+ if (process.env.MANAGER_MODE === "true") return NextResponse.json({ error: "Not available on Manager." }, { status: 404 });
+
const secret = process.env.CRON_SECRET;
if (!secret) {
return NextResponse.json({ error: "CRON_SECRET is not configured on this deployment." }, { status: 503 });
@@ -44,10 +41,18 @@ export async function POST(request: Request) {
// src/lib/installation/installation-state.ts. A 200 with a no-op result
// (not an error) so an external scheduler hitting this on a fixed
// interval before day-one setup doesn't page anyone.
+ const { isInstalledForRequest } = await import("@/lib/installation/installation-state");
if (!(await isInstalledForRequest())) {
return NextResponse.json({ skipped: "not_installed" });
}
+ const [{ prisma }, { expireOverdueRewards }, { getEmailProvider, getPushProvider }, { processDueNotificationDeliveries }] = await Promise.all([
+ import("@/lib/db/prisma"),
+ import("@/services/loyalty/loyalty.service"),
+ import("@/services/notifications/providers"),
+ import("@/services/notifications/reminder-worker"),
+ ]);
+
const result = await processDueNotificationDeliveries(prisma, {
emailProvider: getEmailProvider(),
pushProvider: getPushProvider(),
diff --git a/src/app/api/cron/sync-google-reviews/route.ts b/src/app/api/cron/sync-google-reviews/route.ts
index 5cfec13..64dd29c 100644
--- a/src/app/api/cron/sync-google-reviews/route.ts
+++ b/src/app/api/cron/sync-google-reviews/route.ts
@@ -1,8 +1,5 @@
import { NextResponse } from "next/server";
-import { prisma } from "@/lib/db/prisma";
-import { isInstalledForRequest } from "@/lib/installation/installation-state";
import { secretsMatch } from "@/lib/security/secrets-match";
-import { syncGoogleReviewsForAllBusinesses } from "@/services/reviews/review-sync.service";
/**
* External trigger for the Google Reviews sync — mirrors
@@ -19,6 +16,8 @@ import { syncGoogleReviewsForAllBusinesses } from "@/services/reviews/review-syn
* never duplicates anything.
*/
export async function POST(request: Request) {
+ if (process.env.MANAGER_MODE === "true") return NextResponse.json({ error: "Not available on Manager." }, { status: 404 });
+
const secret = process.env.CRON_SECRET;
if (!secret) {
return NextResponse.json({ error: "CRON_SECRET is not configured on this deployment." }, { status: 503 });
@@ -29,10 +28,15 @@ export async function POST(request: Request) {
return NextResponse.json({ error: "Unauthorized." }, { status: 401 });
}
+ const { isInstalledForRequest } = await import("@/lib/installation/installation-state");
if (!(await isInstalledForRequest())) {
return NextResponse.json({ skipped: "not_installed" });
}
+ const [{ prisma }, { syncGoogleReviewsForAllBusinesses }] = await Promise.all([
+ import("@/lib/db/prisma"),
+ import("@/services/reviews/review-sync.service"),
+ ]);
const results = await syncGoogleReviewsForAllBusinesses(prisma);
return NextResponse.json({ results });
}
diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts
index 665f9da..4910843 100644
--- a/src/app/api/health/route.ts
+++ b/src/app/api/health/route.ts
@@ -1,6 +1,4 @@
import { NextResponse } from "next/server";
-import { prisma } from "@/lib/db/prisma";
-import { getInstallationState } from "@/lib/installation/installation-state";
/**
* Docker/orchestrator health check — deliberately never requires
@@ -11,6 +9,14 @@ import { getInstallationState } from "@/lib/installation/installation-state";
* trace — just enough for a health probe to act on.
*/
export async function GET() {
+ if (process.env.MANAGER_MODE === "true") {
+ return NextResponse.json({ status: "healthy", deployment: "manager" });
+ }
+
+ const [{ prisma }, { getInstallationState }] = await Promise.all([
+ import("@/lib/db/prisma"),
+ import("@/lib/installation/installation-state"),
+ ]);
try {
await prisma.$queryRaw`SELECT 1`;
} catch {
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index c3a3219..dd761df 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,6 +1,6 @@
import type { Metadata, Viewport } from "next";
import { Inter } from "next/font/google";
-import { Providers } from "@/components/providers";
+import { ManagerProviders } from "@/components/manager-providers";
import "./globals.css";
const inter = Inter({
@@ -86,8 +86,17 @@ export default function RootLayout({
>
Skip to main content
- {children}
+ {process.env.MANAGER_MODE === "true" ? (
+ {children}
+ ) : (
+ {children}
+ )}