demo and password demo
+
+
+ Live Demo 🎮
+ •
Install 💻
•
diff --git a/packages/api/src/env.ts b/packages/api/src/env.ts
index 7cc81cc57..ba26a264a 100644
--- a/packages/api/src/env.ts
+++ b/packages/api/src/env.ts
@@ -1,12 +1,14 @@
import { z } from "zod/v4";
-import { createEnv } from "@homarr/core/infrastructure/env";
+import { createBooleanSchema, createEnv } from "@homarr/core/infrastructure/env";
export const env = createEnv({
server: {
KUBERNETES_SERVICE_ACCOUNT_NAME: z.string().optional(),
+ DEMO_MODE: createBooleanSchema(false),
},
runtimeEnv: {
KUBERNETES_SERVICE_ACCOUNT_NAME: process.env.KUBERNETES_SERVICE_ACCOUNT_NAME,
+ DEMO_MODE: process.env.DEMO_MODE,
},
});
diff --git a/packages/api/src/middlewares/docker.ts b/packages/api/src/middlewares/docker.ts
index 5a317b95a..e4f04f192 100644
--- a/packages/api/src/middlewares/docker.ts
+++ b/packages/api/src/middlewares/docker.ts
@@ -2,11 +2,11 @@ import { TRPCError } from "@trpc/server";
import { env } from "@homarr/docker/env";
-import { publicProcedure } from "../trpc";
+import { isDemoMode, publicProcedure } from "../trpc";
export const dockerMiddleware = () => {
return publicProcedure.use(async ({ next }) => {
- if (env.ENABLE_DOCKER) {
+ if (env.ENABLE_DOCKER || isDemoMode) {
return await next();
}
throw new TRPCError({
diff --git a/packages/api/src/router/info.ts b/packages/api/src/router/info.ts
index 3c205963c..8befca412 100644
--- a/packages/api/src/router/info.ts
+++ b/packages/api/src/router/info.ts
@@ -1,7 +1,7 @@
import z from "zod/v4";
import packageJson from "../../../../package.json";
-import { createTRPCRouter, protectedProcedure } from "../trpc";
+import { createTRPCRouter, isDemoMode, publicProcedure, protectedProcedure } from "../trpc";
export const infoRouter = createTRPCRouter({
getInfo: protectedProcedure
@@ -13,4 +13,5 @@ export const infoRouter = createTRPCRouter({
version: packageJson.version,
};
}),
+ isDemoMode: publicProcedure.query(() => isDemoMode),
});
diff --git a/packages/api/src/trpc.ts b/packages/api/src/trpc.ts
index bedd60f3d..eb4da06c2 100644
--- a/packages/api/src/trpc.ts
+++ b/packages/api/src/trpc.ts
@@ -18,6 +18,7 @@ import { createLogger } from "@homarr/core/infrastructure/logs";
import { db } from "@homarr/db";
import type { GroupPermissionKey, OnboardingStep } from "@homarr/definitions";
+import { env } from "./env";
import { getOnboardingOrFallbackAsync } from "./router/onboard/onboard-queries";
const logger = createLogger({ module: "trpc" });
@@ -87,6 +88,20 @@ export const createCallerFactory = t.createCallerFactory;
*/
export const createTRPCRouter = t.router;
+export const isDemoMode = env.DEMO_MODE;
+
+const enforceDemoModeReadOnly = t.middleware(({ ctx, next, type }) => {
+ if (env.DEMO_MODE && type === "mutation") {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "Mutations are disabled in demo mode",
+ });
+ }
+ return next({ ctx });
+});
+
+const baseProcedure = env.DEMO_MODE ? t.procedure.use(enforceDemoModeReadOnly) : t.procedure;
+
/**
* Public (unauthed) procedure
*
@@ -94,19 +109,14 @@ export const createTRPCRouter = t.router;
* tRPC API. It does not guarantee that a user querying is authorized, but you
* can still access user session data if they are logged in
*/
-export const publicProcedure = t.procedure;
+export const publicProcedure = baseProcedure;
-/**
- * Reusable middleware that enforces users are logged in before running the
- * procedure
- */
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
- // infers the `session` as non-nullable
session: { ...ctx.session, user: ctx.session.user },
},
});
@@ -121,7 +131,7 @@ const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
*
* @see https://trpc.io/docs/procedures
*/
-export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);
+export const protectedProcedure = baseProcedure.use(enforceUserIsAuthed);
/**
* Procedure that requires a specific permission
diff --git a/packages/db/migrations/seed.ts b/packages/db/migrations/seed.ts
index a2dec94af..0a00e9674 100644
--- a/packages/db/migrations/seed.ts
+++ b/packages/db/migrations/seed.ts
@@ -1,3 +1,5 @@
+import SuperJSON from "superjson";
+
import { createId, objectKeys } from "@homarr/common";
import {
createDocumentationLink,
@@ -8,6 +10,7 @@ import {
integrationDefs,
integrationKinds,
} from "@homarr/definitions";
+import type { WidgetKind } from "@homarr/definitions";
import { defaultServerSettings, defaultServerSettingsKeys } from "@homarr/server-settings";
import type { Database } from "..";
@@ -18,12 +21,17 @@ import {
insertServerSettingByKeyAsync,
updateServerSettingByKeyAsync,
} from "../queries/server-setting";
+
import {
+ apps,
boards,
groupMembers,
groupPermissions,
groups,
+ integrationItems,
integrations,
+ itemLayouts,
+ items,
layouts,
onboarding,
searchEngines,
@@ -39,7 +47,7 @@ export const seedDataAsync = async (db: Database) => {
await seedDefaultSearchEnginesAsync(db);
await seedDefaultIntegrationsAsync(db);
- if (process.env.DEMO_MODE === "true") {
+ if (["1", "yes", "t", "true"].includes((process.env.DEMO_MODE ?? "").toLowerCase())) {
await seedDemoUserAsync(db);
}
};
@@ -209,6 +217,134 @@ const seedDefaultIntegrationsAsync = async (db: Database) => {
console.log(`Created ${createdCount} default integrations through seeding process`);
};
+interface DemoWidget {
+ kind: WidgetKind;
+ width: number;
+ height: number;
+ needsIntegration: boolean;
+ options?: Record