feat(demo): allow mutations in demo mode via DEMO_READ_ONLY flag (#6224)

This commit is contained in:
Thomas authored and GitHub committed 2026-07-07 10:01:36 +02:00
1 parent d067261171
commit fc2c251557
5 files changed
+25 -9

No files matched your search

+7
View File
@@ -60,3 +60,10 @@ TURBO_TELEMETRY_DISABLED=1
# Enable mock integration
UNSAFE_ENABLE_MOCK_INTEGRATION=true
# Enable demo mode (seeds a demo user "demo"/"demo", a sample board and a mock integration)
# DEMO_MODE=true
# Whether demo mode is read-only. Defaults to true (the public demo blocks all mutations).
# Set to false for writable preview deployments so the demo user can modify the dashboard.
# DEMO_READ_ONLY=false
@@ -45,7 +45,9 @@ export const BoardContentHeaderActions = () => {
const [isEditMode] = useEditMode();
const board = useRequiredBoard();
const { hasChangeAccess } = useBoardPermissions(board);
const { data: demoMode, isLoading } = clientApi.info.isDemoMode.useQuery();
// Fall back to read-only if the query has no data (e.g. errored) so we never expose
// edit/save/settings UI that would then fail server-side.
const { data: demoReadOnly = true, isLoading } = clientApi.info.isDemoReadOnly.useQuery();
if (!hasChangeAccess || isLoading) {
return <SelectBoardsMenu />;
@@ -55,9 +57,9 @@ export const BoardContentHeaderActions = () => {
<>
{isEditMode && <AddMenu />}
<EditModeMenu demoMode={demoMode ?? false} />
<EditModeMenu demoReadOnly={demoReadOnly} />
{!demoMode && (
{!demoReadOnly && (
<OnboardingTour.Target id="board-settings">
<HeaderButton href={`/boards/${board.name}/settings`}>
<IconSettings stroke={1.5} />
@@ -158,7 +160,7 @@ const AddMenu = () => {
);
};
const EditModeMenu = ({ demoMode }: { demoMode: boolean }) => {
const EditModeMenu = ({ demoReadOnly }: { demoReadOnly: boolean }) => {
const [isEditMode, { open, close }] = useEditMode();
const board = useRequiredBoard();
const utils = clientApi.useUtils();
@@ -188,11 +190,11 @@ const EditModeMenu = ({ demoMode }: { demoMode: boolean }) => {
const toggle = useCallback(() => {
if (isEditMode) {
if (demoMode) return discardDemoChanges();
if (demoReadOnly) return discardDemoChanges();
return saveBoard(board);
}
open();
}, [board, isEditMode, demoMode, saveBoard, open, discardDemoChanges]);
}, [board, isEditMode, demoReadOnly, saveBoard, open, discardDemoChanges]);
useHotkeys([[hotkeys.toggleBoardEdit, toggle]]);
usePreventLeaveWithDirty(isEditMode);
+2
View File
@@ -6,9 +6,11 @@ export const env = createEnv({
server: {
KUBERNETES_SERVICE_ACCOUNT_NAME: z.string().optional(),
DEMO_MODE: createBooleanSchema(false),
DEMO_READ_ONLY: createBooleanSchema(true),
},
runtimeEnv: {
KUBERNETES_SERVICE_ACCOUNT_NAME: process.env.KUBERNETES_SERVICE_ACCOUNT_NAME,
DEMO_MODE: process.env.DEMO_MODE,
DEMO_READ_ONLY: process.env.DEMO_READ_ONLY,
},
});
+2 -1
View File
@@ -1,7 +1,7 @@
import z from "zod/v4";
import packageJson from "../../../../package.json";
import { createTRPCRouter, isDemoMode, publicProcedure, protectedProcedure } from "../trpc";
import { createTRPCRouter, isDemoMode, isDemoReadOnly, publicProcedure, protectedProcedure } from "../trpc";
export const infoRouter = createTRPCRouter({
getInfo: protectedProcedure
@@ -20,4 +20,5 @@ export const infoRouter = createTRPCRouter({
};
}),
isDemoMode: publicProcedure.query(() => isDemoMode),
isDemoReadOnly: publicProcedure.query(() => isDemoReadOnly),
});
+6 -2
View File
@@ -95,8 +95,12 @@ export const createTRPCRouter = t.router;
export const isDemoMode = env.DEMO_MODE;
// The public "real demo" (demo + mock integrations) stays read-only, while preview
// deployments run with DEMO_MODE + DEMO_READ_ONLY=false to allow mutations.
export const isDemoReadOnly = env.DEMO_MODE && env.DEMO_READ_ONLY;
const enforceDemoModeReadOnly = t.middleware(({ ctx, next, type }) => {
if (env.DEMO_MODE && type === "mutation") {
if (isDemoReadOnly && type === "mutation") {
throw new TRPCError({
code: "FORBIDDEN",
message: "Mutations are disabled in demo mode",
@@ -105,7 +109,7 @@ const enforceDemoModeReadOnly = t.middleware(({ ctx, next, type }) => {
return next({ ctx });
});
const baseProcedure = env.DEMO_MODE ? t.procedure.use(enforceDemoModeReadOnly) : t.procedure;
const baseProcedure = isDemoReadOnly ? t.procedure.use(enforceDemoModeReadOnly) : t.procedure;
/**
* Public (unauthed) procedure