diff --git a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx
index 13ff2d6e4..e4af05376 100644
--- a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx
+++ b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx
@@ -190,7 +190,7 @@ export const SetupServer = ({ serverId, asButton = false }: Props) => {
Automatic process
diff --git a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-ssh-key.tsx b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-ssh-key.tsx
index a0312562e..5924bba50 100644
--- a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-ssh-key.tsx
+++ b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-ssh-key.tsx
@@ -172,7 +172,7 @@ export const CreateSSHKey = () => {
etc.)
diff --git a/apps/dokploy/components/layouts/dashboard-layout.tsx b/apps/dokploy/components/layouts/dashboard-layout.tsx
index 727649922..222f69b9e 100644
--- a/apps/dokploy/components/layouts/dashboard-layout.tsx
+++ b/apps/dokploy/components/layouts/dashboard-layout.tsx
@@ -11,20 +11,19 @@ interface Props {
export const DashboardLayout = ({ children }: Props) => {
const { data: haveRootAccess } = api.user.haveRootAccess.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
- const { data: isUserSubscribed } = api.settings.isUserSubscribed.useQuery(
- undefined,
- {
- enabled: isCloud === true,
- refetchOnWindowFocus: false,
- refetchOnMount: false,
- refetchOnReconnect: false,
- },
- );
+ const { data: currentPlan } = api.stripe.getCurrentPlan.useQuery(undefined, {
+ enabled: isCloud === true,
+ refetchOnWindowFocus: false,
+ refetchOnMount: false,
+ refetchOnReconnect: false,
+ });
+
+ const isChatEnabled = isCloud === true && currentPlan === "startup";
return (
<>
{children}
- {isCloud === true && isUserSubscribed === true && (
+ {isChatEnabled && (
<>
>
diff --git a/apps/dokploy/server/api/routers/stripe.ts b/apps/dokploy/server/api/routers/stripe.ts
index c910b1d58..109f2aac3 100644
--- a/apps/dokploy/server/api/routers/stripe.ts
+++ b/apps/dokploy/server/api/routers/stripe.ts
@@ -21,9 +21,51 @@ import {
STARTUP_PRODUCT_ID,
WEBSITE_URL,
} from "@/server/utils/stripe";
-import { adminProcedure, createTRPCRouter } from "../trpc";
+import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
export const stripeRouter = createTRPCRouter({
+ /** Returns the current billing plan for the user's organization. Used to gate features like chat (Startup only). */
+ getCurrentPlan: protectedProcedure.query(async ({ ctx }) => {
+ if (!IS_CLOUD) return null;
+ const owner = await findUserById(ctx.user.ownerId);
+ if (!owner?.stripeCustomerId) return null;
+
+ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
+ apiVersion: "2024-09-30.acacia",
+ });
+ const subscriptions = await stripe.subscriptions.list({
+ customer: owner.stripeCustomerId,
+ status: "active",
+ expand: ["data.items.data.price"],
+ });
+ const activeSub = subscriptions.data[0];
+ if (!activeSub) return null;
+
+ const priceIds = activeSub.items.data.map(
+ (item) => (item.price as Stripe.Price).id,
+ );
+ if (
+ priceIds.some(
+ (id) =>
+ id === STARTUP_BASE_PRICE_MONTHLY_ID ||
+ id === STARTUP_BASE_PRICE_ANNUAL_ID,
+ )
+ ) {
+ return "startup" as const;
+ }
+ if (
+ priceIds.some(
+ (id) => id === HOBBY_PRICE_MONTHLY_ID || id === HOBBY_PRICE_ANNUAL_ID,
+ )
+ ) {
+ return "hobby" as const;
+ }
+ if (priceIds.some((id) => LEGACY_PRICE_IDS.includes(id))) {
+ return "legacy" as const;
+ }
+ return null;
+ }),
+
getProducts: adminProcedure.query(async ({ ctx }) => {
const user = await findUserById(ctx.user.ownerId);
const stripeCustomerId = user.stripeCustomerId;
diff --git a/packages/server/src/setup/server-setup.ts b/packages/server/src/setup/server-setup.ts
index 32e5e4a7e..c15856dc3 100644
--- a/packages/server/src/setup/server-setup.ts
+++ b/packages/server/src/setup/server-setup.ts
@@ -281,17 +281,43 @@ const installRequirements = async (
.on("error", (err) => {
client.end();
if (err.level === "client-authentication") {
- onData?.(
- `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
- );
+ const technicalDetail = `Error: ${err.message} ${err.level}`;
+ const friendlyMessage = [
+ "",
+ "❌ Couldn't connect to your server — the SSH key was not accepted.",
+ "",
+ "This usually means the key doesn't match what's on the server, or the key format is invalid.",
+ "",
+ `Technical details: ${technicalDetail}`,
+ "",
+ "💡 Hints:",
+ " • Check that the SSH key you added in Dokploy is the same one installed on the server (e.g. in ~/.ssh/authorized_keys).",
+ " • Try generating a new SSH key in Dokploy and add only the public key to the server, then try again.",
+ " • Make sure to follow the instructions on the Setup Server Button on the SSH Keys tab",
+ ].join("\n");
+ onData?.(friendlyMessage);
reject(
new Error(
- `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
+ `Authentication failed: Invalid SSH private key. ${technicalDetail}`,
),
);
} else {
- onData?.(`SSH connection error: ${err.message} ${err.level}`);
- reject(new Error(`SSH connection error: ${err.message}`));
+ const technicalDetail = `${err.message} ${err.level ?? ""}`.trim();
+ const friendlyMessage = [
+ "",
+ "❌ Couldn't connect to your server.",
+ "",
+ "The connection failed before setup could run. Common causes: wrong IP or port, firewall blocking access, or the server is offline.",
+ "",
+ `Technical details: ${technicalDetail}`,
+ "",
+ "💡 Hints:",
+ " • Check that the server IP address and SSH port are correct and the server is powered on.",
+ " • If the server is in a private network, ensure Dokploy can reach it (VPN, firewall rules, or correct security groups).",
+ " • Make sure the SSH port (usually 22) is open and the SSH service is running on the server.",
+ ].join("\n");
+ onData?.(friendlyMessage);
+ reject(new Error(`SSH connection error: ${technicalDetail}`));
}
})
.connect({
diff --git a/packages/server/src/utils/process/execAsync.ts b/packages/server/src/utils/process/execAsync.ts
index cd0249000..d44e3ccaf 100644
--- a/packages/server/src/utils/process/execAsync.ts
+++ b/packages/server/src/utils/process/execAsync.ts
@@ -201,14 +201,31 @@ export const execAsyncRemote = async (
.on("error", (err) => {
conn.end();
if (err.level === "client-authentication") {
+ const technicalDetail = `Error: ${err.message} ${err.level}`;
+ const friendlyMessage = [
+ "",
+ "❌ Couldn't connect to your server — the SSH key was not accepted.",
+ "",
+ "This usually means the key doesn't match what's on the server, or the key format is invalid.",
+ "",
+ `Technical details: ${technicalDetail}`,
+ "",
+ "💡 Hints:",
+ " • Check that the SSH key you added in Dokploy is the same one installed on the server (e.g. in ~/.ssh/authorized_keys).",
+ " • Try generating a new SSH key in Dokploy and add only the public key to the server, then try again.",
+ " • Make sure to follow the instructions on the Setup Server Button on the SSH Keys tab and then click on deployments tab and check the logs for more details.",
+ ].join("\n");
const errorMsg = `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`;
- onData?.(errorMsg);
+ onData?.(friendlyMessage);
reject(
- new ExecError(errorMsg, {
- command,
- serverId,
- originalError: err,
- }),
+ new ExecError(
+ `Authentication failed: Invalid SSH private key. ${friendlyMessage}`,
+ {
+ command,
+ serverId,
+ originalError: err,
+ },
+ ),
);
} else {
const errorMsg = `SSH connection error: ${err.message}`;