diff --git a/apps/web/src/app/[lang]/dho/[id]/_components/select-settings-action.tsx b/apps/web/src/app/[lang]/dho/[id]/_components/select-settings-action.tsx
index 0abd9f8e9..7cea6f63e 100644
--- a/apps/web/src/app/[lang]/dho/[id]/_components/select-settings-action.tsx
+++ b/apps/web/src/app/[lang]/dho/[id]/_components/select-settings-action.tsx
@@ -4,7 +4,6 @@ import { SelectAction, useActionGating } from '@hypha-platform/epics';
import { Locale } from '@hypha-platform/i18n';
import { isAbsoluteUrl } from '@hypha-platform/ui-utils';
import {
- ArchiveIcon,
ArrowDownIcon,
ArrowLeftIcon,
ArrowRightIcon,
@@ -68,16 +67,6 @@ export const SelectSettingsAction = ({
baseTab: 'agreements',
icon: ,
},
- {
- group: 'Overview',
- title: 'Archive Space (Coming Soon)',
- description:
- 'Archive this space to disable activity while preserving its data and history.',
- href: '#',
- icon: ,
- baseTab: 'members',
- disabled: true,
- },
{
defaultDurationDays: 4,
group: 'Agreements',
diff --git a/apps/web/src/app/[lang]/dho/[id]/layout.tsx b/apps/web/src/app/[lang]/dho/[id]/layout.tsx
index 9eba52a44..79aa7e5a3 100644
--- a/apps/web/src/app/[lang]/dho/[id]/layout.tsx
+++ b/apps/web/src/app/[lang]/dho/[id]/layout.tsx
@@ -27,6 +27,7 @@ import {
DEFAULT_SPACE_LEAD_IMAGE,
fetchSpaceDetails,
fetchSpaceProposalsIds,
+ isSpaceArchived,
} from '@hypha-platform/core/client';
import { notFound } from 'next/navigation';
import { db } from '@hypha-platform/storage-postgres';
@@ -165,6 +166,9 @@ export default async function DhoLayout({
web3SpaceId={spaceFromDb.web3SpaceId as number}
isSandbox={spaceFromDb.flags.includes('sandbox')}
isDemo={spaceFromDb.flags.includes('demo')}
+ isArchived={
+ spaceFromDb.flags.includes('archived') || spaceMembers === 0
+ }
configPath={`${getDhoPathAgreements(
lang,
daoSlug,
@@ -202,6 +206,7 @@ export default async function DhoLayout({
title={space.title as string}
isSandbox={space.flags?.includes('sandbox') ?? false}
isDemo={space.flags?.includes('demo') ?? false}
+ isArchived={isSpaceArchived(space)}
web3SpaceId={space.web3SpaceId as number}
configPath={`${getDhoPathAgreements(
lang,
diff --git a/apps/web/src/app/[lang]/my-spaces/page.tsx b/apps/web/src/app/[lang]/my-spaces/page.tsx
index b124e0d78..0205185e6 100644
--- a/apps/web/src/app/[lang]/my-spaces/page.tsx
+++ b/apps/web/src/app/[lang]/my-spaces/page.tsx
@@ -4,6 +4,7 @@ import {
SpaceSearch,
AuthenticatedLinkButton,
} from '@hypha-platform/epics';
+import { isSpaceArchived } from '@hypha-platform/core/client';
import Link from 'next/link';
import { Locale } from '@hypha-platform/i18n';
import {
@@ -91,6 +92,7 @@ export default async function Index(props: PageProps) {
title={space.title as string}
isSandbox={space.flags?.includes('sandbox') ?? false}
isDemo={space.flags?.includes('demo') ?? false}
+ isArchived={isSpaceArchived(space)}
web3SpaceId={space.web3SpaceId as number}
createdAt={space.createdAt}
configPath={`${getDhoPathAgreements(
diff --git a/apps/web/src/app/[lang]/network/page.tsx b/apps/web/src/app/[lang]/network/page.tsx
index aa72b91cc..a0a0265fd 100644
--- a/apps/web/src/app/[lang]/network/page.tsx
+++ b/apps/web/src/app/[lang]/network/page.tsx
@@ -52,6 +52,7 @@ export default async function Index(props: PageProps) {
const spaces = await getAllSpaces({
search: query?.trim() || undefined,
parentOnly: false,
+ omitArchived: true,
});
const uniqueCategories = extractUniqueCategories(spaces);
diff --git a/packages/core/src/categories/types.ts b/packages/core/src/categories/types.ts
index f07bf6683..84627df8b 100644
--- a/packages/core/src/categories/types.ts
+++ b/packages/core/src/categories/types.ts
@@ -49,5 +49,5 @@ export type SpaceOrder = (typeof SPACE_ORDERS)[number];
/**
* @todo fix duplication. Origin at `packages/storage-postgres/src/schema/flags.ts`
*/
-export const SPACE_FLAGS = ['sandbox', 'demo'] as const;
+export const SPACE_FLAGS = ['sandbox', 'demo', 'archived'] as const;
export type SpaceFlags = (typeof SPACE_FLAGS)[number];
diff --git a/packages/core/src/space/index.ts b/packages/core/src/space/index.ts
index 9c24fd80c..a9ace402b 100644
--- a/packages/core/src/space/index.ts
+++ b/packages/core/src/space/index.ts
@@ -1,3 +1,4 @@
export * from './client';
export * from './types';
+export * from './utils';
export * from './validation';
diff --git a/packages/core/src/space/server/queries.ts b/packages/core/src/space/server/queries.ts
index 0620959e4..f95404e64 100644
--- a/packages/core/src/space/server/queries.ts
+++ b/packages/core/src/space/server/queries.ts
@@ -19,11 +19,17 @@ type FindAllSpacesProps = {
search?: string;
parentOnly?: boolean;
omitSandbox?: boolean;
+ omitArchived?: boolean;
};
export const findAllSpaces = async (
{ db }: DbConfig,
- { search, parentOnly = true, omitSandbox = false }: FindAllSpacesProps,
+ {
+ search,
+ parentOnly = true,
+ omitSandbox = false,
+ omitArchived = false,
+ }: FindAllSpacesProps,
) => {
const results = await db
.select({
@@ -50,6 +56,9 @@ export const findAllSpaces = async (
omitSandbox
? not(sql`${spaces.flags} @> '["sandbox"]'::jsonb`)
: undefined,
+ omitArchived
+ ? not(sql`${spaces.flags} @> '["archived"]'::jsonb`)
+ : undefined,
search
? sql`(
-- Full-text search for exact word matches (highest priority)
diff --git a/packages/core/src/space/server/web3/get-all-spaces.ts b/packages/core/src/space/server/web3/get-all-spaces.ts
index 972d0e189..fd1fa0c93 100644
--- a/packages/core/src/space/server/web3/get-all-spaces.ts
+++ b/packages/core/src/space/server/web3/get-all-spaces.ts
@@ -2,7 +2,7 @@
import { db } from '@hypha-platform/storage-postgres';
import { findAllSpaces } from '@hypha-platform/core/server';
-import { Space } from '@hypha-platform/core/client';
+import { Space, isSpaceArchived } from '@hypha-platform/core/client';
import {
fetchSpaceDetails,
fetchSpaceProposalsIds,
@@ -13,6 +13,7 @@ interface GetAllSpacesProps {
search?: string;
parentOnly?: boolean;
omitSandbox?: boolean;
+ omitArchived?: boolean;
}
export async function getAllSpaces(
@@ -35,9 +36,10 @@ export async function getAllSpaces(
const details = formMap(web3details);
const proposalsIds = formMap(web3proposalsIds);
- return spaces.map((space) => {
+ const enrichedSpaces = spaces.map((space) => {
if (space.web3SpaceId === null) {
- return space;
+ // No web3 data = no on-chain members, treat as archived when filtering
+ return { ...space, memberCount: 0 };
}
const spaceDetails = details.get(BigInt(space.web3SpaceId));
@@ -54,6 +56,12 @@ export async function getAllSpaces(
documentCount: spaceProposals?.accepted.length ?? 0,
};
});
+
+ if (props.omitArchived) {
+ return enrichedSpaces.filter((space) => !isSpaceArchived(space));
+ }
+
+ return enrichedSpaces;
} catch (error) {
throw new Error('Failed to get spaces', {
cause: error instanceof Error ? error : new Error(String(error)),
diff --git a/packages/core/src/space/utils.ts b/packages/core/src/space/utils.ts
new file mode 100644
index 000000000..4bca9b5e7
--- /dev/null
+++ b/packages/core/src/space/utils.ts
@@ -0,0 +1,15 @@
+/**
+ * Determines if a space should be considered archived.
+ * A space is archived if:
+ * - It has the 'archived' flag, OR
+ * - It has 0 members (memberCount is explicitly 0 from Web3 data)
+ */
+export function isSpaceArchived(space: {
+ flags?: string[];
+ memberCount?: number;
+}): boolean {
+ return (
+ space.flags?.includes('archived') === true ||
+ (space.memberCount !== undefined && space.memberCount === 0)
+ );
+}
diff --git a/packages/epics/src/spaces/components/create-space-form.tsx b/packages/epics/src/spaces/components/create-space-form.tsx
index f59e5ded4..8be6fcc1e 100644
--- a/packages/epics/src/spaces/components/create-space-form.tsx
+++ b/packages/epics/src/spaces/components/create-space-form.tsx
@@ -300,16 +300,23 @@ export const SpaceForm = ({
[flags],
);
const isDemo = React.useMemo(() => flags?.includes('demo') ?? false, [flags]);
+ const isArchived = React.useMemo(
+ () => flags?.includes('archived') ?? false,
+ [flags],
+ );
const isLive = React.useMemo(
- () => !isDemo && !isSandbox,
- [isDemo, isSandbox],
+ () => !isDemo && !isSandbox && !isArchived,
+ [isDemo, isSandbox, isArchived],
);
const toggleSandbox = React.useCallback(() => {
const current = form.getValues().flags ?? [];
const next = current.includes('sandbox')
? current.filter((f) => f !== 'sandbox')
- : (['sandbox', ...current.filter((f) => f !== 'demo')] as SpaceFlags[]);
+ : ([
+ 'sandbox',
+ ...current.filter((f) => f !== 'demo' && f !== 'archived'),
+ ] as SpaceFlags[]);
form.setValue('flags', next, { shouldDirty: true, shouldValidate: true });
if (next.includes('sandbox')) {
form.clearErrors('categories');
@@ -320,13 +327,29 @@ export const SpaceForm = ({
const current = form.getValues().flags ?? [];
const next = current.includes('demo')
? current.filter((f) => f !== 'demo')
- : (['demo', ...current.filter((f) => f !== 'sandbox')] as SpaceFlags[]);
+ : ([
+ 'demo',
+ ...current.filter((f) => f !== 'sandbox' && f !== 'archived'),
+ ] as SpaceFlags[]);
+ form.setValue('flags', next, { shouldDirty: true, shouldValidate: true });
+ }, [form]);
+
+ const toggleArchived = React.useCallback(() => {
+ const current = form.getValues().flags ?? [];
+ const next = current.includes('archived')
+ ? current.filter((f) => f !== 'archived')
+ : ([
+ 'archived',
+ ...current.filter((f) => f !== 'demo' && f !== 'sandbox'),
+ ] as SpaceFlags[]);
form.setValue('flags', next, { shouldDirty: true, shouldValidate: true });
}, [form]);
const toggleLive = React.useCallback(() => {
const current = form.getValues().flags ?? [];
- const next = current.filter((f) => f !== 'demo' && f !== 'sandbox');
+ const next = current.filter(
+ (f) => f !== 'demo' && f !== 'sandbox' && f !== 'archived',
+ );
form.setValue('flags', next, { shouldDirty: true, shouldValidate: true });
}, [form]);
@@ -363,6 +386,7 @@ export const SpaceForm = ({
async (space) => {
if (
!space.flags?.includes('sandbox') &&
+ !space.flags?.includes('archived') &&
space.categories.length === 0
) {
showCategoriesError();
@@ -377,7 +401,11 @@ export const SpaceForm = ({
(e) => {
const flags = form.getValues()['flags'];
const categories = form.getValues()['categories'];
- if (!flags?.includes('sandbox') && categories.length === 0) {
+ if (
+ !flags?.includes('sandbox') &&
+ !flags?.includes('archived') &&
+ categories.length === 0
+ ) {
showCategoriesError();
}
if (parentSpaceId === -1) {
@@ -678,6 +706,30 @@ export const SpaceForm = ({
+ {label === 'configure' && (
+
+
+ Archive Mode
+
+
+ Archive this space to temporarily pause activity or
+ deactivate it while keeping all data and history safe. You
+ can reactivate it anytime by selecting a different
+ activation mode.
+
+
+