fix (dashboard): disable settings when there is no config server env variable present (#3394)

### **PR Type**
Bug fix, Enhancement


___

### **Description**
- Disable settings when no config server variable

- Improve platform-specific page handling

- Enhance error handling and loading states

- Refactor environment variable checks


___

### Diagram Walkthrough


```mermaid
flowchart LR
  A["Environment Check"] --> B["Settings Visibility"]
  A --> C["Platform-specific Pages"]
  D["Error Handling"] --> E["Loading States"]
  F["Code Refactoring"]
```



<details> <summary><h3> File Walkthrough</h3></summary>

<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody></tr></tbody></table>

</details>

___

---------

Co-authored-by: David Barroso <dbarrosop@dravetech.com>
This commit is contained in:
robertkasza
2025-07-25 13:05:04 +02:00
committed by GitHub
parent f9e170e958
commit 4ffff86752
20 changed files with 242 additions and 139 deletions

View File

@@ -0,0 +1,5 @@
---
'@nhost/dashboard': minor
---
fix (dashboard): Disable settings pages when config server env variable is not set

View File

@@ -21,6 +21,6 @@ find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_STORAGE_URL__~${NEXT_
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL__~${NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL}~g" {} +
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL__~${NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL}~g" {} +
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_API_URL__~${NEXT_PUBLIC_NHOST_HASURA_API_URL}~g" {} +
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_CONFIGSERVER_URL__~${NEXT_PUBLIC_NHOST_CONFIGSERVER_URL}~g" {} +
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_CONFIGSERVER_URL__~${NEXT_PUBLIC_NHOST_CONFIGSERVER_URL:-""}~g" {} +
exec "$@"

View File

@@ -34,7 +34,7 @@ export default function AuthenticatedLayout({
const isMdOrLarger = useMediaQuery('md');
const { isAuthenticated, isLoading } = useAuth();
const isHealthy = useIsHealthy();
const { isHealthy, isLoading: isHealthyLoading } = useIsHealthy();
const [mainNavContainer, setMainNavContainer] = useState(null);
const { mainNavPinned } = useTreeNavState();
@@ -70,7 +70,7 @@ export default function AuthenticatedLayout({
);
}
if (!isPlatform && !isHealthy) {
if (!isPlatform && !isHealthy && !isHealthyLoading) {
return (
<BaseLayout className="h-full" {...props}>
<Header className="flex max-h-[59px] flex-auto" />

View File

@@ -12,9 +12,9 @@ import { StorageIcon } from '@/components/ui/v2/icons/StorageIcon';
import { UserIcon } from '@/components/ui/v2/icons/UserIcon';
import { Badge } from '@/components/ui/v3/badge';
import { Button } from '@/components/ui/v3/button';
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
import { useOrgs, type Org } from '@/features/orgs/projects/hooks/useOrgs';
import { cn } from '@/lib/utils';
import { getConfigServerUrl, isPlatform as getIsPlatform } from '@/utils/env';
import { Box, ChevronDown, ChevronRight, Plus } from 'lucide-react';
import Link from 'next/link';
import { useMemo, type ReactElement } from 'react';
@@ -160,7 +160,12 @@ const projectSettingsPages = [
{ name: 'Configuration Editor', slug: 'editor', route: 'editor' },
];
const createOrganization = (org: Org, isPlatform: boolean) => {
const createOrganization = (org: Org) => {
const isNotPlatform = !getIsPlatform();
const configServerVariableNotSet = getConfigServerUrl() === '';
const shouldDisableSettings = isNotPlatform && configServerVariableNotSet;
const shouldDisableGraphite = shouldDisableSettings;
const result = {};
result[org.slug] = {
@@ -211,7 +216,7 @@ const createOrganization = (org: Org, isPlatform: boolean) => {
slug: 'new',
icon: <Plus className="mr-1 h-4 w-4 font-bold" strokeWidth={3} />,
targetUrl: `/orgs/${org.slug}/projects/new`,
disabled: !isPlatform,
disabled: isNotPlatform,
},
};
@@ -238,9 +243,9 @@ const createOrganization = (org: Org, isPlatform: boolean) => {
result[`${org.slug}-${_app.subdomain}-${_page.slug}`] = {
index: `${org.slug}-${_app.subdomain}-${_page.slug}`,
canMove: false,
isFolder: _page.name === 'Settings',
isFolder: _page.name === 'Settings' && !shouldDisableSettings,
children:
_page.name === 'Settings'
_page.name === 'Settings' && !shouldDisableSettings
? projectSettingsPages.map(
(p) => `${org.slug}-${_app.subdomain}-settings-${p.slug}`,
)
@@ -251,9 +256,12 @@ const createOrganization = (org: Org, isPlatform: boolean) => {
isProjectPage: true,
targetUrl: `/orgs/${org.slug}/projects/${_app.subdomain}/${_page.route}`,
disabled:
['deployments', 'backups', 'logs', 'metrics'].includes(
(['deployments', 'backups', 'logs', 'metrics'].includes(
_page.slug,
) && !isPlatform,
) &&
isNotPlatform) ||
(_page.name === 'Settings' && shouldDisableSettings) ||
(_page.name === 'AI' && shouldDisableGraphite),
},
canRename: false,
};
@@ -272,6 +280,7 @@ const createOrganization = (org: Org, isPlatform: boolean) => {
p.slug === 'general'
? `/orgs/${org.slug}/projects/${_app.subdomain}/settings`
: `/orgs/${org.slug}/projects/${_app.subdomain}/settings/${p.route}`,
disabled: shouldDisableSettings,
},
canRename: false,
};
@@ -286,7 +295,7 @@ const createOrganization = (org: Org, isPlatform: boolean) => {
data: {
name: 'Settings',
targetUrl: `/orgs/${org.slug}/settings`,
disabled: !isPlatform,
disabled: isNotPlatform,
},
canRename: false,
};
@@ -299,7 +308,7 @@ const createOrganization = (org: Org, isPlatform: boolean) => {
data: {
name: 'Members',
targetUrl: `/orgs/${org.slug}/members`,
disabled: !isPlatform,
disabled: isNotPlatform,
},
canRename: false,
};
@@ -312,7 +321,7 @@ const createOrganization = (org: Org, isPlatform: boolean) => {
data: {
name: 'Billing',
targetUrl: `/orgs/${org.slug}/billing`,
disabled: !isPlatform,
disabled: isNotPlatform,
},
canRename: false,
};
@@ -333,7 +342,6 @@ type NavItem = {
const buildNavTreeData = (
org: Org,
isPlatform: boolean,
): { items: Record<TreeItemIndex, TreeItem<NavItem>> } => {
if (!org) {
return {
@@ -365,7 +373,7 @@ const buildNavTreeData = (
data: { name: 'root' },
canRename: false,
},
...createOrganization(org, isPlatform),
...createOrganization(org),
},
};
@@ -374,11 +382,8 @@ const buildNavTreeData = (
export default function NavTree() {
const { currentOrg: org } = useOrgs();
const isPlatform = useIsPlatform();
const navTree = useMemo(
() => buildNavTreeData(org, isPlatform),
[org, isPlatform],
);
// eslint-disable-next-line react-hooks/exhaustive-deps
const navTree = useMemo(() => buildNavTreeData(org), [org?.slug]);
const { orgsTreeViewState, setOrgsTreeViewState, setOpen } =
useTreeNavState();

View File

@@ -19,11 +19,15 @@ export default function SocialProvidersSettings() {
);
const github = useMemo(
() =>
nhost.auth.signInProviderURL('github', {
connect: token,
redirectTo: `${window.location.origin}/account`,
}),
() => {
if (typeof window !== 'undefined') {
return nhost.auth.signInProviderURL('github', {
connect: token,
redirectTo: `${window.location.origin}/account`,
});
}
return '';
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[token],
);

View File

@@ -36,7 +36,8 @@ export default function TransferOrUpgradeProjectDialog({
}, [session_id, setOpen, isRouterReady]);
const path = asPath.split('?')[0];
const redirectUrl = `${window.location.origin}${path}`;
const redirectUrl =
typeof window !== 'undefined' ? `${window.location.origin}${path}` : '';
const handleCreateDialogOpenStateChange = (newState: boolean) => {
setShowCreateOrgModal(newState);

View File

@@ -16,11 +16,36 @@ import { useAppState } from '@/features/orgs/projects/common/hooks/useAppState';
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
import { useProjectWithState } from '@/features/orgs/projects/hooks/useProjectWithState';
import { ApplicationStatus } from '@/types/application';
import { getConfigServerUrl, isPlatform as isPlatformFn } from '@/utils/env';
import { NextSeo } from 'next-seo';
import { useRouter } from 'next/router';
import { useCallback, useMemo, type ReactNode } from 'react';
import { useCallback, useEffect, useMemo, type ReactNode } from 'react';
import { twMerge } from 'tailwind-merge';
const platFormOnlyPages = [
'/orgs/[orgSlug]/projects/[appSubdomain]/deployments',
'/orgs[orgSlug]/projects/[appSubdomain]/backups',
'/orgs/[orgSlug]/projects/[appSubdomain]/logs',
'/orgs/[orgSlug]/projects/[appSubdomain]/metrics',
'/orgs/[orgSlug]/projects/[appSubdomain]/deployments/[deploymentId]',
];
function isSelfHostedAndGraphitePage(route: string) {
const isGraphitePage = route.startsWith(
'/orgs/[orgSlug]/projects/[appSubdomain]/ai',
);
const isConfigEnvVariableNotSet = getConfigServerUrl() === '';
return isGraphitePage && isConfigEnvVariableNotSet;
}
function isPlatformOnlyPage(route: string) {
const platFormOnlyPage = !!platFormOnlyPages.find((page) => route === page);
const isNotPlatform = !isPlatformFn();
return isNotPlatform && platFormOnlyPage;
}
export interface ProjectLayoutContentProps extends AuthenticatedLayoutProps {
/**
* Props passed to the internal `<main />` element.
@@ -35,10 +60,12 @@ function ProjectLayoutContent({
const {
route,
query: { appSubdomain },
push,
} = useRouter();
const { state } = useAppState();
const isPlatform = useIsPlatform();
const { project, loading, error } = useProjectWithState();
const isOnOverviewPage = route === '/orgs/[orgSlug]/projects/[appSubdomain]';
@@ -132,6 +159,16 @@ function ProjectLayoutContent({
renderPausedProjectContent,
]);
useEffect(() => {
if (isPlatformOnlyPage(route) || isSelfHostedAndGraphitePage(route)) {
push('/404');
}
}, [route, push]);
if (isPlatformOnlyPage(route) || isSelfHostedAndGraphitePage(route)) {
return null;
}
// Handle loading state
if (loading) {
return <LoadingScreen />;

View File

@@ -3,14 +3,29 @@ import { Alert } from '@/components/ui/v2/Alert';
import { Box } from '@/components/ui/v2/Box';
import { Text } from '@/components/ui/v2/Text';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { useSettingsDisabled } from '@/hooks/useSettingsDisabled';
import { useTheme } from '@mui/material';
import { useRouter } from 'next/router';
import type { PropsWithChildren } from 'react';
import { useEffect } from 'react';
import { twMerge } from 'tailwind-merge';
export default function SettingsLayout({ children }: PropsWithChildren) {
const theme = useTheme();
const { project } = useProject();
const hasGitRepo = !!project?.githubRepository;
const isSettingsDisabled = useSettingsDisabled();
const router = useRouter();
useEffect(() => {
if (isSettingsDisabled) {
router.push('/404');
}
}, [router, isSettingsDisabled]);
if (isSettingsDisabled) {
return null;
}
return (
<Box

View File

@@ -165,6 +165,8 @@ export default function DevAssistant() {
);
}
const slug = isPlatform ? currentOrg?.slug : 'local';
if (
(isPlatform && !currentOrg?.plan?.isFree && !project?.config?.ai) ||
!isGraphiteEnabled
@@ -176,7 +178,7 @@ export default function DevAssistant() {
<Text component="span">
To enable graphite, configure the service first in{' '}
<Link
href={`/orgs/${currentOrg?.slug}/projects/${project?.subdomain}/settings/ai`}
href={`/orgs/${slug}/projects/${project?.subdomain}/settings/ai`}
target="_blank"
rel="noopener noreferrer"
underline="hover"

View File

@@ -16,7 +16,7 @@ export default function useIsHealthy() {
'hasura',
);
const { failureCount, status } = useQuery(
const { failureCount, status, isLoading } = useQuery(
['/v1/version'],
() => fetch(`${appUrl}/v1/version`),
{
@@ -27,5 +27,8 @@ export default function useIsHealthy() {
},
);
return isPlatform || (status === 'success' && failureCount === 0);
return {
isHealthy: isPlatform || (status === 'success' && failureCount === 0),
isLoading,
};
}

View File

@@ -2,7 +2,7 @@ import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
import { render, screen, waitFor } from '@/tests/testUtils';
import { graphql } from 'msw';
import { setupServer } from 'msw/node';
import { beforeAll, expect, test } from 'vitest';
import { beforeAll, expect, test, vi } from 'vitest';
import HasuraCorsDomainSettings from './HasuraCorsDomainSettings';
const server = setupServer(
@@ -22,9 +22,11 @@ const server = setupServer(
enableConsole: false,
devMode: false,
enabledAPIs: [],
inferFunctionPermissions: false,
},
logs: [],
events: [],
resources: [],
},
},
}),
@@ -32,62 +34,70 @@ const server = setupServer(
),
);
beforeAll(() => {
server.listen();
});
describe('HasuraCorsDomainSettings', () => {
vi.stubEnv(
'NEXT_PUBLIC_NHOST_CONFIGSERVER_URL',
'https://my-config-server.com',
);
beforeAll(() => {
server.listen();
});
afterEach(() => {
server.resetHandlers();
});
afterEach(() => {
server.resetHandlers();
});
afterAll(() => {
server.close();
});
afterAll(() => {
server.close();
});
test('should not enable switch by default when CORS domain is set to *', async () => {
render(<HasuraCorsDomainSettings />);
test('should not enable switch by default when CORS domain is set to *', async () => {
render(<HasuraCorsDomainSettings />);
expect(await screen.findByText(/configure cors/i)).toBeInTheDocument();
expect(await screen.findByText(/configure cors/i)).toBeInTheDocument();
expect(screen.getByRole('checkbox')).not.toBeChecked();
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
});
expect(screen.getByRole('checkbox')).not.toBeChecked();
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
});
test('should enable switch by default when CORS domain is set to one or more domains', async () => {
server.use(
graphql.query('GetHasuraSettings', (_req, res, ctx) =>
res(
ctx.data({
config: {
id: 'HasuraSettings',
__typename: 'HasuraSettings',
hasura: {
version: 'v2.25.1-ce',
settings: {
corsDomain: ['https://example.com', 'https://*.example.com'],
enableAllowList: false,
enableRemoteSchemaPermissions: false,
enableConsole: false,
devMode: false,
enabledAPIs: [],
test('should enable switch by default when CORS domain is set to one or more domains', async () => {
server.use(
graphql.query('GetHasuraSettings', (_req, res, ctx) =>
res(
ctx.data({
config: {
id: 'HasuraSettings',
__typename: 'HasuraSettings',
hasura: {
version: 'v2.25.1-ce',
settings: {
corsDomain: ['https://example.com', 'https://*.example.com'],
enableAllowList: false,
enableRemoteSchemaPermissions: false,
enableConsole: false,
devMode: false,
enabledAPIs: [],
inferFunctionPermissions: false,
},
logs: [],
events: [],
resources: [],
},
logs: [],
events: [],
},
},
}),
}),
),
),
),
);
);
render(<HasuraCorsDomainSettings />);
render(<HasuraCorsDomainSettings />);
expect(await screen.findByText(/configure cors/i)).toBeInTheDocument();
expect(await screen.findByText(/configure cors/i)).toBeInTheDocument();
await waitFor(() => expect(screen.getByRole('checkbox')).toBeChecked());
await waitFor(() => expect(screen.getByRole('checkbox')).toBeChecked());
expect(screen.getByRole('textbox')).toBeInTheDocument();
expect(screen.getByRole('textbox')).toHaveValue(
'https://example.com, https://*.example.com',
);
expect(screen.getByRole('textbox')).toBeInTheDocument();
expect(screen.getByRole('textbox')).toHaveValue(
'https://example.com, https://*.example.com',
);
});
});

View File

@@ -1,7 +1,7 @@
import { type Org } from '@/features/orgs/projects/hooks/useOrgs';
import { ApplicationStatus } from '@/types/application';
import { getHasuraAdminSecret } from '@/utils/env';
import { type GetProjectQuery } from '@/utils/__generated__/graphql';
import { getHasuraAdminSecret } from '@/utils/env';
export const localApplication: GetProjectQuery['apps'][0] = {
id: '00000000-0000-0000-0000-000000000000',

View File

@@ -0,0 +1 @@
export { default as useSettingsDisabled } from './useSettingsDisabled';

View File

@@ -0,0 +1,8 @@
import { getConfigServerUrl, isPlatform } from '@/utils/env';
export default function useSettingsDisabled() {
const noConfigServerEnvVariableSet = getConfigServerUrl() === '';
const notPlatform = !isPlatform();
return notPlatform && noConfigServerEnvVariableSet;
}

View File

@@ -109,6 +109,7 @@ export default function AssistantsPage() {
</Container>
);
}
const slug = isPlatform ? org?.slug : 'local';
if (
(isPlatform && !org?.plan?.isFree && !project.config?.ai) ||
@@ -124,7 +125,7 @@ export default function AssistantsPage() {
<Text component="span">
To enable graphite, configure the service first in{' '}
<Link
href={`/orgs/${org?.slug}/projects/${project?.subdomain}/settings/ai`}
href={`/orgs/${slug}/projects/${project?.subdomain}/settings/ai`}
rel="noopener noreferrer"
underline="hover"
>

View File

@@ -101,6 +101,8 @@ export default function AutoEmbeddingsPage() {
);
}
const slug = isPlatform ? org?.slug : 'local';
if (
(isPlatform && !org?.plan?.isFree && !project?.config?.ai) ||
!isGraphiteEnabled
@@ -115,7 +117,7 @@ export default function AutoEmbeddingsPage() {
<Text component="span">
To enable graphite, configure the service first in{' '}
<Link
href={`/orgs/${org?.slug}/projects/${project?.subdomain}/settings/ai`}
href={`/orgs/${slug}/projects/${project?.subdomain}/settings/ai`}
rel="noopener noreferrer"
underline="hover"
>

View File

@@ -83,6 +83,8 @@ export default function FileStoresPage() {
);
}
const slug = isPlatform ? org?.slug : 'local';
if (
(isPlatform && !org?.plan?.isFree && !project.config?.ai) ||
!isGraphiteEnabled
@@ -97,7 +99,7 @@ export default function FileStoresPage() {
<Text component="span">
To enable graphite, configure the service first in{' '}
<Link
href={`/orgs/${org?.slug}/projects/${project?.subdomain}/settings/ai`}
href={`/orgs/${slug}/projects/${project?.subdomain}/settings/ai`}
rel="noopener noreferrer"
underline="hover"
>

View File

@@ -23,6 +23,7 @@ import { GraphiQLInterface } from 'graphiql';
import 'graphiql/graphiql.min.css';
import { createClient } from 'graphql-ws';
import debounce from 'lodash.debounce';
import dynamic from 'next/dynamic';
import type { ReactElement } from 'react';
import { useEffect, useMemo, useState } from 'react';
@@ -209,66 +210,76 @@ function GraphiQLEditor({ onHeaderChange }: GraphiQLEditorProps) {
);
}
export default function GraphQLPage() {
const { project } = useProject();
const [userHeaders, setUserHeaders] = useState<Record<string, any>>({});
const GraphQLPageContent = dynamic(
() =>
Promise.resolve(() => {
const { project } = useProject();
const [userHeaders, setUserHeaders] = useState<Record<string, any>>({});
if (!project?.subdomain || !project?.config?.hasura.adminSecret) {
return <LoadingScreen />;
}
if (!project?.subdomain || !project?.config?.hasura.adminSecret) {
return <LoadingScreen />;
}
const appUrl = generateAppServiceUrl(
project.subdomain,
project.region,
'graphql',
);
const appUrl = generateAppServiceUrl(
project.subdomain,
project.region,
'graphql',
);
const subscriptionUrl = `${appUrl
.replace('https', 'wss')
.replace('http', 'ws')}`;
const subscriptionUrl = `${appUrl
.replace('https', 'wss')
.replace('http', 'ws')}`;
const headers = {
'content-type': 'application/json',
'x-hasura-admin-secret': project.config?.hasura.adminSecret,
...userHeaders,
};
const headers = {
'content-type': 'application/json',
'x-hasura-admin-secret': project.config?.hasura.adminSecret,
...userHeaders,
};
const fetcher = createGraphiQLFetcher({
url: appUrl,
headers,
wsClient: createClient({
url: subscriptionUrl,
keepAlive: 2000,
connectionParams: {
const fetcher = createGraphiQLFetcher({
url: appUrl,
headers,
},
wsClient: createClient({
url: subscriptionUrl,
keepAlive: 2000,
connectionParams: {
headers,
},
}),
});
function handleUserChange(userId: string) {
setUserHeaders((currentHeaders) => ({
...currentHeaders,
'x-hasura-user-id': userId,
}));
}
function handleRoleChange(role: string) {
setUserHeaders((currentHeaders) => ({
...currentHeaders,
'x-hasura-role': role,
}));
}
return (
<GraphiQLProvider fetcher={fetcher} shouldPersistHeaders>
<GraphiQLHeader
onUserChange={handleUserChange}
onRoleChange={handleRoleChange}
/>
<GraphiQLEditor onHeaderChange={setUserHeaders} />
</GraphiQLProvider>
);
}),
});
function handleUserChange(userId: string) {
setUserHeaders((currentHeaders) => ({
...currentHeaders,
'x-hasura-user-id': userId,
}));
}
function handleRoleChange(role: string) {
setUserHeaders((currentHeaders) => ({
...currentHeaders,
'x-hasura-role': role,
}));
}
{ ssr: false },
);
export default function GraphQLPage() {
return (
<RetryableErrorBoundary>
<GraphiQLProvider fetcher={fetcher} shouldPersistHeaders>
<GraphiQLHeader
onUserChange={handleUserChange}
onRoleChange={handleRoleChange}
/>
<GraphiQLEditor onHeaderChange={setUserHeaders} />
</GraphiQLProvider>
<GraphQLPageContent />
</RetryableErrorBoundary>
);
}

View File

@@ -95,8 +95,5 @@ export function getHasuraApiUrl() {
* Custom URL of the config service.
*/
export function getConfigServerUrl() {
return (
process.env.NEXT_PUBLIC_NHOST_CONFIGSERVER_URL ||
'https://local.dashboard.local.nhost.run/v1/configserver/graphql'
);
return process.env.NEXT_PUBLIC_NHOST_CONFIGSERVER_URL;
}

View File

@@ -1,6 +1,6 @@
services:
auth:
image: nhost/hasura-auth:0.37.1
image: nhost/hasura-auth:0.40.2
depends_on:
graphql:
condition: service_healthy
@@ -96,7 +96,7 @@ services:
read_only: false
console:
image: nhost/graphql-engine:v2.36.9-ce.cli-migrations-v3
image: nhost/graphql-engine:v2.46.0-ce.cli-migrations-v3
depends_on:
graphql:
condition: service_healthy
@@ -179,7 +179,6 @@ services:
NEXT_PUBLIC_ENV: dev
NEXT_PUBLIC_NHOST_ADMIN_SECRET: ${GRAPHQL_ADMIN_SECRET}
NEXT_PUBLIC_NHOST_AUTH_URL: http://${AUTH_URL}/v1
NEXT_PUBLIC_NHOST_CONFIGSERVER_URL: http://unused.in.self.hosting
NEXT_PUBLIC_NHOST_FUNCTIONS_URL: http://${FUNCTIONS_URL}/v1
NEXT_PUBLIC_NHOST_GRAPHQL_URL: http://${GRAPHQL_URL}/v1/graphql
NEXT_PUBLIC_NHOST_HASURA_API_URL: http://${GRAPHQL_URL}
@@ -255,7 +254,7 @@ services:
read_only: false
graphql:
image: nhost/graphql-engine:v2.36.9-ce
image: nhost/graphql-engine:v2.46.0-ce
depends_on:
postgres:
condition: service_healthy
@@ -406,7 +405,7 @@ services:
read_only: true
storage:
image: nhost/hasura-storage:0.7.1
image: nhost/hasura-storage:0.7.2
depends_on:
graphql:
condition: service_healthy